PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / feedback / SupportRequestButton.php

SupportRequestButton.php in 404 Solution 4.3.0, at includes/feedback/SupportRequestButton.php

147 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * HTML mount point for the reusable "Send debug log to developer" button.
9 *
10 * Pure rendering helper. No DAO calls, no singleton, no globals. Returns
11 * a single empty <div> carrying the data attributes the JS component
12 * (includes/js/support-request-button.js) needs to bootstrap itself. The
13 * component reads `data-triggered-from` (allowlisted server-side by
14 * Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES) and the optional
15 * `data-context-summary` (a one-line free-text hint shown to the admin in
16 * the confirmation modal so they remember which screen they're sending
17 * from).
18 *
19 * Caller responsibilities:
20 * 1. wp_enqueue_script() the abj404-support-request-button bundle and
21 * its dependency abj404-support-request-client (handled by
22 * AdminAssetEnqueuer::registerSupportRequestAssets()).
23 * 2. Pass a triggered_from slug that exists in the AJAX allowlist.
24 * Drift is caught at the server side (400 response) but the lint in
25 * tests/SupportRequestButtonRenderTest enforces the matching set.
26 */
27 final class ABJ_404_Solution_SupportRequestButton {
28
29 /**
30 * Render an empty mount-point div for the support-request button. The
31 * JS component (window.ABJ404.SupportRequestButton.mount) discovers
32 * the div via its CSS class and bootstraps the button, modal, and
33 * AJAX wiring.
34 *
35 * Both data attributes are escaped with esc_attr() so the caller may
36 * pass arbitrary user-derived strings (e.g. an error message excerpt
37 * as the context_summary) without opening an XSS hole.
38 *
39 * @param string $triggeredFrom Required. One of
40 * Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES.
41 * @param string|null $contextSummary Optional one-line summary shown
42 * inside the modal so the user knows which screen the report is
43 * anchored to. Pass null or '' to omit.
44 * @return string HTML safe to print directly into an admin page.
45 */
46 public static function render(string $triggeredFrom, ?string $contextSummary = null): string {
47 $contextAttr = '';
48 if ($contextSummary !== null && $contextSummary !== '') {
49 $contextAttr = ' data-context-summary="' . self::escAttr($contextSummary) . '"';
50 }
51 $template = self::readTemplate('supportRequestButtonMount.html');
52 $html = strtr($template, [
53 '{triggered_from}' => self::escAttr($triggeredFrom),
54 '{context_summary_attr}' => $contextAttr,
55 ]);
56 return rtrim($html, "\n");
57 }
58
59 /**
60 * Render an inline `<a class="abj404-support-request-link">` anchor that
61 * the JS component (window.ABJ404.SupportRequestButton.mountAll) discovers
62 * and binds to the modal via attachLink(). Used inside admin error
63 * notices where the affordance must read as a small text link in the
64 * body of the notice (UI_AESTHETIC.md "Notices"), not a button or a
65 * call-to-action chip.
66 *
67 * The href fallback is the same-page `#abj404-support-request` anchor
68 * so JS-disabled browsers degrade to a working flow. The link label
69 * is the user-visible text and is escaped with esc_html() because
70 * callers may pass a translated string.
71 *
72 * @param string $triggeredFrom One of
73 * Ajax_SupportRequest::ALLOWED_TRIGGER_SOURCES.
74 * @param string|null $contextSummary Optional one-line summary shown
75 * inside the support modal so the admin knows which screen the
76 * report is anchored to.
77 * @param string $label User-visible link text. Defaults
78 * to "Send debug log to developer" (translated when WP is loaded).
79 * @return string HTML safe to print inside a notice body.
80 */
81 public static function renderInlineLink(string $triggeredFrom,
82 ?string $contextSummary = null, string $label = ''): string {
83 if ($label === '') {
84 $label = function_exists('__')
85 ? (string)__('Send debug log to developer', '404-solution')
86 : 'Send debug log to developer';
87 }
88 $contextAttr = '';
89 if ($contextSummary !== null && $contextSummary !== '') {
90 $contextAttr = ' data-context-summary="' . self::escAttr($contextSummary) . '"';
91 }
92 $template = self::readTemplate('supportRequestInlineLink.html');
93 $html = str_replace('{triggered_from}', self::escAttr($triggeredFrom), $template);
94 $html = str_replace('{context_summary_attr}', $contextAttr, $html);
95 $html = str_replace('{label}', self::escHtml($label), $html);
96 return rtrim($html, "\n");
97 }
98
99 /**
100 * Read an HTML template from includes/html/. Kept private and minimal
101 * so the helper has no dependency on the main Functions facade (so it
102 * works on the degraded admin page where the autoloader may be
103 * incomplete).
104 *
105 * @param string $filename Template filename relative to includes/html/.
106 * @return string Template contents or '' on read failure.
107 */
108 private static function readTemplate(string $filename): string {
109 $path = dirname(__DIR__) . '/html/' . $filename;
110 if (!is_readable($path)) {
111 return '';
112 }
113 $contents = @file_get_contents($path);
114 return is_string($contents) ? $contents : '';
115 }
116
117 /**
118 * esc_html() that survives running outside of a real WordPress request.
119 * Mirrors escAttr() above.
120 *
121 * @param string $value
122 * @return string
123 */
124 private static function escHtml(string $value): string {
125 if (function_exists('esc_html')) {
126 return (string)esc_html($value);
127 }
128 return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
129 }
130
131 /**
132 * Wrapper around esc_attr() that survives running outside of a real
133 * WordPress request (PHPStan-bench, structural tests). When WP is
134 * loaded the real esc_attr() handles the escaping; otherwise a
135 * minimal htmlspecialchars() shim keeps the output safe.
136 *
137 * @param string $value
138 * @return string
139 */
140 private static function escAttr(string $value): string {
141 if (function_exists('esc_attr')) {
142 return (string)esc_attr($value);
143 }
144 return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
145 }
146 }
147