'read', '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, 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 { $settings = $this->settings(); $capability = $this->required_capability($settings); if (!is_user_logged_in()) { $login_url = wp_login_url(get_permalink()); return '
' . esc_html__('Bitte melde dich an, um das QA Tool zu nutzen.', 'obyte-qa-tool') . ' ' . esc_html__('Zum Login', 'obyte-qa-tool') . '
'; } if (!current_user_can($capability)) { return '
' . esc_html__('Dein WordPress Benutzer hat keine Berechtigung für das QA Tool.', 'obyte-qa-tool') . '
'; } $root_id = wp_unique_id('obyte-qa-tool-'); $this->enqueue_frontend_assets($root_id); ob_start(); ?>

Qualitätssicherung

QA System

Keine Vorlage 0 Steps Bereit

Setup

Template laden, Laufdaten setzen, Ticket verknüpfen.

GitLab: nicht geladen DocBee: unbekannt

Testschritte

Status, Kommentar und Evidenz direkt am Schritt pflegen.

Step Erwartung Status Kommentar / Evidenz
Keine Vorlage geladen.
'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['required_capability'] = sanitize_text_field((string) ($input['required_capability'] ?? $old['required_capability'])); $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; ?>

o-Byte QA Tool

Shortcode: [obyte_qa_tool]

Allgemein

GitLab Templates

Speicherung in WordPress

DocBee

get_results("SELECT * FROM {$reports_table} ORDER BY created_at DESC LIMIT 100", ARRAY_A); ?>

o-Byte QA Reports

Die letzten 100 gespeicherten QA-Exports aus der WordPress Datenbank.

ID Datum Modul Version OLM Tester Summary PDF DocBee
Noch keine QA Reports gespeichert.
PDF - DocBee Ticket -
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 { $settings = $this->settings(); return is_user_logged_in() && current_user_can($this->required_capability($settings)); } 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; } } $attachment_id = 0; $pdf_url = ''; $files = $request->get_file_params(); if (!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_pdf_attachment($files['pdf'], $run); if (is_wp_error($uploaded)) { return $uploaded; } $attachment_id = (int) $uploaded['attachment_id']; $pdf_url = (string) $uploaded['url']; } $report_id = 0; $summary = $this->summary_text($run); if (!empty($settings['storage_enabled'])) { $stored = $this->store_report($run, $summary, $attachment_id, $pdf_url, $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_attachment_id' => $attachment_id, 'pdf_url' => $pdf_url, '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'] ?? '') : ''; 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]); } $filename = $this->safe_filename((string) ($template['module'] ?? $template['name'] ?? 'qa-template')) . '.yaml'; $template_path = trim((string) $settings['gitlab_template_path'], '/'); $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; $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); return rest_ensure_response([ 'reports' => is_array($rows) ? $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 required_capability(array $settings): string { $capability = trim((string) ($settings['required_capability'] ?? 'read')); return $capability !== '' ? $capability : 'read'; } 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, int $attachment_id, string $pdf_url, ?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' => $attachment_id, 'pdf_url' => esc_url_raw($pdf_url), 'run_json' => wp_json_encode($run), ], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%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_pdf_attachment(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]); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $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') ); $file_array = [ 'name' => $filename, 'type' => 'application/pdf', 'tmp_name' => $file['tmp_name'], 'error' => (int) ($file['error'] ?? 0), 'size' => (int) ($file['size'] ?? 0), ]; $upload = wp_handle_sideload($file_array, [ 'test_form' => false, 'mimes' => ['pdf' => 'application/pdf'], ]); if (!empty($upload['error'])) { return new WP_Error('qa_pdf_upload_failed', (string) $upload['error'], ['status' => 500]); } $title = 'QA Report'; if (!empty($run['module'])) { $title .= ' - ' . $this->plain($run['module']); } $attachment_id = wp_insert_attachment( [ 'post_mime_type' => 'application/pdf', 'post_title' => $title, 'post_content' => '', 'post_status' => 'inherit', ], $upload['file'] ); if (is_wp_error($attachment_id) || !$attachment_id) { return new WP_Error('qa_pdf_attachment_failed', 'PDF could not be added to the Media Library.', ['status' => 500]); } $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']); if (is_array($metadata)) { wp_update_attachment_metadata($attachment_id, $metadata); } return [ 'attachment_id' => (int) $attachment_id, 'url' => wp_get_attachment_url($attachment_id) ?: '', ]; } 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 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();