PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 / gsc / GscAdminSectionRenderer.php

GscAdminSectionRenderer.php in 404 Solution trunk, at includes/gsc/GscAdminSectionRenderer.php

253 lines 11.7 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 * Renders the Google Search Console admin settings/status section.
9 *
10 * HTML templates live in includes/html/gsc*.html with {placeholder} tokens; this
11 * class loads them via FileSystemService::readFileContents() and substitutes
12 * translated labels / per-request URLs / dynamic data via str_replace().
13 */
14 class ABJ_404_Solution_GscAdminSectionRenderer {
15
16 /** @var ABJ_404_Solution_GscOAuthTokenStore */
17 private $oauthStore;
18
19 /** @var ABJ_404_Solution_GscSearchAnalyticsClient */
20 private $searchAnalytics;
21
22 public function __construct(
23 ABJ_404_Solution_GscOAuthTokenStore $oauthStore,
24 ABJ_404_Solution_GscSearchAnalyticsClient $searchAnalytics
25 ) {
26 $this->oauthStore = $oauthStore;
27 $this->searchAnalytics = $searchAnalytics;
28 }
29
30 /**
31 * Render the inner content for the GSC settings/status card.
32 *
33 * By default the UI state is derived from the OAuth/token store this
34 * renderer already holds, so callers do not have to compute and pass it.
35 * A caller may pass an explicit $state to render a specific branch (used to
36 * exercise states that are unreachable through normal configuration, e.g.
37 * 'not_configured' while centralized mode is active).
38 *
39 * @param string|null $state Force a specific UI state, or null to derive it.
40 * @return string HTML.
41 */
42 public function renderAdminSection(?string $state = null): string {
43 if ($state === null) {
44 $state = $this->oauthStore->getState();
45 }
46 switch ($state) {
47 case 'not_configured':
48 return $this->renderNotConfiguredState();
49 case 'configured_not_connected':
50 return $this->renderConfiguredNotConnectedState();
51 case 'error':
52 return $this->renderErrorState();
53 default:
54 return $this->renderConnectedState();
55 }
56 }
57
58 /**
59 * State: no custom credentials entered and centralized mode not yet authorized.
60 *
61 * @return string
62 */
63 private function renderNotConfiguredState(): string {
64 $tpl = $this->loadTemplate('gscNotConfiguredState.html');
65 return strtr($tpl, [
66 '{intro_text}' => esc_html__('Connect to Google Search Console to see which broken URLs were getting real search traffic.', '404-solution'),
67 '{advanced_label}' => esc_html__('Advanced: use your own Google Cloud credentials', '404-solution'),
68 '{custom_credentials_form}' => $this->renderCustomCredentialsForm(),
69 ]);
70 }
71
72 /**
73 * State: configured but OAuth not yet completed.
74 *
75 * @return string
76 */
77 private function renderConfiguredNotConnectedState(): string {
78 $authUrl = $this->oauthStore->buildAuthUrl();
79 $revokeUrl = wp_nonce_url(admin_url('admin-ajax.php?action=abj404_gsc_revoke'), 'abj404_gsc_revoke');
80
81 if ($this->oauthStore->isCentralizedMode()) {
82 $tpl = $this->loadTemplate('gscConfiguredNotConnectedCentralized.html');
83 return strtr($tpl, [
84 '{intro_text}' => esc_html__('Connect to Google Search Console to see which broken URLs were getting real search traffic.', '404-solution'),
85 '{auth_url}' => esc_url($authUrl),
86 '{connect_label}' => esc_html__('Connect to Google Search Console', '404-solution'),
87 '{advanced_label}' => esc_html__('Advanced: use your own Google Cloud credentials', '404-solution'),
88 '{custom_credentials_form}' => $this->renderCustomCredentialsForm(),
89 ]);
90 }
91
92 $tpl = $this->loadTemplate('gscConfiguredNotConnectedCustom.html');
93 // allow-em-dash: preserving existing translated admin status label during renderer extraction.
94 $statusLabel = esc_html__('Credentials saved — authorization required', '404-solution');
95 return strtr($tpl, [
96 '{status_label}' => $statusLabel,
97 '{auth_prompt}' => esc_html__('Click the button below to authorize access to your Search Console data.', '404-solution'),
98 '{auth_url}' => esc_url($authUrl),
99 '{authorize_label}' => esc_html__('Authorize with Google', '404-solution'),
100 '{revoke_url}' => esc_url($revokeUrl),
101 '{remove_label}' => esc_html__('Remove Credentials', '404-solution'),
102 ]);
103 }
104
105 /**
106 * Render the custom credentials form used in advanced sections.
107 *
108 * @return string HTML.
109 */
110 private function renderCustomCredentialsForm(): string {
111 $callbackUrl = $this->oauthStore->getCallbackUrl();
112 $s = $this->oauthStore->getSettings();
113 $copiedLabel = esc_js(__('Copied!', '404-solution'));
114
115 $stepCreateProject = sprintf(
116 esc_html__('Create a project in %s.', '404-solution'),
117 '<a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a>'
118 );
119
120 $tpl = $this->loadTemplate('gscCustomCredentialsForm.html');
121 return strtr($tpl, [
122 '{setup_steps_label}' => esc_html__('Setup steps:', '404-solution'),
123 '{step_create_project}' => $stepCreateProject,
124 '{step_enable_api}' => esc_html__('Enable the "Google Search Console API".', '404-solution'),
125 '{step_create_credentials}' => esc_html__('Create OAuth 2.0 credentials (Web application type).', '404-solution'),
126 '{step_add_redirect_uri}' => esc_html__('Add this Authorized Redirect URI to your OAuth client:', '404-solution'),
127 '{callback_url}' => esc_html($callbackUrl),
128 '{copy_label}' => esc_html__('Copy', '404-solution'),
129 '{copied_label}' => (string)$copiedLabel,
130 '{step_enter_credentials}' => esc_html__('Enter your Client ID and Client Secret below.', '404-solution'),
131 '{nonce_field}' => wp_nonce_field('abj404_gsc_save', '_wpnonce_gsc', true, false),
132 '{client_id_label}' => esc_html__('Client ID', '404-solution'),
133 '{client_id_value}' => esc_attr($s['client_id']),
134 '{client_secret_label}' => esc_html__('Client Secret', '404-solution'),
135 '{client_secret_value}' => esc_attr($s['client_secret']),
136 '{site_url_label}' => esc_html__('Search Console Site URL', '404-solution'),
137 '{site_url_value}' => esc_attr($s['site_url']),
138 '{site_url_help}' => esc_html__('The site URL as registered in Search Console (e.g. https://example.com/).', '404-solution'),
139 '{save_label}' => esc_html__('Save Credentials', '404-solution'),
140 ]);
141 }
142
143 /**
144 * State: fully connected.
145 *
146 * @return string
147 */
148 private function renderConnectedState(): string {
149 $revokeUrl = wp_nonce_url(admin_url('admin-ajax.php?action=abj404_gsc_revoke'), 'abj404_gsc_revoke');
150
151 $headerTpl = $this->loadTemplate('gscConnectedHeader.html');
152 $html = strtr($headerTpl, [
153 '{status_label}' => esc_html__('Connected to Google Search Console', '404-solution'),
154 '{description}' => esc_html__('Search traffic data for your captured 404 URLs is shown below. Data is refreshed nightly.', '404-solution'),
155 '{revoke_url}' => esc_url($revokeUrl),
156 '{disconnect_label}' => esc_html__('Disconnect', '404-solution'),
157 ]);
158
159 $cached = $this->searchAnalytics->getCachedData();
160
161 if (is_array($cached) && !empty($cached)) {
162 $rowTpl = $this->loadTemplate('gscConnectedTableRow.html');
163 $rows = '';
164 foreach (array_slice($cached, 0, 25) as $row) {
165 if (!is_array($row)) {
166 continue;
167 }
168 $rowUrl = isset($row['url']) && is_scalar($row['url']) ? (string)$row['url'] : '';
169 $rowClicks = isset($row['clicks']) && is_scalar($row['clicks']) ? (string)$row['clicks'] : '0';
170 $rowImpressions = isset($row['impressions']) && is_scalar($row['impressions']) ? (string)$row['impressions'] : '0';
171 $rowPosition = isset($row['position']) && is_scalar($row['position']) ? (string)$row['position'] : '-';
172 $rows .= strtr($rowTpl, [
173 '{url}' => esc_html($rowUrl),
174 '{clicks}' => esc_html($rowClicks),
175 '{impressions}' => esc_html($rowImpressions),
176 '{position}' => esc_html($rowPosition),
177 ]);
178 }
179 $tableTpl = $this->loadTemplate('gscConnectedTable.html');
180 $html .= strtr($tableTpl, [
181 '{table_heading}' => esc_html__('404 URLs with Search Traffic (last 90 days)', '404-solution'),
182 '{th_url}' => esc_html__('URL', '404-solution'),
183 '{th_clicks}' => esc_html__('Clicks', '404-solution'),
184 '{th_impressions}' => esc_html__('Impressions', '404-solution'),
185 '{th_position}' => esc_html__('Avg. Position', '404-solution'),
186 '{rows}' => $rows,
187 ]);
188 } elseif ($this->searchAnalytics->isRefreshNeeded()) {
189 $html .= $this->renderMutedNotice(
190 esc_html__('GSC data is being fetched in the background. Reload this page in a few minutes.', '404-solution')
191 );
192 } else {
193 $html .= $this->renderMutedNotice(
194 esc_html__('No search traffic data found for your captured 404 URLs in the last 90 days.', '404-solution')
195 );
196 }
197
198 return $html;
199 }
200
201 /**
202 * State: authorization was attempted but failed.
203 *
204 * @return string
205 */
206 private function renderErrorState(): string {
207 $error = $this->oauthStore->getLastOAuthError();
208 $authUrl = $this->oauthStore->buildAuthUrl();
209 $revokeUrl = wp_nonce_url(admin_url('admin-ajax.php?action=abj404_gsc_revoke'), 'abj404_gsc_revoke');
210
211 $errorBlock = '';
212 if ($error !== '') {
213 $errorParaTpl = $this->loadTemplate('gscErrorMessageParagraph.html');
214 $errorBlock = strtr($errorParaTpl, ['{error}' => esc_html($error)]);
215 }
216
217 $tpl = $this->loadTemplate('gscErrorState.html');
218 return strtr($tpl, [
219 '{title}' => esc_html__('Authorization failed', '404-solution'),
220 '{error_block}' => $errorBlock,
221 '{instructions}' => esc_html__("Click 'Try Again' to retry authorization, or 'Remove Credentials' to start over.", '404-solution'),
222 '{auth_url}' => esc_url($authUrl),
223 '{try_again_label}' => esc_html__('Try Again', '404-solution'),
224 '{revoke_url}' => esc_url($revokeUrl),
225 '{remove_label}' => esc_html__('Remove Credentials', '404-solution'),
226 ]);
227 }
228
229 /**
230 * Render the small muted notice paragraph used when the connected table is
231 * empty (refresh-in-progress / no-data cases).
232 *
233 * @param string $message Already-escaped translated message.
234 * @return string HTML.
235 */
236 private function renderMutedNotice(string $message): string {
237 $tpl = $this->loadTemplate('gscMutedNotice.html');
238 return strtr($tpl, ['{message}' => $message]);
239 }
240
241 /**
242 * Read an HTML template from includes/html/.
243 *
244 * @param string $filename Template filename relative to includes/html/.
245 * @return string Template contents.
246 */
247 private function loadTemplate(string $filename): string {
248 return ABJ_404_Solution_FileSystemService::readFileContents(
249 dirname(__DIR__) . '/html/' . $filename
250 );
251 }
252 }
253