Files
2026-11-08 20:09:19 +00:00

300 lines
14 KiB
PHP

<?php
/**
* Verbindung zur öffentlichen DeskHub-API (api/public/v1/tickets.php).
* Der API-Schlüssel wird nur hier und nur vom Server aus gesendet, nie an den Browser eines Besuchers.
*/
if (!defined('ABSPATH')) {
exit;
}
class Deskhub_Tickets_Client
{
/** @var string Adresse der DeskHub-Installation ohne Schrägstrich am Ende, zum Beispiel https://support.example.de */
private $base;
/** @var string */
private $key;
public function __construct(string $baseUrl, string $apiKey)
{
$this->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<int,string>} $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<int,array{id:int,name:string,path:string,departmentId:int}>|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<string,string> $query
* @param array<string,mixed>|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]
);
}
}