Quelltext des aktuellen Standes
<?php
/**
* Plugin Name: OpenNIT | Connector
* Plugin URI: https://opennit.de
* Description: OpenNIT Connector integrates your WordPress intranet with the OpenNIT IT documentation platform. Provides employees with self-service access to loan device management, personal asset overviews, and loan requests — directly within WordPress.
* Version: 1.1.1
* Requires at least: 5.8
* Requires PHP: 8.0
* Author: OpenNIT
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: opennit-connector
*
* Copyright (C) 2025 Friederich Loheide
*
* Dieses Plugin ist freie Software: Du kannst es unter den Bedingungen der GNU
* General Public License, Version 2 oder (nach deiner Wahl) jeder späteren
* Version, weitergeben und/oder verändern. Der vollständige Lizenztext liegt
* als LICENSE neben dieser Datei.
*
* Die Weitergabe erfolgt in der Hoffnung, dass sie nützlich ist, jedoch OHNE
* JEDE GEWÄHRLEISTUNG – sogar ohne die implizite Gewährleistung der Marktreife
* oder der Eignung für einen bestimmten Zweck.
*
* Das Plugin ist ein eigenständiges Werk: Es kommuniziert ausschließlich über
* HTTP mit einer OpenNIT-Installation und bindet keinen Programmcode des unter
* AGPL-3.0-only stehenden Servers ein. Die abweichende Lizenz ist Voraussetzung
* für eine Verteilung über WordPress.org.
*/
if (!defined('ABSPATH')) {
exit;
}
define('OPENNIT_VERSION', '1.1.1');
define('OPENNIT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('OPENNIT_PLUGIN_URL', plugin_dir_url(__FILE__));
// Rückwärtskompatibilität: Sind in der wp-config.php noch die alten Konstanten
// (IT_DOKU_API_URL/-TOKEN/-SIGNING_SECRET) gesetzt, die neuen OPENNIT_*-Konstanten
// daraus ableiten – so brechen vorhandene wp-config-Overrides nach dem Rename nicht.
foreach (['API_URL', 'API_TOKEN', 'API_SIGNING_SECRET'] as $legacyConst) {
if (!defined('OPENNIT_' . $legacyConst) && defined('IT_DOKU_' . $legacyConst)) {
define('OPENNIT_' . $legacyConst, constant('IT_DOKU_' . $legacyConst));
}
}
// Klassen laden
require_once OPENNIT_PLUGIN_DIR . 'includes/class-api-client.php';
require_once OPENNIT_PLUGIN_DIR . 'includes/class-wiki.php';
require_once OPENNIT_PLUGIN_DIR . 'includes/class-shortcodes.php';
require_once OPENNIT_PLUGIN_DIR . 'includes/class-assistant.php';
require_once OPENNIT_PLUGIN_DIR . 'includes/class-admin-settings.php';
// Plugin initialisieren
add_action('init', 'opennit_init');
function opennit_init(): void {
$shortcodes = new OpenNIT_Shortcodes();
$shortcodes->register();
$assistant = new OpenNIT_Assistant();
$assistant->register();
}
// Admin-Einstellungen
if (is_admin()) {
$settings = new OpenNIT_AdminSettings();
$settings->register();
}
// Styles und Scripts einbinden – nur auf Seiten mit mindestens einem opennit_*-Shortcode
add_action('wp_enqueue_scripts', 'opennit_enqueue_assets');
function opennit_enqueue_assets(): void {
global $post;
if (!is_a($post, 'WP_Post')) {
return;
}
$shortcodes = ['opennit_loan_devices', 'opennit_assets', 'opennit_ticket_form', 'opennit_my_tickets', 'opennit_wiki'];
$has_sc = false;
foreach ($shortcodes as $sc) {
if (has_shortcode($post->post_content, $sc)) {
$has_sc = true;
break;
}
}
if (!$has_sc) {
return;
}
wp_enqueue_style('opennit-connector', OPENNIT_PLUGIN_URL . 'assets/style.css', [], OPENNIT_VERSION);
// Externes JS statt inline-Script – damit Elementor / Autoptimize / WP Rocket
// es nicht filtern oder blockieren. Beide Files werden im Footer geladen,
// damit der DOM beim Ausführen bereits vollständig gerendert ist.
if (has_shortcode($post->post_content, 'opennit_ticket_form')) {
wp_enqueue_script(
'opennit-ticket-form',
OPENNIT_PLUGIN_URL . 'assets/js/ticket-form.js',
[],
OPENNIT_VERSION,
true
);
}
if (has_shortcode($post->post_content, 'opennit_loan_devices')) {
wp_enqueue_script(
'opennit-loan-devices',
OPENNIT_PLUGIN_URL . 'assets/js/loan-devices.js',
[],
OPENNIT_VERSION,
true
);
}
}
// Upgrade-Routine: läuft einmal pro Versionswechsel.
// Migriert u. a. die gespeicherte API-Basis-URL vom alten Pfad /api/wp auf /api/ex
// (OpenNIT hat den WordPress-API-Präfix umbenannt). So funktioniert die
// Verbindung nach dem Plugin-Update ohne manuelles Anpassen der URL weiter.
add_action('plugins_loaded', 'opennit_connector_maybe_upgrade');
function opennit_connector_maybe_upgrade(): void {
$stored = (string) get_option('opennit_connector_version', '');
if ($stored === OPENNIT_VERSION) {
return;
}
// Einmalige Migration alter Option-Keys (it_doku_* → opennit_*) aus früheren
// Plugin-Versionen. Kopiert nur, wenn der neue Key noch nicht existiert, und
// räumt den alten anschließend auf. So bleiben API-Token, Signatur-Secret und
// URL nach dem Update erhalten, ohne dass etwas neu eingegeben werden muss.
$optionMap = [
'it_doku_api_url' => 'opennit_api_url',
'it_doku_api_token' => 'opennit_api_token',
'it_doku_api_signing_secret' => 'opennit_api_signing_secret',
'it_doku_settings' => 'opennit_settings',
'it_doku_menu_name' => 'opennit_menu_name',
'it_doku_ticket_page_url' => 'opennit_ticket_page_url',
];
foreach ($optionMap as $old => $new) {
$oldVal = get_option($old, null);
if ($oldVal !== null && get_option($new, null) === null) {
update_option($new, $oldVal);
delete_option($old);
}
}
delete_transient('it_doku_config_cache');
delete_transient('it_doku_menu_name_synced');
// API-Basis-URL: alter Pfad /api/wp → /api/ex (auf dem ggf. migrierten Key).
$url = (string) get_option('opennit_api_url', '');
if ($url !== '') {
$migrated = preg_replace('#/api/wp/?$#', '/api/ex', $url);
if ($migrated !== null && $migrated !== $url) {
update_option('opennit_api_url', $migrated);
delete_transient('opennit_config_cache');
}
}
update_option('opennit_connector_version', OPENNIT_VERSION);
}
// Transient-Cache bei Änderung der API-Einstellungen invalidieren
add_action('update_option_opennit_api_url', 'opennit_flush_config_cache');
add_action('update_option_opennit_api_token', 'opennit_flush_config_cache');
function opennit_flush_config_cache(): void {
delete_transient('opennit_config_cache');
}
// Rate-Limiting Hilfsfunktion (Transient-basiert, max. $max Anfragen pro Minute pro User+Aktion)
function opennit_rate_limit(string $action, int $max = 10): bool {
$uid = get_current_user_id();
$key = 'opennit_rl_' . $action . '_' . $uid;
$count = (int)get_transient($key);
if ($count >= $max) {
return false;
}
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
return true;
}
// AJAX-Handler: Leihgerät anfragen
add_action('wp_ajax_opennit_loan_request', 'opennit_handle_loan_request');
function opennit_handle_loan_request(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
if (!opennit_rate_limit('loan_request', 10)) {
wp_send_json_error('Zu viele Anfragen. Bitte warten Sie einen Moment.');
return;
}
$user = wp_get_current_user();
$device_id = isset($_POST['device_id']) ? intval($_POST['device_id']) : 0;
$planned_start = isset($_POST['planned_start']) ? sanitize_text_field($_POST['planned_start']) : '';
$planned_end = isset($_POST['planned_end']) ? sanitize_text_field($_POST['planned_end']) : '';
$purpose = isset($_POST['purpose']) ? sanitize_textarea_field($_POST['purpose']) : '';
if ($device_id <= 0 || empty($planned_start) || empty($planned_end)) {
wp_send_json_error('Bitte alle Pflichtfelder ausfüllen.');
return;
}
$data = [
'device_id' => $device_id,
'planned_start' => $planned_start,
'planned_end' => $planned_end,
'purpose' => $purpose,
'borrower_name' => $user->display_name,
'borrower_email' => $user->user_email,
];
$client = new OpenNIT_ApiClient();
$result = $client->create_loan_request($data);
if (is_wp_error($result)) {
// Konflikt-Daten (conflict_from, conflict_to) aus WP_Error-Data mitgeben
$errData = $result->get_error_data() ?: [];
wp_send_json_error([
'message' => $result->get_error_message(),
'conflict_from' => $errData['conflict_from'] ?? null,
'conflict_to' => $errData['conflict_to'] ?? null,
]);
return;
}
wp_send_json_success([
'message' => $result['message'] ?? 'Anfrage erfolgreich übermittelt.',
// status: 'pending' = wartet auf Bestätigung, 'reserved' = direkt fest gebucht
'status' => $result['status'] ?? 'pending',
]);
}
// AJAX-Handler: Geräte nach Typ und Zeitraum filtern
add_action('wp_ajax_opennit_filter_devices', 'opennit_handle_filter_devices');
function opennit_handle_filter_devices(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
if (!opennit_rate_limit('filter_devices', 30)) {
wp_send_json_error('Zu viele Anfragen. Bitte warten Sie einen Moment.');
return;
}
$type = sanitize_text_field($_POST['type'] ?? '');
$date_from = sanitize_text_field($_POST['date_from'] ?? '');
$date_to = sanitize_text_field($_POST['date_to'] ?? '');
// Datumsformat prüfen
if ($date_from && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_from)) $date_from = '';
if ($date_to && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_to)) $date_to = '';
$client = new OpenNIT_ApiClient();
$result = $client->get_loan_devices($type, $date_from, $date_to);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
wp_send_json_success(['devices' => $result['devices'] ?? []]);
}
// AJAX-Handler: Leihe stornieren
add_action('wp_ajax_opennit_cancel_loan', 'opennit_handle_cancel_loan');
function opennit_handle_cancel_loan(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
if (!opennit_rate_limit('cancel_loan', 10)) {
wp_send_json_error('Zu viele Anfragen. Bitte warten Sie einen Moment.');
return;
}
$user = wp_get_current_user();
$lending_id = isset($_POST['lending_id']) ? intval($_POST['lending_id']) : 0;
if ($lending_id <= 0) {
wp_send_json_error('Ungültige Anfrage.');
return;
}
$client = new OpenNIT_ApiClient();
$result = $client->cancel_loan_request($lending_id, $user->user_email);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
if (!empty($result['error'])) {
wp_send_json_error($result['error']);
return;
}
wp_send_json_success(['message' => 'Reservierung wurde storniert.']);
}
// AJAX-Handler: Bild-Proxy (nur eingeloggte WP-User – Token bleibt server-seitig)
add_action('wp_ajax_opennit_proxy_image', 'opennit_handle_proxy_image');
function opennit_handle_proxy_image(): void {
if (!is_user_logged_in()) {
status_header(403);
exit;
}
$path = isset($_GET['path']) ? sanitize_text_field($_GET['path']) : '';
// Nur einfache relative Pfade erlaubt (kein Path-Traversal)
if (!$path || !preg_match('#^[a-z0-9_-]+/[a-zA-Z0-9_\-\.]+$#i', $path)) {
status_header(400);
exit;
}
// Signierter Proxy-Aufruf (OpenNIT verlangt HMAC-Signatur – sonst 401)
$result = (new OpenNIT_ApiClient())->proxy_get('/image?path=' . rawurlencode($path));
if (is_wp_error($result)) {
status_header(502);
exit;
}
if ($result['status'] !== 200) {
status_header($result['status'] >= 400 ? $result['status'] : 502);
exit;
}
$mime = $result['content_type'] ?: 'image/jpeg';
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
if (!in_array(strtolower(explode(';', $mime)[0]), $allowed, true)) {
status_header(403);
exit;
}
header('Content-Type: ' . $mime);
header('Cache-Control: private, max-age=3600');
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $result['body'];
exit;
}
// AJAX-Handler: Ticket erstellen
add_action('wp_ajax_opennit_submit_ticket', 'opennit_handle_submit_ticket');
function opennit_handle_submit_ticket(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error(['message' => 'Nicht eingeloggt.'], 403);
return;
}
if (!opennit_rate_limit('submit_ticket', 5)) {
wp_send_json_error(['message' => 'Zu viele Anfragen. Bitte warten Sie einen Moment.']);
return;
}
$subject = isset($_POST['subject']) ? sanitize_text_field($_POST['subject']) : '';
$description = isset($_POST['description']) ? trim(sanitize_textarea_field($_POST['description'])) : '';
$location_id = isset($_POST['location_id']) ? intval($_POST['location_id']) : 0;
$category_id = isset($_POST['category_id']) ? intval($_POST['category_id']) : 0;
$from_email = isset($_POST['from_email']) ? sanitize_email($_POST['from_email']) : '';
$from_name = isset($_POST['from_name']) ? sanitize_text_field($_POST['from_name']) : '';
$asset_id = isset($_POST['asset_id']) ? intval($_POST['asset_id']) : 0;
// Dateianhang (base64 aus FileReader)
$attachment_b64 = isset($_POST['attachment_b64']) ? $_POST['attachment_b64'] : '';
$attachment_name = isset($_POST['attachment_name']) ? sanitize_file_name($_POST['attachment_name']) : '';
$attachment_mime = isset($_POST['attachment_mime']) ? sanitize_text_field($_POST['attachment_mime']) : '';
if (empty($subject) || empty($description) || $location_id <= 0 || $category_id <= 0) {
wp_send_json_error(['message' => 'Bitte alle Pflichtfelder ausfüllen.']);
return;
}
$data = [
'subject' => $subject,
'description' => $description,
'location_id' => $location_id,
'category_id' => $category_id,
'from_email' => $from_email,
'from_name' => $from_name,
];
if ($asset_id > 0) {
$data['asset_id'] = $asset_id;
}
if ($attachment_b64 !== '' && $attachment_name !== '') {
$data['attachment_b64'] = $attachment_b64;
$data['attachment_name'] = $attachment_name;
$data['attachment_mime'] = $attachment_mime;
}
$client = new OpenNIT_ApiClient();
$result = $client->create_ticket($data);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
return;
}
if (!empty($result['error'])) {
wp_send_json_error(['message' => $result['error']]);
return;
}
wp_send_json_success(['message' => $result['message'] ?? 'Ticket erfolgreich übermittelt.']);
}
// AJAX-Handler: Ticket-Details abrufen
add_action('wp_ajax_opennit_get_ticket', 'opennit_handle_get_ticket');
function opennit_handle_get_ticket(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
$ticket_id = isset($_POST['ticket_id']) ? intval($_POST['ticket_id']) : 0;
if ($ticket_id <= 0) {
wp_send_json_error('Ungültige Ticket-ID.');
return;
}
$user = wp_get_current_user();
$client = new OpenNIT_ApiClient();
$result = $client->get_ticket_detail($ticket_id, $user->user_email);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
wp_send_json_success($result);
}
// AJAX-Handler: Auf Ticket antworten
add_action('wp_ajax_opennit_reply_ticket', 'opennit_handle_reply_ticket');
function opennit_handle_reply_ticket(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
if (!opennit_rate_limit('reply_ticket', 10)) {
wp_send_json_error('Zu viele Anfragen. Bitte warten Sie einen Moment.');
return;
}
$ticket_id = isset($_POST['ticket_id']) ? intval($_POST['ticket_id']) : 0;
$message = isset($_POST['message']) ? sanitize_textarea_field($_POST['message']) : '';
if ($ticket_id <= 0 || $message === '') {
wp_send_json_error('Ungültige Anfrage.');
return;
}
$user = wp_get_current_user();
$client = new OpenNIT_ApiClient();
$result = $client->reply_ticket($ticket_id, [
'email' => $user->user_email,
'message' => $message,
]);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
if (!empty($result['error'])) {
wp_send_json_error($result['error']);
return;
}
wp_send_json_success($result);
}
// AJAX-Handler: Ticket-Anhang herunterladen (Proxy – Token bleibt server-seitig)
add_action('wp_ajax_opennit_download_attachment', 'opennit_handle_download_attachment');
function opennit_handle_download_attachment(): void {
if (!is_user_logged_in()) {
status_header(403);
exit;
}
check_ajax_referer('opennit_nonce', 'nonce');
$attachment_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($attachment_id <= 0) {
status_header(400);
exit;
}
$user = wp_get_current_user();
// Signierter Proxy-Aufruf (OpenNIT verlangt HMAC-Signatur – sonst 401)
$result = (new OpenNIT_ApiClient())->proxy_get(
'/ticket-attachments/' . $attachment_id . '?email=' . rawurlencode($user->user_email)
);
if (is_wp_error($result)) {
status_header(502);
exit;
}
if ($result['status'] !== 200) {
// Echten Status durchreichen (403 = nicht Eigentümer, 404 = nicht gefunden …)
status_header($result['status'] >= 400 ? $result['status'] : 502);
exit;
}
$content_type = $result['content_type'] ?: 'application/octet-stream';
$content_disposition = $result['content_disposition'] ?: 'attachment';
header('Content-Type: ' . $content_type);
header('Content-Disposition: ' . $content_disposition);
header('Cache-Control: private, no-store');
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $result['body'];
exit;
}
// AJAX-Handler: Belegte Zeiträume (Slots) eines Leihgeräts abrufen (für Kalender-Widget)
add_action('wp_ajax_opennit_get_device_slots', 'opennit_handle_get_device_slots');
function opennit_handle_get_device_slots(): void {
check_ajax_referer('opennit_nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error('Nicht eingeloggt.', 403);
return;
}
$device_id = isset($_POST['device_id']) ? intval($_POST['device_id']) : 0;
if ($device_id <= 0) {
wp_send_json_error('Ungültige Geräte-ID.');
return;
}
$client = new OpenNIT_ApiClient();
$result = $client->get_loan_device_slots($device_id);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
return;
}
wp_send_json_success($result);
}