base = rtrim($baseUrl, '/'); $this->key = $apiKey; } /** Client aus den Einstellungen; null, solange Adresse oder Schlüssel fehlen. */ public static function from_settings(): ?self { $opts = deskhub_tickets_options(); $url = self::normalize_url((string) $opts['server_url']); if (is_wp_error($url) || trim((string) $opts['api_key']) === '') { return null; } return new self($url, trim((string) $opts['api_key'])); } /** * Bereinigt die Server-Adresse: https ist Pflicht (http nur für lokale Tests), kein Benutzername, keine Parameter. * * @return string|WP_Error */ public static function normalize_url(string $input) { $s = trim($input); if ($s === '') { return new WP_Error('deskhub_url', __('Bitte die Adresse Ihres DeskHub-Servers eintragen.', 'deskhub-tickets')); } if (strpos($s, '://') === false) { $s = 'https://' . $s; } $p = wp_parse_url($s); if (!$p || empty($p['host']) || !empty($p['user']) || !empty($p['pass']) || !empty($p['query']) || !empty($p['fragment'])) { return new WP_Error('deskhub_url', __('Die Server-Adresse ist ungültig.', 'deskhub-tickets')); } $scheme = strtolower((string) ($p['scheme'] ?? '')); $host = strtolower((string) $p['host']); $local = in_array($host, ['localhost', '127.0.0.1', '[::1]', '::1'], true); $insecureOk = $local || (bool) apply_filters('deskhub_tickets_allow_insecure', false, $host); if ($scheme !== 'https' && !($scheme === 'http' && $insecureOk)) { return new WP_Error('deskhub_url', __('Die Server-Adresse muss mit https beginnen.', 'deskhub-tickets')); } $port = isset($p['port']) ? ':' . (int) $p['port'] : ''; $path = isset($p['path']) ? rtrim((string) $p['path'], '/') : ''; return $scheme . '://' . $host . $port . $path; } /** * Legt ein Ticket an. * * @param array{name:string,email:string,subject:string,message:string,helpTopicId?:int,departmentId?:int,priorityId?:int,fields?:array} $data * @return array{number:string,status:string}|WP_Error */ public function create_ticket(array $data) { $fields = $data['fields'] ?? null; $attachments = $data['attachments'] ?? null; unset($data['fields'], $data['attachments']); $payload = array_filter($data, static function ($v) { return $v !== null && $v !== '' && $v !== 0; }); if (is_array($fields)) { $payload['fields'] = (object) $fields; // auch leer senden: Damit prüft DeskHub die Angaben gegen das Kundenformular } if (is_array($attachments) && $attachments) { $payload['attachments'] = array_values($attachments); } $res = $this->request('POST', [], $payload); if (is_wp_error($res)) { return $res; } if (empty($res['number']) || !is_string($res['number'])) { return new WP_Error('deskhub_response', __('Der Server hat eine unerwartete Antwort geliefert.', 'deskhub-tickets')); } return ['number' => $res['number'], 'status' => (string) ($res['status'] ?? 'open')]; } /** * Das Kundenformular aus DeskHub für ein Hilfethema oder eine Abteilung: eigene Felder, Texte, Reihenfolge, Pflichtfelder. * Das Ergebnis wird kurz zwischengespeichert, damit nicht jeder Seitenaufruf den Server fragt. * Kennt der Server die Schnittstelle noch nicht (ältere DeskHub-Version), kommt ein WP_Error "deskhub_form_unsupported": * Dann zeigt das Plugin das einfache Formular. * * @return array|WP_Error */ public function get_form(?int $topicId, ?int $deptId) { $key = 'dht_form_' . md5($this->base . '|' . (int) $topicId . '|' . (int) $deptId); $cached = get_transient($key); if (is_array($cached)) { return !empty($cached['unsupported']) ? new WP_Error('deskhub_form_unsupported', __('Der DeskHub-Server kennt die Formular-Schnittstelle noch nicht.', 'deskhub-tickets')) : $cached['form']; } $query = ['form' => '1']; if ($topicId) { $query['helpTopicId'] = (string) $topicId; } if ($deptId) { $query['departmentId'] = (string) $deptId; } $ttl = (int) apply_filters('deskhub_tickets_form_cache_seconds', 300); $res = $this->request('GET', $query); $data = is_wp_error($res) ? $res->get_error_data() : null; $apiCode = is_array($data) ? (string) ($data['api_code'] ?? '') : ''; // Ältere Server lesen "form=1" als Ticketabfrage und antworten "Parameter fehlen"; unbekannte Antworten zählen ebenso als "nicht unterstützt" if ((is_wp_error($res) && $apiCode === 'MISSING_PARAMS') || (!is_wp_error($res) && empty($res['form']))) { set_transient($key, ['unsupported' => true], max(30, $ttl)); return new WP_Error('deskhub_form_unsupported', __('Der DeskHub-Server kennt die Formular-Schnittstelle noch nicht.', 'deskhub-tickets')); } if (is_wp_error($res)) { return $res; } if ($ttl > 0) { set_transient($key, ['form' => $res['form']], $ttl); } return $res['form']; } /** * Die öffentlichen Hilfethemen des Servers (für die Auswahl in den Einstellungen). * * @return array|WP_Error */ public function get_topics() { $res = $this->request('GET', ['topics' => '1']); if (is_wp_error($res)) { return $res; } return isset($res['topics']) && is_array($res['topics']) ? $res['topics'] : new WP_Error('deskhub_form_unsupported', __('Der DeskHub-Server kennt die Formular-Schnittstelle noch nicht.', 'deskhub-tickets')); } /** Leert den Zwischenspeicher der Formulare (zum Beispiel nach einer Änderung in DeskHub). */ public static function clear_form_cache(): void { global $wpdb; if (isset($wpdb) && is_object($wpdb)) { $wpdb->query($wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like('_transient_dht_form_') . '%', $wpdb->esc_like('_transient_timeout_dht_form_') . '%' )); } if (function_exists('wp_cache_flush_group')) { @wp_cache_flush_group('transient'); } } /** * Status und öffentlicher Verlauf eines Tickets (nur mit passender E-Mail-Adresse). * * @return array|WP_Error das Feld "ticket" der Antwort */ public function get_ticket(string $number, string $email) { $res = $this->request('GET', ['number' => $number, 'email' => $email]); if (is_wp_error($res)) { return $res; } if (empty($res['ticket']) || !is_array($res['ticket'])) { return new WP_Error('deskhub_response', __('Der Server hat eine unerwartete Antwort geliefert.', 'deskhub-tickets')); } return $res['ticket']; } /** * Prüft Adresse und Schlüssel, ohne ein Ticket anzulegen. Ein Aufruf mit erfundenen Daten genügt: Antwortet der Server mit * "nicht gefunden" (404) oder "keine Leseberechtigung" (403), ist der Schlüssel gültig. 401 heißt: Schlüssel falsch. * * @return array{ok:bool,message:string,canRead:bool} */ public function test_connection(): array { $res = $this->request('GET', ['number' => 'VERBINDUNGSTEST', 'email' => 'test@example.invalid']); if (!is_wp_error($res)) { return ['ok' => true, 'message' => __('Verbindung steht.', 'deskhub-tickets'), 'canRead' => true]; } $data = $res->get_error_data(); $status = is_array($data) ? (int) ($data['status'] ?? 0) : 0; if ($status === 404) { return ['ok' => true, 'message' => __('Verbindung steht, der Schlüssel ist gültig und darf Tickets lesen.', 'deskhub-tickets'), 'canRead' => true]; } if ($status === 403) { return ['ok' => true, 'message' => __('Verbindung steht, der Schlüssel ist gültig. Er darf aber keine Tickets lesen: Die Statusabfrage funktioniert damit nicht (in DeskHub unter Verwalten → API-Schlüssel das Recht „Tickets lesen“ geben).', 'deskhub-tickets'), 'canRead' => false]; } $apiCode = is_array($data) ? (string) ($data['api_code'] ?? '') : ''; if ($status === 401 && $apiCode === 'IP_NOT_ALLOWED') { // DeskHub nennt die Adresse, die es sieht (bei Cloudflare oder Proxy oft nicht die erwartete) return ['ok' => false, 'message' => sprintf(__('Der Schlüssel ist gültig, aber DeskHub lässt diese Adresse nicht zu. DeskHub meldet: %s', 'deskhub-tickets'), (string) ($data['api_message'] ?? '')), 'canRead' => false]; } if ($status === 401) { return ['ok' => false, 'message' => __('Der Server hat den API-Schlüssel abgelehnt. Bitte den Schlüssel prüfen: Er muss genau so eingetragen sein, aktiv sein und darf bei „Erlaubte IPs“ keine andere Adresse verlangen als die dieses WordPress-Servers (im Zweifel das Feld leer lassen).', 'deskhub-tickets'), 'canRead' => false]; } if ($status === 0) { $detail = is_array($data) && !empty($data['transport']) ? ' (' . (string) $data['transport'] . ')' : ''; return ['ok' => false, 'message' => $res->get_error_message() . $detail, 'canRead' => false]; } return ['ok' => false, 'message' => sprintf(__('Der Server antwortet mit einem Fehler (%d).', 'deskhub-tickets'), $status), 'canRead' => false]; } /** * Freundliche Meldung für Besucher. Technische Einzelheiten (auch der Schlüssel) gelangen nie in die Seite. */ public static function user_message(WP_Error $e): string { $data = $e->get_error_data(); $api = is_array($data) ? (string) ($data['api_code'] ?? '') : ''; switch ($api) { case 'INVALID_NAME': return __('Bitte geben Sie Ihren Namen an (höchstens 120 Zeichen).', 'deskhub-tickets'); case 'INVALID_EMAIL': return __('Bitte geben Sie eine gültige E-Mail-Adresse an.', 'deskhub-tickets'); case 'INVALID_SUBJECT': return __('Bitte geben Sie einen Betreff an (höchstens 255 Zeichen).', 'deskhub-tickets'); case 'INVALID_MESSAGE': return __('Bitte beschreiben Sie Ihr Anliegen.', 'deskhub-tickets'); case 'BANNED': case 'REJECTED': return __('Ihre Anfrage konnte leider nicht angenommen werden.', 'deskhub-tickets'); case 'RATE_LIMITED': return __('Gerade sind sehr viele Anfragen eingegangen. Bitte versuchen Sie es in einer Minute noch einmal.', 'deskhub-tickets'); case 'INVALID_FIELD': // Diese Meldungen schreibt DeskHub für Besucher („Bitte füllen Sie das Feld … aus.“) return is_array($data) && !empty($data['api_message']) ? (string) $data['api_message'] : __('Bitte prüfen Sie Ihre Angaben.', 'deskhub-tickets'); case 'NOT_FOUND': return __('Zu dieser Kombination aus Ticketnummer und E-Mail-Adresse wurde nichts gefunden.', 'deskhub-tickets'); } return __('Der Support ist gerade nicht erreichbar. Bitte versuchen Sie es später noch einmal.', 'deskhub-tickets'); } /** * Ein Aufruf der API. * * @param array $query * @param array|null $body * @return array|WP_Error */ private function request(string $method, array $query = [], ?array $body = null) { $url = $this->base . '/api/public/v1/tickets.php'; if ($query) { $url = add_query_arg(array_map('rawurlencode', $query), $url); } $args = [ 'method' => $method, 'timeout' => 15, 'redirection' => 0, 'headers' => [ 'X-API-Key' => $this->key, 'Accept' => 'application/json', 'User-Agent' => 'DeskHub-Tickets-WordPress/' . DESKHUB_TICKETS_VERSION, ], ]; if ($body !== null) { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode($body); } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { error_log('DeskHub Tickets: Server nicht erreichbar: ' . $response->get_error_message()); return new WP_Error('deskhub_unreachable', __('Der DeskHub-Server ist nicht erreichbar.', 'deskhub-tickets'), ['status' => 0, 'transport' => mb_substr(preg_replace('/\s+/', ' ', $response->get_error_message()), 0, 200)]); } $status = (int) wp_remote_retrieve_response_code($response); $decoded = json_decode((string) wp_remote_retrieve_body($response), true); if ($status >= 200 && $status < 300 && is_array($decoded)) { return $decoded; } $apiCode = is_array($decoded) ? (string) ($decoded['error']['code'] ?? '') : ''; $apiMessage = is_array($decoded) ? (string) ($decoded['error']['message'] ?? '') : ''; if ($status >= 500 || $status === 0 || !is_array($decoded)) { error_log('DeskHub Tickets: unerwartete Antwort (HTTP ' . $status . ')'); } elseif ($status === 401 || $status === 403) { error_log('DeskHub Tickets: DeskHub antwortet ' . $status . ' ' . $apiCode . ': ' . $apiMessage); // enthält nie den Schlüssel } return new WP_Error( 'deskhub_api_' . ($apiCode !== '' ? strtolower($apiCode) : 'error'), $apiMessage !== '' ? $apiMessage : __('Der Server hat die Anfrage abgelehnt.', 'deskhub-tickets'), ['status' => $status, 'api_code' => $apiCode, 'api_message' => $apiMessage] ); } }