300 lines
14 KiB
PHP
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]
|
|
);
|
|
}
|
|
}
|