1836 lines
74 KiB
PHP
1836 lines
74 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: o-Byte QA Tool
|
|
* Description: WordPress version of the o-Byte manual QA runner with GitLab templates, exports, and DocBee posting.
|
|
* Version: 1.0.8
|
|
* Author: o-byte.com
|
|
* Text Domain: obyte-qa-tool
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
final class Obyte_QA_Tool
|
|
{
|
|
private const VERSION = '1.0.8';
|
|
private const OPTION_NAME = 'obyte_qa_tool_settings';
|
|
private const REST_NAMESPACE = 'obyte-qa-tool/v1';
|
|
|
|
private static ?Obyte_QA_Tool $instance = null;
|
|
|
|
public static function instance(): Obyte_QA_Tool
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
public static function activate(): void
|
|
{
|
|
if (!get_option(self::OPTION_NAME)) {
|
|
add_option(self::OPTION_NAME, self::defaults());
|
|
}
|
|
|
|
self::create_tables();
|
|
}
|
|
|
|
private function __construct()
|
|
{
|
|
add_shortcode('obyte_qa_tool', [$this, 'render_shortcode']);
|
|
add_action('wp_enqueue_scripts', [$this, 'register_assets']);
|
|
add_action('admin_enqueue_scripts', [$this, 'register_assets']);
|
|
add_action('admin_menu', [$this, 'register_admin_page']);
|
|
add_action('admin_init', [$this, 'register_settings']);
|
|
add_action('admin_post_obyte_qa_pdf', [$this, 'download_secure_pdf']);
|
|
add_action('rest_api_init', [$this, 'register_rest_routes']);
|
|
}
|
|
|
|
private static function defaults(): array
|
|
{
|
|
return [
|
|
'default_pbx_version' => '',
|
|
'gitlab_enabled' => 1,
|
|
'gitlab_base_url' => 'https://git.steinert.cc',
|
|
'gitlab_project' => 'qa/templates',
|
|
'gitlab_ref' => 'main',
|
|
'gitlab_template_path' => 'templates',
|
|
'gitlab_token' => '',
|
|
'gitlab_per_page' => 100,
|
|
'gitlab_allow_writes' => 1,
|
|
'storage_enabled' => 1,
|
|
'media_pdf_enabled' => 1,
|
|
'docbee_enabled' => 1,
|
|
'docbee_base_url' => 'https://obyte.docbee.com',
|
|
'docbee_username' => '',
|
|
'docbee_password' => '',
|
|
'docbee_lifetime' => 1440,
|
|
'docbee_refresh_lifetime' => 1,
|
|
'docbee_message_internal' => 1,
|
|
'docbee_message_hidden' => 0,
|
|
'docbee_enable_fallback_note' => 0,
|
|
'docbee_preserve_ticket_status' => 1,
|
|
];
|
|
}
|
|
|
|
private function settings(): array
|
|
{
|
|
$stored = get_option(self::OPTION_NAME, []);
|
|
return wp_parse_args(is_array($stored) ? $stored : [], self::defaults());
|
|
}
|
|
|
|
private static function reports_table(): string
|
|
{
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'obyte_qa_reports';
|
|
}
|
|
|
|
private static function steps_table(): string
|
|
{
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'obyte_qa_steps';
|
|
}
|
|
|
|
private static function create_tables(): void
|
|
{
|
|
global $wpdb;
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$reports_table = self::reports_table();
|
|
$steps_table = self::steps_table();
|
|
|
|
dbDelta("
|
|
CREATE TABLE {$reports_table} (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
created_at DATETIME NOT NULL,
|
|
user_id BIGINT UNSIGNED NULL,
|
|
module VARCHAR(255) NULL,
|
|
module_version VARCHAR(100) NULL,
|
|
pbx_version VARCHAR(100) NULL,
|
|
olm_nummer VARCHAR(100) NULL,
|
|
tester VARCHAR(255) NULL,
|
|
docbee_url TEXT NULL,
|
|
docbee_result_url TEXT NULL,
|
|
summary VARCHAR(255) NULL,
|
|
pdf_attachment_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
pdf_url TEXT NULL,
|
|
pdf_file TEXT NULL,
|
|
run_json LONGTEXT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY module (module(120)),
|
|
KEY created_at (created_at),
|
|
KEY user_id (user_id)
|
|
) {$charset_collate};
|
|
");
|
|
|
|
dbDelta("
|
|
CREATE TABLE {$steps_table} (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
report_id BIGINT UNSIGNED NOT NULL,
|
|
step_index INT UNSIGNED NOT NULL DEFAULT 0,
|
|
step_id VARCHAR(80) NULL,
|
|
title TEXT NULL,
|
|
expected TEXT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT '',
|
|
required TINYINT(1) NOT NULL DEFAULT 0,
|
|
comment TEXT NULL,
|
|
evidence TEXT NULL,
|
|
group_title VARCHAR(255) NULL,
|
|
group_index INT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY report_id (report_id),
|
|
KEY status (status)
|
|
) {$charset_collate};
|
|
");
|
|
}
|
|
|
|
public function register_assets(): void
|
|
{
|
|
wp_register_style(
|
|
'obyte-qa-tool',
|
|
plugins_url('assets/css/qa-tool.css', __FILE__),
|
|
[],
|
|
self::VERSION
|
|
);
|
|
|
|
wp_register_script(
|
|
'obyte-qa-tool-js-yaml',
|
|
'https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js',
|
|
[],
|
|
'4.1.0',
|
|
true
|
|
);
|
|
|
|
wp_register_script(
|
|
'obyte-qa-tool',
|
|
plugins_url('assets/js/qa-tool.js', __FILE__),
|
|
['obyte-qa-tool-js-yaml'],
|
|
self::VERSION,
|
|
true
|
|
);
|
|
}
|
|
|
|
private function enqueue_frontend_assets(string $root_id): void
|
|
{
|
|
$settings = $this->settings();
|
|
$user = wp_get_current_user();
|
|
|
|
wp_enqueue_style('obyte-qa-tool');
|
|
wp_enqueue_style('dashicons');
|
|
wp_enqueue_script('obyte-qa-tool-js-yaml');
|
|
wp_enqueue_script('obyte-qa-tool');
|
|
wp_localize_script(
|
|
'obyte-qa-tool',
|
|
'ObyteQaTool',
|
|
[
|
|
'rootId' => $root_id,
|
|
'restUrl' => esc_url_raw(rest_url(self::REST_NAMESPACE . '/')),
|
|
'nonce' => wp_create_nonce('wp_rest'),
|
|
'tester' => $this->tester_name($user),
|
|
'defaultPbxVersion' => (string) $settings['default_pbx_version'],
|
|
'gitlabEnabled' => !empty($settings['gitlab_enabled']),
|
|
'gitlabWritesEnabled' => !empty($settings['gitlab_enabled']) && !empty($settings['gitlab_allow_writes']),
|
|
'docbeeEnabled' => !empty($settings['docbee_enabled']),
|
|
'docbeeConfigured' => $this->docbee_configured($settings),
|
|
'storageEnabled' => !empty($settings['storage_enabled']),
|
|
'mediaPdfEnabled' => !empty($settings['media_pdf_enabled']),
|
|
'logoUrl' => esc_url_raw(plugins_url('assets/img/o-byte_Logo_2024_Dark.svg', __FILE__)),
|
|
'printLogoUrl' => esc_url_raw(plugins_url('assets/img/o-byte_Logo_2024_Dark.svg', __FILE__)),
|
|
'jsPdfUrls' => [
|
|
'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js',
|
|
'https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js',
|
|
],
|
|
'autoTableUrls' => [
|
|
'https://cdn.jsdelivr.net/npm/jspdf-autotable@3.8.2/dist/jspdf.plugin.autotable.min.js',
|
|
'https://unpkg.com/jspdf-autotable@3.8.2/dist/jspdf.plugin.autotable.min.js',
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
public function render_shortcode(array $atts = []): string
|
|
{
|
|
if (!is_user_logged_in()) {
|
|
$login_url = wp_login_url(get_permalink());
|
|
return '<div class="obyte-qa-tool-notice">' .
|
|
esc_html__('Bitte melde dich an, um das QA Tool zu nutzen.', 'obyte-qa-tool') .
|
|
' <a href="' . esc_url($login_url) . '">' . esc_html__('Zum Login', 'obyte-qa-tool') . '</a></div>';
|
|
}
|
|
|
|
$root_id = wp_unique_id('obyte-qa-tool-');
|
|
$this->enqueue_frontend_assets($root_id);
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="obyte-qa-tool-shell">
|
|
<div id="<?php echo esc_attr($root_id); ?>" class="obyte-qa-tool" data-obyte-qa-tool>
|
|
<header class="oqt-header">
|
|
<div class="oqt-title-block">
|
|
<p class="oqt-kicker">Qualitätssicherung</p>
|
|
<h1>QA System</h1>
|
|
<div class="oqt-tags" aria-live="polite">
|
|
<span class="oqt-tag" data-field="tplName">Keine Vorlage</span>
|
|
<span class="oqt-tag oqt-tag--muted" data-field="stepSummary">0 Steps</span>
|
|
<span class="oqt-tag oqt-tag--muted" data-field="statusTag">Bereit</span>
|
|
</div>
|
|
</div>
|
|
<div class="oqt-header-actions">
|
|
<button type="button" class="oqt-btn oqt-btn--primary" data-action="exportAll">
|
|
<span class="dashicons dashicons-upload" aria-hidden="true"></span>
|
|
<span>Exportieren</span>
|
|
</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="printPdf">
|
|
<span class="dashicons dashicons-media-document" aria-hidden="true"></span>
|
|
<span>PDF</span>
|
|
</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="pushDocbee">
|
|
<span class="dashicons dashicons-format-chat" aria-hidden="true"></span>
|
|
<span>DocBee</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="oqt-panel oqt-panel--setup" aria-label="Vorlagen und Metadaten">
|
|
<div class="oqt-panel-head">
|
|
<div>
|
|
<h2>Setup</h2>
|
|
<p>Template laden, Laufdaten setzen, Ticket verknüpfen.</p>
|
|
</div>
|
|
<div class="oqt-service-tags">
|
|
<span class="oqt-tag" data-field="gitlabTplStatus">GitLab: nicht geladen</span>
|
|
<span class="oqt-tag" data-field="docbeeTokenStatus">DocBee: unbekannt</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="oqt-setup-grid">
|
|
<div class="oqt-template-card">
|
|
<label class="oqt-field oqt-field--select">
|
|
<span>GitLab Vorlage</span>
|
|
<select data-field="gitlabTplSelect">
|
|
<option value="">GitLab-Templates laden</option>
|
|
</select>
|
|
</label>
|
|
<div class="oqt-template-actions">
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="reloadGitlab">
|
|
<span class="dashicons dashicons-update" aria-hidden="true"></span>
|
|
<span>Neu laden</span>
|
|
</button>
|
|
<input class="oqt-file" type="file" data-field="templateFile" accept=".yaml,.yml,.json">
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="pickTemplate">
|
|
<span class="dashicons dashicons-open-folder" aria-hidden="true"></span>
|
|
<span>Lokal</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="oqt-meta-grid">
|
|
<label class="oqt-field">
|
|
<span>Modul</span>
|
|
<input type="text" data-field="module" placeholder="CallRouting">
|
|
</label>
|
|
<label class="oqt-field">
|
|
<span>Modul-Version</span>
|
|
<input type="text" data-field="moduleVersion" placeholder="1.4.2">
|
|
</label>
|
|
<label class="oqt-field">
|
|
<span>OLM-Nummer</span>
|
|
<input type="text" data-field="olmNummer" placeholder="OLM-12345">
|
|
</label>
|
|
<label class="oqt-field">
|
|
<span>PBX-Version</span>
|
|
<input type="text" data-field="pbxVersion" placeholder="8.1.2">
|
|
</label>
|
|
<label class="oqt-field">
|
|
<span>Tester</span>
|
|
<input type="text" data-field="tester" readonly>
|
|
</label>
|
|
<label class="oqt-field oqt-field--wide-line">
|
|
<span>DocBee Ticket-URL</span>
|
|
<input type="url" data-field="docbeeUrl" placeholder="https://.../ticket/show/12345">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="oqt-panel oqt-panel--steps" aria-label="Testschritte">
|
|
<div class="oqt-panel-head">
|
|
<div>
|
|
<h2>Testschritte</h2>
|
|
<p>Status, Kommentar und Evidenz direkt am Schritt pflegen.</p>
|
|
</div>
|
|
<div class="oqt-step-actions">
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="addStep">
|
|
<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
|
|
<span>Step</span>
|
|
</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="addGroup">
|
|
<span class="dashicons dashicons-category" aria-hidden="true"></span>
|
|
<span>Gruppe</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="oqt-table-wrap">
|
|
<table class="oqt-steps-table" data-field="stepsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Step</th>
|
|
<th>Erwartung</th>
|
|
<th>Status</th>
|
|
<th>Kommentar / Evidenz</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody data-field="stepsTableBody">
|
|
<tr class="oqt-empty-row"><td colspan="4">Keine Vorlage geladen.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="oqt-action-dock" aria-label="Exporte und DocBee">
|
|
<div class="oqt-action-group">
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="saveJson">
|
|
<span class="dashicons dashicons-saved" aria-hidden="true"></span>
|
|
<span>JSON</span>
|
|
</button>
|
|
<input class="oqt-file" type="file" data-field="loadJson" accept=".json,.yaml,.yml">
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="pickRun">
|
|
<span class="dashicons dashicons-upload" aria-hidden="true"></span>
|
|
<span>Lauf laden</span>
|
|
</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="exportMd">MD</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="exportCsv">CSV</button>
|
|
<button type="button" class="oqt-btn oqt-btn--light" data-action="exportTemplateYaml">
|
|
<span class="dashicons dashicons-cloud-upload" aria-hidden="true"></span>
|
|
<span>Template</span>
|
|
</button>
|
|
</div>
|
|
<div class="oqt-message" data-field="message" aria-live="polite"></div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
public function register_admin_page(): void
|
|
{
|
|
add_menu_page(
|
|
'o-Byte QA Tool',
|
|
'QA Tool',
|
|
'manage_options',
|
|
'obyte-qa-tool',
|
|
[$this, 'render_admin_page'],
|
|
'dashicons-clipboard',
|
|
58
|
|
);
|
|
|
|
add_submenu_page(
|
|
'obyte-qa-tool',
|
|
'o-Byte QA Tool Settings',
|
|
'Settings',
|
|
'manage_options',
|
|
'obyte-qa-tool',
|
|
[$this, 'render_admin_page']
|
|
);
|
|
|
|
add_submenu_page(
|
|
'obyte-qa-tool',
|
|
'o-Byte QA Reports',
|
|
'Reports',
|
|
'manage_options',
|
|
'obyte-qa-reports',
|
|
[$this, 'render_reports_page']
|
|
);
|
|
}
|
|
|
|
public function register_settings(): void
|
|
{
|
|
register_setting(
|
|
'obyte_qa_tool',
|
|
self::OPTION_NAME,
|
|
[
|
|
'type' => 'array',
|
|
'sanitize_callback' => [$this, 'sanitize_settings'],
|
|
'default' => self::defaults(),
|
|
]
|
|
);
|
|
}
|
|
|
|
public function sanitize_settings($input): array
|
|
{
|
|
$input = is_array($input) ? $input : [];
|
|
$old = $this->settings();
|
|
$out = self::defaults();
|
|
|
|
$out['default_pbx_version'] = sanitize_text_field((string) ($input['default_pbx_version'] ?? ''));
|
|
$out['gitlab_enabled'] = empty($input['gitlab_enabled']) ? 0 : 1;
|
|
$out['gitlab_base_url'] = $this->base_url((string) ($input['gitlab_base_url'] ?? ''));
|
|
$out['gitlab_project'] = sanitize_text_field((string) ($input['gitlab_project'] ?? ''));
|
|
$out['gitlab_ref'] = sanitize_text_field((string) ($input['gitlab_ref'] ?? ''));
|
|
$out['gitlab_template_path'] = trim(sanitize_text_field((string) ($input['gitlab_template_path'] ?? '')), '/');
|
|
$out['gitlab_per_page'] = min(100, max(1, absint($input['gitlab_per_page'] ?? 100)));
|
|
$out['gitlab_token'] = $this->sanitize_secret_field('gitlab_token', $input, $old, 'gitlab_token_clear');
|
|
$out['gitlab_allow_writes'] = empty($input['gitlab_allow_writes']) ? 0 : 1;
|
|
|
|
$out['storage_enabled'] = empty($input['storage_enabled']) ? 0 : 1;
|
|
$out['media_pdf_enabled'] = empty($input['media_pdf_enabled']) ? 0 : 1;
|
|
|
|
$out['docbee_enabled'] = empty($input['docbee_enabled']) ? 0 : 1;
|
|
$out['docbee_base_url'] = $this->base_url((string) ($input['docbee_base_url'] ?? ''));
|
|
$out['docbee_username'] = sanitize_text_field((string) ($input['docbee_username'] ?? ''));
|
|
$out['docbee_password'] = $this->sanitize_secret_field('docbee_password', $input, $old, 'docbee_password_clear');
|
|
$out['docbee_lifetime'] = min(10080, max(1, absint($input['docbee_lifetime'] ?? 1440)));
|
|
$out['docbee_refresh_lifetime'] = empty($input['docbee_refresh_lifetime']) ? 0 : 1;
|
|
$out['docbee_message_internal'] = empty($input['docbee_message_internal']) ? 0 : 1;
|
|
$out['docbee_message_hidden'] = empty($input['docbee_message_hidden']) ? 0 : 1;
|
|
$out['docbee_enable_fallback_note'] = empty($input['docbee_enable_fallback_note']) ? 0 : 1;
|
|
$out['docbee_preserve_ticket_status'] = empty($input['docbee_preserve_ticket_status']) ? 0 : 1;
|
|
|
|
return $out;
|
|
}
|
|
|
|
public function render_admin_page(): void
|
|
{
|
|
if (!current_user_can('manage_options')) {
|
|
return;
|
|
}
|
|
|
|
$settings = $this->settings();
|
|
$option = self::OPTION_NAME;
|
|
?>
|
|
<div class="wrap obyte-qa-admin">
|
|
<h1>o-Byte QA Tool</h1>
|
|
<p>Shortcode: <code>[obyte_qa_tool]</code></p>
|
|
|
|
<form method="post" action="options.php">
|
|
<?php settings_fields('obyte_qa_tool'); ?>
|
|
|
|
<h2>Allgemein</h2>
|
|
<table class="form-table" role="presentation">
|
|
<tr>
|
|
<th scope="row"><label for="oqt-default-pbx-version">Standard PBX-Version</label></th>
|
|
<td>
|
|
<input id="oqt-default-pbx-version" class="regular-text" type="text" name="<?php echo esc_attr($option); ?>[default_pbx_version]" value="<?php echo esc_attr($settings['default_pbx_version']); ?>">
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h2>GitLab Templates</h2>
|
|
<table class="form-table" role="presentation">
|
|
<tr>
|
|
<th scope="row">Aktiv</th>
|
|
<td><label><input type="checkbox" name="<?php echo esc_attr($option); ?>[gitlab_enabled]" value="1" <?php checked($settings['gitlab_enabled']); ?>> GitLab-Vorlagen im Frontend anbieten</label></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row">Schreiben</th>
|
|
<td><label><input type="checkbox" name="<?php echo esc_attr($option); ?>[gitlab_allow_writes]" value="1" <?php checked($settings['gitlab_allow_writes']); ?>> Templates aus dem Frontend nach GitLab schreiben</label></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-base">GitLab Base URL</label></th>
|
|
<td><input id="oqt-gitlab-base" class="regular-text" type="url" name="<?php echo esc_attr($option); ?>[gitlab_base_url]" value="<?php echo esc_attr($settings['gitlab_base_url']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-project">Projekt-ID oder Pfad</label></th>
|
|
<td><input id="oqt-gitlab-project" class="regular-text" type="text" name="<?php echo esc_attr($option); ?>[gitlab_project]" value="<?php echo esc_attr($settings['gitlab_project']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-ref">Branch / Ref</label></th>
|
|
<td><input id="oqt-gitlab-ref" class="regular-text" type="text" name="<?php echo esc_attr($option); ?>[gitlab_ref]" value="<?php echo esc_attr($settings['gitlab_ref']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-path">Template-Pfad</label></th>
|
|
<td><input id="oqt-gitlab-path" class="regular-text" type="text" name="<?php echo esc_attr($option); ?>[gitlab_template_path]" value="<?php echo esc_attr($settings['gitlab_template_path']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-per-page">Max. Dateien</label></th>
|
|
<td><input id="oqt-gitlab-per-page" class="small-text" type="number" min="1" max="100" name="<?php echo esc_attr($option); ?>[gitlab_per_page]" value="<?php echo esc_attr((string) $settings['gitlab_per_page']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-gitlab-token">Private Token</label></th>
|
|
<td>
|
|
<input id="oqt-gitlab-token" class="regular-text" type="password" autocomplete="new-password" name="<?php echo esc_attr($option); ?>[gitlab_token]" value="" placeholder="<?php echo empty($settings['gitlab_token']) ? '' : esc_attr__('Gespeichert', 'obyte-qa-tool'); ?>">
|
|
<p class="description">Leer lassen, um den gespeicherten Token beizubehalten.</p>
|
|
<?php if (!empty($settings['gitlab_token'])) : ?>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[gitlab_token_clear]" value="1"> Gespeicherten Token entfernen</label>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h2>Speicherung in WordPress</h2>
|
|
<table class="form-table" role="presentation">
|
|
<tr>
|
|
<th scope="row">QA Reports</th>
|
|
<td>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[storage_enabled]" value="1" <?php checked($settings['storage_enabled']); ?>> Reports und Steps in WordPress Tabellen speichern</label>
|
|
<p class="description">Nutzt die Tabellen <code><?php global $wpdb; echo esc_html($wpdb->prefix . 'obyte_qa_reports'); ?></code> und <code><?php echo esc_html($wpdb->prefix . 'obyte_qa_steps'); ?></code>.</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row">PDFs</th>
|
|
<td>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[media_pdf_enabled]" value="1" <?php checked($settings['media_pdf_enabled']); ?>> Export-PDFs geschützt speichern</label>
|
|
<p class="description">PDFs werden nicht als öffentliche Media-Library-Dateien abgelegt. Der Zugriff erfolgt über einmalige Backend-Links.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h2>DocBee</h2>
|
|
<table class="form-table" role="presentation">
|
|
<tr>
|
|
<th scope="row">Aktiv</th>
|
|
<td><label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_enabled]" value="1" <?php checked($settings['docbee_enabled']); ?>> DocBee-Posting im Frontend anbieten</label></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-docbee-base">DocBee Base URL</label></th>
|
|
<td><input id="oqt-docbee-base" class="regular-text" type="url" name="<?php echo esc_attr($option); ?>[docbee_base_url]" value="<?php echo esc_attr($settings['docbee_base_url']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-docbee-username">Benutzername</label></th>
|
|
<td><input id="oqt-docbee-username" class="regular-text" type="text" name="<?php echo esc_attr($option); ?>[docbee_username]" value="<?php echo esc_attr($settings['docbee_username']); ?>"></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-docbee-password">Passwort</label></th>
|
|
<td>
|
|
<input id="oqt-docbee-password" class="regular-text" type="password" autocomplete="new-password" name="<?php echo esc_attr($option); ?>[docbee_password]" value="" placeholder="<?php echo empty($settings['docbee_password']) ? '' : esc_attr__('Gespeichert', 'obyte-qa-tool'); ?>">
|
|
<p class="description">Leer lassen, um das gespeicherte Passwort beizubehalten.</p>
|
|
<?php if (!empty($settings['docbee_password'])) : ?>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_password_clear]" value="1"> Gespeichertes Passwort entfernen</label>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><label for="oqt-docbee-lifetime">Token-Laufzeit</label></th>
|
|
<td><input id="oqt-docbee-lifetime" class="small-text" type="number" min="1" max="10080" name="<?php echo esc_attr($option); ?>[docbee_lifetime]" value="<?php echo esc_attr((string) $settings['docbee_lifetime']); ?>"> Minuten</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row">Token Refresh</th>
|
|
<td><label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_refresh_lifetime]" value="1" <?php checked($settings['docbee_refresh_lifetime']); ?>> Token-Laufzeit bei Calls verlängern</label></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row">Nachricht</th>
|
|
<td>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_message_internal]" value="1" <?php checked($settings['docbee_message_internal']); ?>> Intern posten</label><br>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_message_hidden]" value="1" <?php checked($settings['docbee_message_hidden']); ?>> Versteckt posten</label><br>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_enable_fallback_note]" value="1" <?php checked($settings['docbee_enable_fallback_note']); ?>> Bei Fehler als Note versuchen</label><br>
|
|
<label><input type="checkbox" name="<?php echo esc_attr($option); ?>[docbee_preserve_ticket_status]" value="1" <?php checked($settings['docbee_preserve_ticket_status']); ?>> Ticketstatus nach dem Posten wiederherstellen</label>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<?php submit_button(); ?>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
public function render_reports_page(): void
|
|
{
|
|
if (!current_user_can('manage_options')) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
self::create_tables();
|
|
$reports_table = self::reports_table();
|
|
$this->migrate_legacy_pdf_attachments(500);
|
|
$rows = $wpdb->get_results("SELECT * FROM {$reports_table} ORDER BY created_at DESC LIMIT 100", ARRAY_A);
|
|
?>
|
|
<div class="wrap obyte-qa-reports">
|
|
<h1>o-Byte QA Reports</h1>
|
|
<p>Die letzten 100 gespeicherten QA-Exports aus der WordPress Datenbank.</p>
|
|
<table class="widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Datum</th>
|
|
<th>Modul</th>
|
|
<th>Version</th>
|
|
<th>OLM</th>
|
|
<th>Tester</th>
|
|
<th>Summary</th>
|
|
<th>PDF</th>
|
|
<th>DocBee</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($rows)) : ?>
|
|
<tr><td colspan="9">Noch keine QA Reports gespeichert.</td></tr>
|
|
<?php else : ?>
|
|
<?php foreach ($rows as $row) : ?>
|
|
<?php
|
|
$row = $this->ensure_secure_report_pdf($row);
|
|
$pdf_download_url = $this->create_pdf_download_url($row);
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html((string) $row['id']); ?></td>
|
|
<td><?php echo esc_html((string) $row['created_at']); ?></td>
|
|
<td><?php echo esc_html((string) $row['module']); ?></td>
|
|
<td><?php echo esc_html((string) $row['module_version']); ?></td>
|
|
<td><?php echo esc_html((string) $row['olm_nummer']); ?></td>
|
|
<td><?php echo esc_html((string) $row['tester']); ?></td>
|
|
<td><?php echo esc_html((string) $row['summary']); ?></td>
|
|
<td>
|
|
<?php if ($pdf_download_url !== '') : ?>
|
|
<a href="<?php echo esc_url($pdf_download_url); ?>" target="_blank" rel="noopener">PDF</a>
|
|
<?php else : ?>
|
|
-
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if (!empty($row['docbee_result_url'])) : ?>
|
|
<a href="<?php echo esc_url((string) $row['docbee_result_url']); ?>" target="_blank" rel="noopener">DocBee</a>
|
|
<?php elseif (!empty($row['docbee_url'])) : ?>
|
|
<a href="<?php echo esc_url((string) $row['docbee_url']); ?>" target="_blank" rel="noopener">Ticket</a>
|
|
<?php else : ?>
|
|
-
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
public function download_secure_pdf(): void
|
|
{
|
|
if (!is_user_logged_in() || !current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to download this PDF.', 'obyte-qa-tool'), '', ['response' => 403]);
|
|
}
|
|
|
|
$token = isset($_GET['token']) ? sanitize_text_field(wp_unslash((string) $_GET['token'])) : '';
|
|
if ($token === '') {
|
|
wp_die(esc_html__('PDF download link is invalid.', 'obyte-qa-tool'), '', ['response' => 403]);
|
|
}
|
|
|
|
$transient_key = 'oqt_pdf_' . hash('sha256', $token);
|
|
$payload = get_transient($transient_key);
|
|
delete_transient($transient_key);
|
|
|
|
if (!is_array($payload) || (int) ($payload['user_id'] ?? 0) !== get_current_user_id()) {
|
|
wp_die(esc_html__('PDF download link has expired.', 'obyte-qa-tool'), '', ['response' => 403]);
|
|
}
|
|
|
|
global $wpdb;
|
|
$reports_table = self::reports_table();
|
|
$report_id = (int) ($payload['report_id'] ?? 0);
|
|
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$reports_table} WHERE id = %d", $report_id), ARRAY_A);
|
|
if (!is_array($row) || empty($row['pdf_file'])) {
|
|
wp_die(esc_html__('PDF was not found.', 'obyte-qa-tool'), '', ['response' => 404]);
|
|
}
|
|
|
|
$file = $this->secure_pdf_absolute_path((string) $row['pdf_file']);
|
|
if ($file === '' || !is_readable($file)) {
|
|
wp_die(esc_html__('PDF was not found.', 'obyte-qa-tool'), '', ['response' => 404]);
|
|
}
|
|
|
|
$download_name = sanitize_file_name(sprintf(
|
|
'QA-%s-%s-%s.pdf',
|
|
$row['module'] ?: 'report',
|
|
$row['module_version'] ?: 'version',
|
|
$row['pbx_version'] ?: 'pbx'
|
|
));
|
|
|
|
while (ob_get_level()) {
|
|
ob_end_clean();
|
|
}
|
|
|
|
nocache_headers();
|
|
header('Content-Type: application/pdf');
|
|
header('Content-Disposition: inline; filename="' . $download_name . '"');
|
|
$size = filesize($file);
|
|
if ($size !== false) {
|
|
header('Content-Length: ' . $size);
|
|
}
|
|
header('X-Content-Type-Options: nosniff');
|
|
readfile($file);
|
|
exit;
|
|
}
|
|
|
|
private function ensure_secure_report_pdf(array $row): array
|
|
{
|
|
if (!empty($row['pdf_file'])) {
|
|
return $row;
|
|
}
|
|
|
|
$attachment_id = (int) ($row['pdf_attachment_id'] ?? 0);
|
|
if ($attachment_id <= 0) {
|
|
return $row;
|
|
}
|
|
|
|
$source_file = get_attached_file($attachment_id);
|
|
if (!$source_file || !is_readable($source_file)) {
|
|
return $row;
|
|
}
|
|
|
|
$stored = $this->store_secure_pdf_file((string) $source_file, basename((string) $source_file), false);
|
|
if (is_wp_error($stored)) {
|
|
return $row;
|
|
}
|
|
|
|
global $wpdb;
|
|
$reports_table = self::reports_table();
|
|
$wpdb->update(
|
|
$reports_table,
|
|
[
|
|
'pdf_attachment_id' => 0,
|
|
'pdf_url' => '',
|
|
'pdf_file' => (string) $stored['file'],
|
|
],
|
|
['id' => (int) $row['id']],
|
|
['%d', '%s', '%s'],
|
|
['%d']
|
|
);
|
|
|
|
wp_delete_attachment($attachment_id, true);
|
|
|
|
$row['pdf_attachment_id'] = 0;
|
|
$row['pdf_url'] = '';
|
|
$row['pdf_file'] = (string) $stored['file'];
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function migrate_legacy_pdf_attachments(int $limit): void
|
|
{
|
|
global $wpdb;
|
|
|
|
$reports_table = self::reports_table();
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$reports_table} WHERE pdf_attachment_id > 0 AND (pdf_file IS NULL OR pdf_file = '') ORDER BY id DESC LIMIT %d",
|
|
max(1, $limit)
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
if (!is_array($rows)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
if (is_array($row)) {
|
|
$this->ensure_secure_report_pdf($row);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function create_pdf_download_url(array $row): string
|
|
{
|
|
if (empty($row['pdf_file'])) {
|
|
return '';
|
|
}
|
|
|
|
$file = $this->secure_pdf_absolute_path((string) $row['pdf_file']);
|
|
if ($file === '' || !is_readable($file)) {
|
|
return '';
|
|
}
|
|
|
|
$token = wp_generate_password(40, false, false);
|
|
set_transient(
|
|
'oqt_pdf_' . hash('sha256', $token),
|
|
[
|
|
'report_id' => (int) $row['id'],
|
|
'user_id' => get_current_user_id(),
|
|
],
|
|
10 * MINUTE_IN_SECONDS
|
|
);
|
|
|
|
return add_query_arg(
|
|
[
|
|
'action' => 'obyte_qa_pdf',
|
|
'token' => $token,
|
|
],
|
|
admin_url('admin-post.php')
|
|
);
|
|
}
|
|
|
|
public function register_rest_routes(): void
|
|
{
|
|
register_rest_route(self::REST_NAMESPACE, '/templates', [
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'rest_list_templates'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
]);
|
|
|
|
register_rest_route(self::REST_NAMESPACE, '/template', [
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'rest_get_template'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
'args' => [
|
|
'path' => [
|
|
'type' => 'string',
|
|
'required' => true,
|
|
],
|
|
],
|
|
]);
|
|
|
|
register_rest_route(self::REST_NAMESPACE, '/docbee/message', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'rest_post_docbee_message'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
]);
|
|
|
|
register_rest_route(self::REST_NAMESPACE, '/export', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'rest_export_run'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
]);
|
|
|
|
register_rest_route(self::REST_NAMESPACE, '/gitlab/template', [
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => [$this, 'rest_push_gitlab_template'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
]);
|
|
|
|
register_rest_route(self::REST_NAMESPACE, '/reports', [
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => [$this, 'rest_list_reports'],
|
|
'permission_callback' => [$this, 'rest_can_use'],
|
|
]);
|
|
}
|
|
|
|
public function rest_can_use(): bool
|
|
{
|
|
return is_user_logged_in();
|
|
}
|
|
|
|
public function rest_list_templates(WP_REST_Request $request)
|
|
{
|
|
$settings = $this->settings();
|
|
if (empty($settings['gitlab_enabled'])) {
|
|
return new WP_Error('gitlab_disabled', 'GitLab templates are disabled.', ['status' => 403]);
|
|
}
|
|
|
|
$base_url = $settings['gitlab_base_url'];
|
|
$project = $settings['gitlab_project'];
|
|
if (!$base_url || !$project) {
|
|
return new WP_Error('gitlab_missing_config', 'GitLab base URL or project is missing.', ['status' => 400]);
|
|
}
|
|
|
|
$url = $base_url . '/api/v4/projects/' . rawurlencode($project) . '/repository/tree';
|
|
$url = add_query_arg(
|
|
[
|
|
'path' => $settings['gitlab_template_path'],
|
|
'ref' => $settings['gitlab_ref'],
|
|
'per_page' => (int) $settings['gitlab_per_page'],
|
|
],
|
|
$url
|
|
);
|
|
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 20,
|
|
'headers' => $this->gitlab_headers($settings),
|
|
]);
|
|
|
|
$data = $this->remote_json($response, 'gitlab_tree_failed');
|
|
if (is_wp_error($data)) {
|
|
return $data;
|
|
}
|
|
|
|
$files = [];
|
|
foreach ($data as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$name = (string) ($item['name'] ?? '');
|
|
$path = (string) ($item['path'] ?? '');
|
|
$type = (string) ($item['type'] ?? '');
|
|
if ($type === 'blob' && preg_match('/\.ya?ml$/i', $name)) {
|
|
$files[] = [
|
|
'name' => $name,
|
|
'path' => $path,
|
|
];
|
|
}
|
|
}
|
|
|
|
return rest_ensure_response(['templates' => $files]);
|
|
}
|
|
|
|
public function rest_get_template(WP_REST_Request $request)
|
|
{
|
|
$settings = $this->settings();
|
|
if (empty($settings['gitlab_enabled'])) {
|
|
return new WP_Error('gitlab_disabled', 'GitLab templates are disabled.', ['status' => 403]);
|
|
}
|
|
|
|
$path = $this->clean_gitlab_path((string) $request->get_param('path'));
|
|
if (!$path) {
|
|
return new WP_Error('gitlab_bad_path', 'Template path is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
$url = $settings['gitlab_base_url'] . '/api/v4/projects/' . rawurlencode($settings['gitlab_project']) .
|
|
'/repository/files/' . rawurlencode($path) . '/raw';
|
|
$url = add_query_arg(['ref' => $settings['gitlab_ref']], $url);
|
|
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 20,
|
|
'headers' => $this->gitlab_headers($settings),
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return new WP_Error('gitlab_raw_failed', $response->get_error_message(), ['status' => 502]);
|
|
}
|
|
|
|
$code = (int) wp_remote_retrieve_response_code($response);
|
|
if ($code < 200 || $code >= 300) {
|
|
return new WP_Error('gitlab_raw_failed', 'GitLab returned HTTP ' . $code . '.', ['status' => $code ?: 502]);
|
|
}
|
|
|
|
return rest_ensure_response([
|
|
'path' => $path,
|
|
'content' => (string) wp_remote_retrieve_body($response),
|
|
]);
|
|
}
|
|
|
|
public function rest_post_docbee_message(WP_REST_Request $request)
|
|
{
|
|
$settings = $this->settings();
|
|
$params = $request->get_json_params();
|
|
$run = is_array($params) && isset($params['run']) && is_array($params['run']) ? $params['run'] : [];
|
|
if (empty($run['steps']) || !is_array($run['steps'])) {
|
|
return new WP_Error('docbee_bad_run', 'QA run payload is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
$posted = $this->docbee_post_run($run, $settings);
|
|
if (is_wp_error($posted)) {
|
|
return $posted;
|
|
}
|
|
|
|
return rest_ensure_response($posted);
|
|
}
|
|
|
|
public function rest_export_run(WP_REST_Request $request)
|
|
{
|
|
$settings = $this->settings();
|
|
$run_raw = (string) ($request->get_param('run') ?? '');
|
|
$run = json_decode($run_raw, true);
|
|
if (!is_array($run) || empty($run['steps']) || !is_array($run['steps'])) {
|
|
return new WP_Error('qa_bad_run', 'QA run payload is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
$docbee = null;
|
|
if (!empty($settings['docbee_enabled']) && $this->docbee_configured($settings) && $this->extract_ticket_id((string) ($run['docbee_url'] ?? ''))) {
|
|
$posted = $this->docbee_post_run($run, $settings);
|
|
if (is_wp_error($posted)) {
|
|
$docbee = [
|
|
'ok' => false,
|
|
'message' => $posted->get_error_message(),
|
|
];
|
|
} else {
|
|
$docbee = $posted;
|
|
}
|
|
}
|
|
|
|
$pdf_file = '';
|
|
$files = $request->get_file_params();
|
|
if (!empty($settings['storage_enabled']) && !empty($settings['media_pdf_enabled']) && isset($files['pdf']) && is_array($files['pdf']) && (int) ($files['pdf']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
|
|
$uploaded = $this->store_secure_pdf_upload($files['pdf'], $run);
|
|
if (is_wp_error($uploaded)) {
|
|
return $uploaded;
|
|
}
|
|
$pdf_file = (string) $uploaded['file'];
|
|
}
|
|
|
|
$report_id = 0;
|
|
$summary = $this->summary_text($run);
|
|
if (!empty($settings['storage_enabled'])) {
|
|
$stored = $this->store_report($run, $summary, $pdf_file, $docbee);
|
|
if (is_wp_error($stored)) {
|
|
return $stored;
|
|
}
|
|
$report_id = (int) $stored;
|
|
}
|
|
|
|
return rest_ensure_response([
|
|
'ok' => true,
|
|
'report_id' => $report_id,
|
|
'summary' => $summary,
|
|
'pdf_stored' => $pdf_file !== '',
|
|
'docbee' => $docbee,
|
|
]);
|
|
}
|
|
|
|
public function rest_push_gitlab_template(WP_REST_Request $request)
|
|
{
|
|
$settings = $this->settings();
|
|
if (empty($settings['gitlab_enabled']) || empty($settings['gitlab_allow_writes'])) {
|
|
return new WP_Error('gitlab_writes_disabled', 'GitLab template writes are disabled.', ['status' => 403]);
|
|
}
|
|
if (empty($settings['gitlab_token'])) {
|
|
return new WP_Error('gitlab_missing_token', 'GitLab token is missing.', ['status' => 400]);
|
|
}
|
|
|
|
$params = $request->get_json_params();
|
|
$template = is_array($params) && isset($params['template']) && is_array($params['template']) ? $params['template'] : [];
|
|
$yaml = is_array($params) ? (string) ($params['yaml'] ?? '') : '';
|
|
$source_path_input = is_array($params) ? (string) ($params['source_path'] ?? '') : '';
|
|
$source_path = $this->clean_gitlab_path($source_path_input);
|
|
if (empty($template['steps']) || !is_array($template['steps']) || $yaml === '') {
|
|
return new WP_Error('gitlab_bad_template', 'Template payload is invalid.', ['status' => 400]);
|
|
}
|
|
if (empty($template['module']) || empty($template['module_version']) || empty($template['pbx_version'])) {
|
|
return new WP_Error('gitlab_missing_template_meta', 'Module, module version, and PBX version are required for GitLab template export.', ['status' => 400]);
|
|
}
|
|
if (trim($source_path_input) !== '' && !$this->is_gitlab_template_file_path($source_path, $settings)) {
|
|
return new WP_Error('gitlab_bad_path', 'GitLab source path is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
$template_path = trim((string) $settings['gitlab_template_path'], '/');
|
|
if ($source_path !== '') {
|
|
$path = $source_path;
|
|
} else {
|
|
$filename = $this->safe_filename((string) ($template['module'] ?? $template['name'] ?? 'qa-template')) . '.yaml';
|
|
$path = ($template_path !== '' ? $template_path . '/' : '') . $filename;
|
|
}
|
|
$file_url = $settings['gitlab_base_url'] . '/api/v4/projects/' . rawurlencode($settings['gitlab_project']) .
|
|
'/repository/files/' . rawurlencode($path);
|
|
|
|
$check_url = add_query_arg(['ref' => $settings['gitlab_ref']], $file_url);
|
|
$check = wp_remote_get($check_url, [
|
|
'timeout' => 20,
|
|
'headers' => $this->gitlab_headers($settings),
|
|
]);
|
|
$exists = !is_wp_error($check) && (int) wp_remote_retrieve_response_code($check) >= 200 && (int) wp_remote_retrieve_response_code($check) < 300;
|
|
|
|
$message = $exists
|
|
? 'Update QA template for ' . sanitize_text_field((string) $template['module']) . ' ' . sanitize_text_field((string) $template['module_version'])
|
|
: 'Add QA template for ' . sanitize_text_field((string) $template['module']) . ' ' . sanitize_text_field((string) $template['module_version']);
|
|
|
|
$response = wp_remote_request($file_url, [
|
|
'method' => $exists ? 'PUT' : 'POST',
|
|
'timeout' => 20,
|
|
'headers' => array_merge($this->gitlab_headers($settings), ['Content-Type' => 'application/json']),
|
|
'body' => wp_json_encode([
|
|
'branch' => $settings['gitlab_ref'],
|
|
'commit_message' => $message,
|
|
'content' => base64_encode($yaml),
|
|
'encoding' => 'base64',
|
|
]),
|
|
]);
|
|
|
|
$data = $this->remote_json($response, 'gitlab_template_push_failed');
|
|
if (is_wp_error($data)) {
|
|
return $data;
|
|
}
|
|
|
|
return rest_ensure_response([
|
|
'ok' => true,
|
|
'path' => $path,
|
|
'created' => !$exists,
|
|
'result' => $data,
|
|
]);
|
|
}
|
|
|
|
public function rest_list_reports(WP_REST_Request $request)
|
|
{
|
|
global $wpdb;
|
|
self::create_tables();
|
|
$reports_table = self::reports_table();
|
|
$limit = min(100, max(1, (int) ($request->get_param('limit') ?: 20)));
|
|
$rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$reports_table} ORDER BY created_at DESC LIMIT %d", $limit), ARRAY_A);
|
|
$rows = is_array($rows) ? array_map(function ($row) {
|
|
$row['has_pdf'] = !empty($row['pdf_file']) || !empty($row['pdf_attachment_id']);
|
|
unset($row['pdf_attachment_id'], $row['pdf_file'], $row['pdf_url']);
|
|
return $row;
|
|
}, $rows) : [];
|
|
|
|
return rest_ensure_response([
|
|
'reports' => $rows,
|
|
]);
|
|
}
|
|
|
|
private function tester_name(WP_User $user): string
|
|
{
|
|
if ($user->exists()) {
|
|
if ($user->display_name) {
|
|
return $user->display_name;
|
|
}
|
|
|
|
return $user->user_login;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function docbee_configured(array $settings): bool
|
|
{
|
|
return !empty($settings['docbee_base_url'])
|
|
&& !empty($settings['docbee_username'])
|
|
&& !empty($settings['docbee_password']);
|
|
}
|
|
|
|
private function docbee_post_run(array $run, array $settings)
|
|
{
|
|
if (empty($settings['docbee_enabled'])) {
|
|
return new WP_Error('docbee_disabled', 'DocBee posting is disabled.', ['status' => 403]);
|
|
}
|
|
|
|
if (!$this->docbee_configured($settings)) {
|
|
return new WP_Error('docbee_missing_config', 'DocBee credentials are missing.', ['status' => 400]);
|
|
}
|
|
|
|
$ticket_id = $this->extract_ticket_id((string) ($run['docbee_url'] ?? ''));
|
|
if (!$ticket_id) {
|
|
return new WP_Error('docbee_missing_ticket', 'No DocBee ticket ID found in the URL.', ['status' => 400]);
|
|
}
|
|
|
|
$token = $this->docbee_token($settings);
|
|
if (is_wp_error($token)) {
|
|
return $token;
|
|
}
|
|
|
|
$previous_status_id = null;
|
|
if (!empty($settings['docbee_preserve_ticket_status'])) {
|
|
$ticket = $this->docbee_get_json($settings['docbee_base_url'] . '/restApi/v1/ticket/' . rawurlencode($ticket_id) . '?fields=ticketStatus.id,ticketStatus.name,status.id,status.name', $token);
|
|
if (!is_wp_error($ticket)) {
|
|
$previous_status_id = $this->extract_docbee_status_id($ticket);
|
|
}
|
|
}
|
|
|
|
$content = $this->format_docbee_message($run);
|
|
$subject = 'QA Report ' . $ticket_id;
|
|
if (!empty($run['module'])) {
|
|
$subject .= ' - ' . sanitize_text_field((string) $run['module']);
|
|
}
|
|
|
|
$message_url = $settings['docbee_base_url'] . '/restApi/v1/ticket/' . rawurlencode($ticket_id) . '/message';
|
|
$payload = [
|
|
'content' => $content,
|
|
'subject' => $subject,
|
|
'internal' => !empty($settings['docbee_message_internal']),
|
|
'hidden' => !empty($settings['docbee_message_hidden']),
|
|
];
|
|
|
|
$message_response = $this->docbee_post_json($message_url, $payload, $token);
|
|
if (!is_wp_error($message_response)) {
|
|
$restored = false;
|
|
if ($previous_status_id !== null) {
|
|
$restored = $this->docbee_restore_ticket_status($ticket_id, $previous_status_id, $settings, $token);
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'type' => 'message',
|
|
'ticket_id' => $ticket_id,
|
|
'status_restored' => $restored,
|
|
'url' => $this->docbee_result_url($settings['docbee_base_url'], $message_response, 'message'),
|
|
];
|
|
}
|
|
|
|
if (empty($settings['docbee_enable_fallback_note'])) {
|
|
return $message_response;
|
|
}
|
|
|
|
$note_url = $settings['docbee_base_url'] . '/restApi/v1/note';
|
|
$note_payload = [
|
|
'note' => [
|
|
'ticket' => ['id' => (int) $ticket_id],
|
|
'subject' => $subject,
|
|
'text' => $content,
|
|
'internal' => false,
|
|
],
|
|
];
|
|
|
|
$note_response = $this->docbee_post_json($note_url, $note_payload, $token);
|
|
if (is_wp_error($note_response)) {
|
|
return $note_response;
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'type' => 'note',
|
|
'ticket_id' => $ticket_id,
|
|
'status_restored' => false,
|
|
'url' => $this->docbee_result_url($settings['docbee_base_url'], $note_response, 'note'),
|
|
];
|
|
}
|
|
|
|
private function store_report(array $run, string $summary, string $pdf_file, ?array $docbee)
|
|
{
|
|
global $wpdb;
|
|
|
|
self::create_tables();
|
|
|
|
$reports_table = self::reports_table();
|
|
$steps_table = self::steps_table();
|
|
$docbee_result_url = is_array($docbee) && !empty($docbee['url']) ? (string) $docbee['url'] : '';
|
|
|
|
$inserted = $wpdb->insert(
|
|
$reports_table,
|
|
[
|
|
'created_at' => current_time('mysql'),
|
|
'user_id' => get_current_user_id(),
|
|
'module' => $this->plain($run['module'] ?? ''),
|
|
'module_version' => $this->plain($run['module_version'] ?? ''),
|
|
'pbx_version' => $this->plain($run['pbx_version'] ?? ''),
|
|
'olm_nummer' => $this->plain($run['olm_nummer'] ?? ''),
|
|
'tester' => $this->plain($run['tester'] ?? ''),
|
|
'docbee_url' => esc_url_raw((string) ($run['docbee_url'] ?? '')),
|
|
'docbee_result_url' => esc_url_raw($docbee_result_url),
|
|
'summary' => $summary,
|
|
'pdf_attachment_id' => 0,
|
|
'pdf_url' => '',
|
|
'pdf_file' => sanitize_text_field($pdf_file),
|
|
'run_json' => wp_json_encode($run),
|
|
],
|
|
['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s']
|
|
);
|
|
|
|
if (!$inserted) {
|
|
return new WP_Error('qa_report_store_failed', $wpdb->last_error ?: 'Report could not be stored.', ['status' => 500]);
|
|
}
|
|
|
|
$report_id = (int) $wpdb->insert_id;
|
|
$step_index = 0;
|
|
$group_index = -1;
|
|
$current_group = null;
|
|
|
|
foreach ((array) ($run['steps'] ?? []) as $step) {
|
|
if (!is_array($step)) {
|
|
continue;
|
|
}
|
|
|
|
$kind = (string) ($step['kind'] ?? $step['type'] ?? 'step');
|
|
if ($kind === 'group') {
|
|
$current_group = $this->plain($step['title'] ?? '');
|
|
$group_index++;
|
|
continue;
|
|
}
|
|
|
|
$step_index++;
|
|
$wpdb->insert(
|
|
$steps_table,
|
|
[
|
|
'report_id' => $report_id,
|
|
'step_index' => $step_index,
|
|
'step_id' => $this->plain($step['id'] ?? ''),
|
|
'title' => $this->plain($step['title'] ?? ''),
|
|
'expected' => wp_strip_all_tags((string) ($step['expected'] ?? '')),
|
|
'status' => $this->normalize_status((string) ($step['status'] ?? '')),
|
|
'required' => empty($step['required']) ? 0 : 1,
|
|
'comment' => wp_strip_all_tags((string) ($step['comment'] ?? '')),
|
|
'evidence' => esc_url_raw((string) ($step['evidence'] ?? '')),
|
|
'group_title' => $current_group,
|
|
'group_index' => $group_index >= 0 ? $group_index : null,
|
|
],
|
|
['%d', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%d']
|
|
);
|
|
}
|
|
|
|
return $report_id;
|
|
}
|
|
|
|
private function store_secure_pdf_upload(array $file, array $run)
|
|
{
|
|
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
|
return new WP_Error('qa_pdf_upload_failed', 'PDF upload is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
if ((int) ($file['error'] ?? 0) !== UPLOAD_ERR_OK) {
|
|
return new WP_Error('qa_pdf_upload_failed', 'PDF upload failed.', ['status' => 400]);
|
|
}
|
|
|
|
$filename = sprintf(
|
|
'QA-%s-%s-%s-%s.pdf',
|
|
$this->safe_filename((string) ($run['module'] ?? 'mod')),
|
|
$this->safe_filename((string) ($run['module_version'] ?? 'v')),
|
|
$this->safe_filename((string) ($run['pbx_version'] ?? 'pbx')),
|
|
wp_date('Ymd_His')
|
|
);
|
|
|
|
return $this->store_secure_pdf_file((string) $file['tmp_name'], $filename, true);
|
|
}
|
|
|
|
private function store_secure_pdf_file(string $source_file, string $filename, bool $uploaded)
|
|
{
|
|
if (!is_readable($source_file) || !$this->looks_like_pdf($source_file)) {
|
|
return new WP_Error('qa_pdf_upload_failed', 'PDF upload is invalid.', ['status' => 400]);
|
|
}
|
|
|
|
$target = $this->secure_pdf_target($filename);
|
|
if (is_wp_error($target)) {
|
|
return $target;
|
|
}
|
|
|
|
if ($uploaded) {
|
|
$stored = move_uploaded_file($source_file, $target['path']);
|
|
} else {
|
|
$stored = copy($source_file, $target['path']);
|
|
}
|
|
|
|
if (!$stored) {
|
|
return new WP_Error('qa_pdf_store_failed', 'PDF could not be stored securely.', ['status' => 500]);
|
|
}
|
|
|
|
@chmod($target['path'], 0640);
|
|
|
|
return [
|
|
'file' => $target['relative'],
|
|
];
|
|
}
|
|
|
|
private function secure_pdf_base_dir(): string
|
|
{
|
|
$uploads = wp_upload_dir(null, false);
|
|
if (!empty($uploads['error']) || empty($uploads['basedir'])) {
|
|
return '';
|
|
}
|
|
|
|
return trailingslashit((string) $uploads['basedir']) . 'obyte-qa-tool-secure';
|
|
}
|
|
|
|
private function secure_pdf_absolute_path(string $relative): string
|
|
{
|
|
$relative = str_replace('\\', '/', trim(wp_unslash($relative)));
|
|
$relative = ltrim($relative, '/');
|
|
if (strpos($relative, 'obyte-qa-tool-secure/') !== 0) {
|
|
return '';
|
|
}
|
|
|
|
$uploads = wp_upload_dir(null, false);
|
|
if (!empty($uploads['error']) || empty($uploads['basedir'])) {
|
|
return '';
|
|
}
|
|
|
|
$path = trailingslashit((string) $uploads['basedir']) . $relative;
|
|
$real_base = realpath($this->secure_pdf_base_dir());
|
|
$real_path = realpath($path);
|
|
if (!$real_base || !$real_path) {
|
|
return '';
|
|
}
|
|
|
|
$real_base = trailingslashit(wp_normalize_path($real_base));
|
|
$real_path = wp_normalize_path($real_path);
|
|
|
|
return strpos($real_path, $real_base) === 0 ? $real_path : '';
|
|
}
|
|
|
|
private function secure_pdf_target(string $filename)
|
|
{
|
|
$base_dir = $this->secure_pdf_base_dir();
|
|
if ($base_dir === '' || !$this->ensure_secure_pdf_storage($base_dir)) {
|
|
return new WP_Error('qa_pdf_store_failed', 'Secure PDF storage could not be created.', ['status' => 500]);
|
|
}
|
|
|
|
$subdir = wp_date('Y/m');
|
|
$target_dir = trailingslashit($base_dir) . $subdir;
|
|
if (!wp_mkdir_p($target_dir)) {
|
|
return new WP_Error('qa_pdf_store_failed', 'Secure PDF storage could not be created.', ['status' => 500]);
|
|
}
|
|
|
|
$name = sanitize_file_name($filename);
|
|
if ($name === '' || !preg_match('/\.pdf$/i', $name)) {
|
|
$name = 'qa-report.pdf';
|
|
}
|
|
|
|
$name = preg_replace('/\.pdf$/i', '-' . wp_generate_password(16, false, false) . '.pdf', $name);
|
|
$name = wp_unique_filename($target_dir, $name);
|
|
|
|
return [
|
|
'path' => trailingslashit($target_dir) . $name,
|
|
'relative' => 'obyte-qa-tool-secure/' . $subdir . '/' . $name,
|
|
];
|
|
}
|
|
|
|
private function ensure_secure_pdf_storage(string $base_dir): bool
|
|
{
|
|
if (!wp_mkdir_p($base_dir)) {
|
|
return false;
|
|
}
|
|
|
|
$index = trailingslashit($base_dir) . 'index.php';
|
|
if (!file_exists($index)) {
|
|
if (file_put_contents($index, "<?php\n// Silence is golden.\n") === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$htaccess = trailingslashit($base_dir) . '.htaccess';
|
|
if (!file_exists($htaccess)) {
|
|
if (file_put_contents($htaccess, "Options -Indexes\n<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n") === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$web_config = trailingslashit($base_dir) . 'web.config';
|
|
if (!file_exists($web_config)) {
|
|
if (file_put_contents($web_config, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n <system.webServer>\n <security>\n <authorization>\n <remove users=\"*\" roles=\"\" verbs=\"\" />\n <add accessType=\"Deny\" users=\"*\" />\n </authorization>\n </security>\n </system.webServer>\n</configuration>\n") === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function looks_like_pdf(string $file): bool
|
|
{
|
|
$handle = fopen($file, 'rb');
|
|
if (!$handle) {
|
|
return false;
|
|
}
|
|
|
|
$header = fread($handle, 5);
|
|
fclose($handle);
|
|
|
|
return $header === '%PDF-';
|
|
}
|
|
|
|
private function summary_text(array $run): string
|
|
{
|
|
$counts = $this->status_counts((array) ($run['steps'] ?? []));
|
|
return sprintf(
|
|
'%d/%d pass, %d fail, %d skip, %d blocked',
|
|
$counts['pass'],
|
|
$counts['total'],
|
|
$counts['fail'],
|
|
$counts['skip'],
|
|
$counts['blocked']
|
|
);
|
|
}
|
|
|
|
private function status_counts(array $steps): array
|
|
{
|
|
$counts = ['total' => 0, 'pass' => 0, 'fail' => 0, 'skip' => 0, 'blocked' => 0, 'na' => 0];
|
|
foreach ($steps as $step) {
|
|
if (!is_array($step)) {
|
|
continue;
|
|
}
|
|
|
|
$kind = (string) ($step['kind'] ?? $step['type'] ?? 'step');
|
|
if ($kind !== 'step') {
|
|
continue;
|
|
}
|
|
|
|
$counts['total']++;
|
|
$status = $this->normalize_status((string) ($step['status'] ?? ''));
|
|
if (isset($counts[$status])) {
|
|
$counts[$status]++;
|
|
}
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
|
|
private function normalize_status(string $status): string
|
|
{
|
|
$status = strtolower(trim($status));
|
|
$map = [
|
|
'ok' => 'pass',
|
|
'passed' => 'pass',
|
|
'success' => 'pass',
|
|
'true' => 'pass',
|
|
'yes' => 'pass',
|
|
'ko' => 'fail',
|
|
'failed' => 'fail',
|
|
'error' => 'fail',
|
|
'x' => 'fail',
|
|
'skipped' => 'skip',
|
|
'block' => 'blocked',
|
|
'n/a' => 'na',
|
|
'not applicable' => 'na',
|
|
];
|
|
|
|
$status = $map[$status] ?? $status;
|
|
return in_array($status, ['pass', 'fail', 'skip', 'blocked', 'na', ''], true) ? $status : '';
|
|
}
|
|
|
|
private function safe_filename(string $value): string
|
|
{
|
|
$value = sanitize_title($value);
|
|
return $value !== '' ? $value : 'qa';
|
|
}
|
|
|
|
private function base_url(string $url): string
|
|
{
|
|
$url = esc_url_raw(trim($url));
|
|
return untrailingslashit($url);
|
|
}
|
|
|
|
private function sanitize_secret_field(string $field, array $input, array $old, string $clear_field): string
|
|
{
|
|
if (!empty($input[$clear_field])) {
|
|
return '';
|
|
}
|
|
|
|
if (!array_key_exists($field, $input) || (string) $input[$field] === '') {
|
|
return (string) ($old[$field] ?? '');
|
|
}
|
|
|
|
$value = (string) wp_unslash($input[$field]);
|
|
$value = preg_replace('/[\x00-\x1F\x7F]/', '', $value);
|
|
|
|
return trim((string) $value);
|
|
}
|
|
|
|
private function gitlab_headers(array $settings): array
|
|
{
|
|
$headers = ['Accept' => 'application/json'];
|
|
if (!empty($settings['gitlab_token'])) {
|
|
$headers['PRIVATE-TOKEN'] = (string) $settings['gitlab_token'];
|
|
}
|
|
|
|
return $headers;
|
|
}
|
|
|
|
private function remote_json($response, string $error_code)
|
|
{
|
|
if (is_wp_error($response)) {
|
|
return new WP_Error($error_code, $response->get_error_message(), ['status' => 502]);
|
|
}
|
|
|
|
$code = (int) wp_remote_retrieve_response_code($response);
|
|
$body = (string) wp_remote_retrieve_body($response);
|
|
if ($code < 200 || $code >= 300) {
|
|
return new WP_Error($error_code, 'Remote service returned HTTP ' . $code . '.', ['status' => $code ?: 502]);
|
|
}
|
|
|
|
$data = json_decode($body, true);
|
|
if (!is_array($data)) {
|
|
return new WP_Error($error_code, 'Remote service returned invalid JSON.', ['status' => 502]);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function clean_gitlab_path(string $path): string
|
|
{
|
|
$path = trim(wp_unslash($path));
|
|
$path = str_replace('\\', '/', $path);
|
|
$path = ltrim($path, '/');
|
|
if ($path === '' || strpos($path, '..') !== false) {
|
|
return '';
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function is_gitlab_template_file_path(string $path, array $settings): bool
|
|
{
|
|
if ($path === '' || !preg_match('/\.ya?ml$/i', $path)) {
|
|
return false;
|
|
}
|
|
|
|
$template_path = trim((string) ($settings['gitlab_template_path'] ?? ''), '/');
|
|
if ($template_path === '') {
|
|
return true;
|
|
}
|
|
|
|
return strpos($path, $template_path . '/') === 0;
|
|
}
|
|
|
|
private function docbee_token(array $settings)
|
|
{
|
|
$payload = [
|
|
'username' => (string) $settings['docbee_username'],
|
|
'password' => (string) $settings['docbee_password'],
|
|
'lifeTime' => (int) $settings['docbee_lifetime'],
|
|
'lifeTimeRefresh' => !empty($settings['docbee_refresh_lifetime']),
|
|
];
|
|
|
|
$response = wp_remote_post($settings['docbee_base_url'] . '/restApi/login', [
|
|
'timeout' => 20,
|
|
'headers' => [
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode($payload),
|
|
]);
|
|
|
|
$data = $this->remote_json($response, 'docbee_login_failed');
|
|
if (is_wp_error($data)) {
|
|
return $data;
|
|
}
|
|
|
|
$token = (string) ($data['access_token'] ?? '');
|
|
if ($token === '') {
|
|
return new WP_Error('docbee_login_failed', 'DocBee did not return an access token.', ['status' => 502]);
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
private function docbee_post_json(string $url, array $payload, string $token)
|
|
{
|
|
$response = wp_remote_post($url, [
|
|
'timeout' => 20,
|
|
'headers' => [
|
|
'Authorization' => 'Bearer ' . $token,
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode($payload),
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return new WP_Error('docbee_post_failed', $response->get_error_message(), ['status' => 502]);
|
|
}
|
|
|
|
$code = (int) wp_remote_retrieve_response_code($response);
|
|
$body = (string) wp_remote_retrieve_body($response);
|
|
if ($code < 200 || $code >= 300) {
|
|
return new WP_Error('docbee_post_failed', 'DocBee returned HTTP ' . $code . '.', ['status' => $code ?: 502]);
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
private function docbee_get_json(string $url, string $token)
|
|
{
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 20,
|
|
'headers' => [
|
|
'Authorization' => 'Bearer ' . $token,
|
|
'Accept' => 'application/json',
|
|
],
|
|
]);
|
|
|
|
return $this->remote_json($response, 'docbee_get_failed');
|
|
}
|
|
|
|
private function docbee_put_json(string $url, array $payload, string $token)
|
|
{
|
|
$response = wp_remote_request($url, [
|
|
'method' => 'PUT',
|
|
'timeout' => 20,
|
|
'headers' => [
|
|
'Authorization' => 'Bearer ' . $token,
|
|
'Content-Type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
],
|
|
'body' => wp_json_encode($payload),
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$code = (int) wp_remote_retrieve_response_code($response);
|
|
if ($code < 200 || $code >= 300) {
|
|
return new WP_Error('docbee_put_failed', 'DocBee returned HTTP ' . $code . '.', ['status' => $code ?: 502]);
|
|
}
|
|
|
|
return (string) wp_remote_retrieve_body($response);
|
|
}
|
|
|
|
private function extract_docbee_status_id(array $ticket): ?int
|
|
{
|
|
$candidates = [
|
|
$ticket['ticketStatus']['id'] ?? null,
|
|
$ticket['ticketStatus'] ?? null,
|
|
$ticket['status']['id'] ?? null,
|
|
$ticket['status'] ?? null,
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if (is_numeric($candidate)) {
|
|
return (int) $candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function docbee_restore_ticket_status(string $ticket_id, int $status_id, array $settings, string $token): bool
|
|
{
|
|
$url = $settings['docbee_base_url'] . '/restApi/v1/ticket/' . rawurlencode($ticket_id);
|
|
$tries = [
|
|
['ticketStatus' => $status_id],
|
|
['ticketStatus' => ['id' => $status_id]],
|
|
['status' => $status_id],
|
|
['status' => ['id' => $status_id]],
|
|
];
|
|
|
|
foreach ($tries as $payload) {
|
|
$result = $this->docbee_put_json($url, $payload, $token);
|
|
if (!is_wp_error($result)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function extract_ticket_id(string $url): string
|
|
{
|
|
if (preg_match('/(?:\/ticket\/show\/|\/tickets\/|\/ticket\/)(\d+)/i', $url, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
if (preg_match('/^\d+$/', trim($url))) {
|
|
return trim($url);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function format_docbee_message(array $run): string
|
|
{
|
|
$counts = $this->status_counts((array) ($run['steps'] ?? []));
|
|
|
|
$timestamp = !empty($run['ts']) ? strtotime((string) $run['ts']) : time();
|
|
if (!$timestamp) {
|
|
$timestamp = time();
|
|
}
|
|
$date = wp_date('d.m.Y H:i', $timestamp);
|
|
$lines = [];
|
|
$lines[] = 'QA REPORT';
|
|
$lines[] = '=========';
|
|
$lines[] = 'Modul: ' . $this->plain($run['module'] ?? '');
|
|
$lines[] = 'Modul-Version: ' . $this->plain($run['module_version'] ?? '');
|
|
$lines[] = 'PBX-Version: ' . $this->plain($run['pbx_version'] ?? '');
|
|
if (!empty($run['olm_nummer'])) {
|
|
$lines[] = 'OLM-Nummer: ' . $this->plain($run['olm_nummer']);
|
|
}
|
|
$lines[] = 'Tester: ' . $this->plain($run['tester'] ?? '');
|
|
if (!empty($run['docbee_url'])) {
|
|
$lines[] = 'Ticket: ' . $this->plain($run['docbee_url']);
|
|
}
|
|
$lines[] = 'Datum: ' . $date;
|
|
$lines[] = '';
|
|
$lines[] = sprintf('Uebersicht: PASS %d | FAIL %d | SKIP %d | BLOCK %d', $counts['pass'], $counts['fail'], $counts['skip'], $counts['blocked']);
|
|
$lines[] = '';
|
|
$lines[] = '------------------------------------------------------------------------';
|
|
$lines[] = 'Schritt Status Titel';
|
|
$lines[] = '------------------------------------------------------------------------';
|
|
|
|
foreach ((array) ($run['steps'] ?? []) as $step) {
|
|
if (!is_array($step)) {
|
|
continue;
|
|
}
|
|
|
|
$kind = (string) ($step['kind'] ?? $step['type'] ?? 'step');
|
|
if ($kind === 'group') {
|
|
$lines[] = '';
|
|
$lines[] = '## ' . $this->plain($step['title'] ?? '');
|
|
$lines[] = '';
|
|
continue;
|
|
}
|
|
|
|
$status = strtoupper((string) ($step['status'] ?? ''));
|
|
$status = $status === 'BLOCKED' ? 'BLOCK' : $status;
|
|
$required = !empty($step['required']) ? ' [required]' : '';
|
|
$lines[] = sprintf(
|
|
'%-12s %-7s %s%s',
|
|
$this->plain($step['id'] ?? ''),
|
|
$status,
|
|
$this->plain($step['title'] ?? ''),
|
|
$required
|
|
);
|
|
if (!empty($step['comment'])) {
|
|
$lines[] = ' Kommentar: ' . $this->plain($step['comment']);
|
|
}
|
|
if (!empty($step['evidence'])) {
|
|
$lines[] = ' Evidenz: ' . $this->plain($step['evidence']);
|
|
}
|
|
}
|
|
|
|
$lines[] = '------------------------------------------------------------------------';
|
|
$lines[] = 'Legende: PASS, FAIL, SKIP, BLOCK';
|
|
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
private function docbee_result_url(string $base_url, string $response_body, string $prefix): string
|
|
{
|
|
$data = json_decode($response_body, true);
|
|
if (!is_array($data)) {
|
|
return '';
|
|
}
|
|
|
|
if (!empty($data['link'])) {
|
|
$link = (string) $data['link'];
|
|
if (preg_match('/^https?:\/\//i', $link)) {
|
|
return esc_url_raw($link);
|
|
}
|
|
|
|
return esc_url_raw(trailingslashit($base_url) . ltrim($link, '/'));
|
|
}
|
|
|
|
if (!empty($data['id'])) {
|
|
return esc_url_raw(trailingslashit($base_url) . $prefix . '/' . rawurlencode((string) $data['id']));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function plain($value): string
|
|
{
|
|
$value = wp_strip_all_tags((string) $value);
|
|
$value = preg_replace('/\s+/', ' ', $value);
|
|
|
|
return trim((string) $value);
|
|
}
|
|
}
|
|
|
|
register_activation_hook(__FILE__, ['Obyte_QA_Tool', 'activate']);
|
|
Obyte_QA_Tool::instance();
|