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 / view / ShortcodeSuggestionsPresenter.php

ShortcodeSuggestionsPresenter.php in 404 Solution 4.3.0, at includes/view/ShortcodeSuggestionsPresenter.php

330 lines 13.6 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 * Presents frontend shortcode suggestion lists and loading placeholders.
9 */
10 class ABJ_404_Solution_ShortcodeSuggestionsPresenter {
11
12 /**
13 * Render suggestions HTML from a pre-computed suggestions packet.
14 *
15 * @param array<int, mixed> $suggestionsPacket
16 * @param string $requestedURL
17 * @param array<string, mixed>|null $options
18 * @param bool $adminDebugModal
19 * @return string
20 */
21 public function renderSuggestionsHTML(array $suggestionsPacket, string $requestedURL = '',
22 ?array $options = null, bool $adminDebugModal = false): string {
23 $options = $options ?? abj_service('options_repository')->getOptions(true);
24
25 $permalinkSuggestions = isset($suggestionsPacket[0]) ? (array)$suggestionsPacket[0] : [];
26 $rowType = isset($suggestionsPacket[1]) && is_string($suggestionsPacket[1])
27 ? $suggestionsPacket[1] : 'pages';
28
29 $showExtraAdminData = $this->shouldShowAdminData();
30 $adminDebugPresenter = new ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter();
31 $extraDataById = ($showExtraAdminData && $adminDebugModal)
32 ? $adminDebugPresenter->collectExtraData($permalinkSuggestions, abj_service('view_read_service'), abj_service('functions'))
33 : [];
34 $adminDebugData = [];
35
36 $rowResult = $this->renderSuggestionRows($permalinkSuggestions, $rowType, $options,
37 $showExtraAdminData, $adminDebugModal, $adminDebugPresenter, $extraDataById, $adminDebugData);
38 $body = $rowResult['body'] . $this->renderResultFooter($rowResult['displayed'], $options);
39
40 $content = $this->fillTemplate('shortcodeSuggestionsContainer.html', array(
41 'title' => wp_kses_post(
42 $this->replaceSuggestionTemplateToken($this->optionString($options, 'suggest_title'),
43 'suggest_title_text', __('Here are some other great pages', '404-solution'))
44 ),
45 'body' => $body,
46 ));
47
48 if ($showExtraAdminData && $adminDebugModal && !empty($adminDebugData)) {
49 $content .= $adminDebugPresenter->renderScript($adminDebugData);
50 }
51
52 return $content;
53 }
54
55 /**
56 * @param array<int|string, mixed> $permalinkSuggestions
57 * @param string $rowType
58 * @param array<string, mixed> $options
59 * @param bool $showExtraAdminData
60 * @param bool $adminDebugModal
61 * @param ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter $adminDebugPresenter
62 * @param array<string, array<string, mixed>> $extraDataById
63 * @param array<int, array<string, mixed>> $adminDebugData
64 * @return array{body: string, displayed: int}
65 */
66 private function renderSuggestionRows(array $permalinkSuggestions, string $rowType, array $options,
67 bool $showExtraAdminData, bool $adminDebugModal,
68 ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter $adminDebugPresenter,
69 array $extraDataById, array &$adminDebugData): array {
70 $body = '';
71 $displayed = 0;
72 $currentSlug = $this->currentSlug(abj_service('plugin_logic'), abj_service('functions'));
73 $commentPartRaw = abj_service('not_found_response')->getCommentPartAndQueryPartOfRequest();
74 $commentPartAndQueryPart = is_string($commentPartRaw) ? $commentPartRaw : '';
75
76 foreach ($permalinkSuggestions as $idAndType => $linkScore) {
77 $row = $this->renderSuggestionRow((string)$idAndType, $linkScore, $rowType, $options,
78 $currentSlug, $commentPartAndQueryPart, $showExtraAdminData, $adminDebugModal,
79 $adminDebugPresenter, $extraDataById);
80 if ($row === null) {
81 continue;
82 }
83 if ($displayed == 0) {
84 $body .= wp_kses_post($this->optionString($options, 'suggest_before'));
85 }
86 $body .= $row['html'];
87 if ($row['debugData'] !== null) {
88 $adminDebugData[] = $row['debugData'];
89 }
90 $displayed++;
91 if ($displayed >= $this->suggestionLimit($options)) {
92 break;
93 }
94 }
95
96 return array('body' => $body, 'displayed' => $displayed);
97 }
98
99 /**
100 * @param string $idAndTypeStr
101 * @param mixed $linkScore
102 * @param string $rowType
103 * @param array<string, mixed> $options
104 * @param string $currentSlug
105 * @param string $commentPartAndQueryPart
106 * @param bool $showExtraAdminData
107 * @param bool $adminDebugModal
108 * @param ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter $adminDebugPresenter
109 * @param array<string, array<string, mixed>> $extraDataById
110 * @return array{html: string, debugData: array<string, mixed>|null}|null
111 */
112 private function renderSuggestionRow(string $idAndTypeStr, $linkScore, string $rowType, array $options,
113 string $currentSlug, string $commentPartAndQueryPart, bool $showExtraAdminData,
114 bool $adminDebugModal, ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter $adminDebugPresenter,
115 array $extraDataById): ?array {
116 if ($this->isExcludedSuggestion($idAndTypeStr)) {
117 return null;
118 }
119
120 $linkScoreFloat = is_scalar($linkScore) ? (float)$linkScore : 0.0;
121 $permalink = ABJ_404_Solution_PermalinkResolver::permalinkInfoToArray(
122 $idAndTypeStr, $linkScoreFloat, $rowType, $options);
123 $permLink = isset($permalink['link']) && is_string($permalink['link']) ? $permalink['link'] : '';
124 $permTitle = isset($permalink['title']) && is_string($permalink['title']) ? $permalink['title'] : '';
125 $permScore = isset($permalink['score']) && is_numeric($permalink['score']) ? (float)$permalink['score'] : 0.0;
126
127 if ($this->shouldSkipPermalink($permLink, $permScore, $currentSlug, $options)) {
128 return null;
129 }
130
131 $debugData = null;
132 $scoreHtml = $this->scoreHtml($showExtraAdminData, $adminDebugModal, $permScore, $adminDebugPresenter);
133 if ($showExtraAdminData && $adminDebugModal) {
134 $debugData = $adminDebugPresenter->buildItemData($idAndTypeStr, $permTitle, $permScore, $permLink, $extraDataById);
135 }
136
137 return array('html' => $this->fillTemplate('shortcodeSuggestionRow.html', array(
138 'entry_before' => wp_kses_post($this->optionString($options, 'suggest_entrybefore')),
139 'href' => esc_url($permLink . $commentPartAndQueryPart),
140 'title_attr' => esc_attr($permTitle),
141 'title_text' => esc_attr($permTitle),
142 'score_html' => $scoreHtml,
143 'entry_after' => wp_kses_post($this->optionString($options, 'suggest_entryafter')),
144 )) . "\n", 'debugData' => $debugData);
145 }
146
147 /**
148 * @param string $permLink
149 * @param float $permScore
150 * @param string $currentSlug
151 * @param array<string, mixed> $options
152 * @return bool
153 */
154 private function shouldSkipPermalink(string $permLink, float $permScore, string $currentSlug, array $options): bool {
155 if ($currentSlug !== '' && basename($permLink) == $currentSlug) {
156 return true;
157 }
158 $minScoreEnabled = isset($options['suggest_minscore_enabled']) && $options['suggest_minscore_enabled'] == '1';
159 $suggestMinscoreRaw = isset($options['suggest_minscore']) && is_scalar($options['suggest_minscore']) ? $options['suggest_minscore'] : 25;
160 return $minScoreEnabled && $permScore < intval($suggestMinscoreRaw);
161 }
162
163 /**
164 * @param bool $showExtraAdminData
165 * @param bool $adminDebugModal
166 * @param float $permScore
167 * @return string
168 */
169 private function scoreHtml(bool $showExtraAdminData, bool $adminDebugModal, float $permScore,
170 ABJ_404_Solution_ShortcodeSuggestionAdminDebugPresenter $adminDebugPresenter): string {
171 if (!$showExtraAdminData) {
172 return '';
173 }
174 if ($adminDebugModal) {
175 return $adminDebugPresenter->renderScoreLink($permScore);
176 }
177 return ' (' . number_format($permScore, 4) . ')';
178 }
179
180 /**
181 * @param int $displayed
182 * @param array<string, mixed> $options
183 * @return string
184 */
185 private function renderResultFooter(int $displayed, array $options): string {
186 if ($displayed >= 1) {
187 return wp_kses_post($this->optionString($options, 'suggest_after')) . "\n";
188 }
189 return wp_kses_post(
190 $this->replaceSuggestionTemplateToken($this->optionString($options, 'suggest_noresults'),
191 'suggest_noresults_text', __('No suggestions. :/ ', '404-solution'))
192 );
193 }
194
195 /**
196 * @param array<string, mixed> $options
197 * @return int
198 */
199 private function suggestionLimit(array $options): int {
200 return isset($options['suggest_max']) && is_scalar($options['suggest_max']) ? (int)$options['suggest_max'] : 5;
201 }
202
203 /**
204 * Render a loading placeholder for async suggestions.
205 *
206 * @param string $requestedURL
207 * @param array<string, mixed> $options
208 * @return string
209 */
210 public function renderAsyncPlaceholder(string $requestedURL, array $options): string {
211 $suggestMaxVal = isset($options['suggest_max']) && is_scalar($options['suggest_max']) ? $options['suggest_max'] : 5;
212 $suggestMax = intval($suggestMaxVal);
213
214 $skeletons = '';
215 for ($i = 0; $i < $suggestMax; $i++) {
216 $skeletons .= $this->fillTemplate('shortcodeSuggestionSkeleton.html', array()) . "\n";
217 }
218
219 return $this->fillTemplate('shortcodeSuggestionsPlaceholder.html', array(
220 'requested_url' => esc_attr($requestedURL),
221 'title' => wp_kses_post(
222 $this->replaceSuggestionTemplateToken($this->optionString($options, 'suggest_title'),
223 'suggest_title_text', __('Here are some other great pages', '404-solution'))
224 ),
225 'before' => wp_kses_post($this->optionString($options, 'suggest_before')),
226 'loading_text' => esc_html__('Loading page suggestions...', '404-solution'),
227 'skeletons' => $skeletons,
228 'after' => wp_kses_post($this->optionString($options, 'suggest_after')),
229 ));
230 }
231
232 /**
233 * @return bool
234 */
235 private function shouldShowAdminData(): bool {
236 if (!is_user_logged_in()) {
237 return false;
238 }
239 $policy = abj_service('admin_access_policy');
240 return is_object($policy)
241 && method_exists($policy, 'isPluginAdmin')
242 && (bool)$policy->isPluginAdmin();
243 }
244
245 /**
246 * @param string $idAndTypeStr
247 * @return bool
248 */
249 private function isExcludedSuggestion(string $idAndTypeStr): bool {
250 $idTypeParts = explode('|', $idAndTypeStr, 2);
251 $idInt = isset($idTypeParts[0]) && is_numeric($idTypeParts[0]) ? (int)$idTypeParts[0] : 0;
252 $typeInt = isset($idTypeParts[1]) && is_numeric($idTypeParts[1]) ? (int)$idTypeParts[1] : 0;
253 if ($idInt <= 0) {
254 return false;
255 }
256
257 $typePost = (int)ABJ404_TYPE_POST;
258 $typeCat = (int)ABJ404_TYPE_CAT;
259 $typeTag = (int)ABJ404_TYPE_TAG;
260 if ($typeInt === $typePost) {
261 return function_exists('get_post_meta') && get_post_meta($idInt, '_abj404_exclude', true) === '1';
262 }
263 if ($typeInt === $typeCat || $typeInt === $typeTag) {
264 return function_exists('get_term_meta') && get_term_meta($idInt, '_abj404_exclude', true) === '1';
265 }
266 return false;
267 }
268
269 /**
270 * @param object|null $abj404logic
271 * @param object|null $functions
272 * @return string
273 */
274 private function currentSlug($abj404logic, $functions): string {
275 if (!isset($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ||
276 !is_object($abj404logic) || !is_object($functions) ||
277 !method_exists($abj404logic, 'urlNormalization') || !method_exists($functions, 'regexReplace')) {
278 return '';
279 }
280 $normalizer = $abj404logic->urlNormalization();
281 if (!is_object($normalizer) || !method_exists($normalizer, 'removeHomeDirectory')) {
282 return '';
283 }
284 $requestUri = abj_service('sanitizer')->normalizeUrlString($_SERVER['REQUEST_URI']);
285 $slugWithHome = $functions->regexReplace('\?.*', '', $requestUri);
286 return is_string($slugWithHome) ? $normalizer->removeHomeDirectory($slugWithHome) : '';
287 }
288
289 /**
290 * @param array<string, mixed> $options
291 * @param string $key
292 * @return string
293 */
294 private function optionString(array $options, string $key): string {
295 return isset($options[$key]) && is_string($options[$key]) ? $options[$key] : '';
296 }
297
298 /**
299 * Replace both placeholder and legacy bare token forms.
300 *
301 * @param string $template
302 * @param string $tokenNameWithoutBraces
303 * @param string $replacement
304 * @return string
305 */
306 private function replaceSuggestionTemplateToken(string $template, string $tokenNameWithoutBraces, string $replacement): string {
307 return str_replace(
308 array('{' . $tokenNameWithoutBraces . '}', $tokenNameWithoutBraces),
309 $replacement,
310 $template
311 );
312 }
313
314 /**
315 * @param string $name
316 * @param array<string, string> $vars
317 * @return string
318 */
319 private function fillTemplate(string $name, array $vars): string {
320 $template = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false);
321 $search = [];
322 $replace = [];
323 foreach ($vars as $key => $value) {
324 $search[] = '{' . $key . '}';
325 $replace[] = $value;
326 }
327 return str_replace($search, $replace, $template);
328 }
329 }
330