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 / frontend / ShortCode.php

ShortCode.php in 404 Solution 4.3.0, at includes/frontend/ShortCode.php

229 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /* Functions in this class should only be for plugging into WordPress listeners (filters, actions, etc). */
9
10 class ABJ_404_Solution_ShortCode {
11
12 /** @var self|null */
13 private static $instance = null;
14 /**
15 * Test seam: install or clear the cached singleton instance without
16 * private-field reflection. Pass null to reset between tests; pass a
17 * configured instance (or double) to install it. Mirrors the setInstance()
18 * contract on DataAccess / PluginLogic (M105 singleton-reset seam).
19 *
20 * @param self|null $instance
21 * @return void
22 */
23 public static function setInstance($instance) {
24 self::$instance = $instance;
25 }
26
27
28 /** @return self */
29 public static function getInstance(): self {
30 if (self::$instance == null) {
31 self::$instance = new ABJ_404_Solution_ShortCode();
32 }
33
34 return self::$instance;
35 }
36
37 /** @return ABJ_404_Solution_FrontendSuggestionLocaleScope */
38 private static function localeScope(): ABJ_404_Solution_FrontendSuggestionLocaleScope {
39 return new ABJ_404_Solution_FrontendSuggestionLocaleScope();
40 }
41
42 /** @return ABJ_404_Solution_ShortcodeRequestedUrlResolver */
43 private static function requestedUrlResolver(): ABJ_404_Solution_ShortcodeRequestedUrlResolver {
44 return new ABJ_404_Solution_ShortcodeRequestedUrlResolver();
45 }
46
47 /** @return ABJ_404_Solution_ShortcodeUrlBarUpdater */
48 private static function urlBarUpdater(): ABJ_404_Solution_ShortcodeUrlBarUpdater {
49 return new ABJ_404_Solution_ShortcodeUrlBarUpdater();
50 }
51
52 /** @return ABJ_404_Solution_ShortcodeSuggestionsPresenter */
53 private static function suggestionsPresenter(): ABJ_404_Solution_ShortcodeSuggestionsPresenter {
54 return new ABJ_404_Solution_ShortcodeSuggestionsPresenter();
55 }
56
57 /** If we're currently redirecting to a custom 404 page and we are about to show page
58 * suggestions then update the URL displayed to the user.
59 * @return void
60 */
61 static function updateURLbarIfNecessary(): void {
62 self::urlBarUpdater()->updateIfNecessary();
63 }
64
65 /**
66 * @param array<string, mixed> $atts
67 * @return string
68 */
69 static function shortcodePageSuggestions( array $atts ): string {
70 $localeScope = self::localeScope();
71 $didSwitchLocale = $localeScope->switchToFrontendLocale();
72 try {
73 $abj404logic = abj_service('plugin_logic');
74 $abj404spellChecker = abj_service('spell_checker');
75 $f = abj_service('functions');
76
77 // Attributes
78 $atts = shortcode_atts(
79 array(
80 ),
81 $atts
82 );
83
84 $options = abj_service('options_repository')->getOptions();
85
86 $content = "\n<!-- " . ABJ404_PP . " - Begin 404 suggestions. -->\n";
87
88 $urlResult = self::requestedUrlResolver()->resolve($f);
89 $content .= $urlResult['cookieScripts'];
90 $urlRequest = $urlResult['url'];
91
92 if ($urlRequest == '') {
93 // if no 404 was detected then we don't offer any suggestions
94 return "<!-- " . ABJ404_PP . " - No 404 was detected. No suggestions to offer. -->\n";
95 }
96
97 // Check for cached suggestion computation (transient-based).
98 // Normalize at the boundary: see ABJ_404_Solution_SuggestionTransient.
99 $urlForCacheKey = abj_service('url_encoder')->normalizeURLForCacheKey($urlRequest);
100 $urlKey = md5($urlForCacheKey);
101 $transientKey = 'abj404_suggest_' . $urlKey;
102 $cached = ABJ_404_Solution_SuggestionTransient::fromRaw(get_transient($transientKey));
103
104 if ($cached !== null) {
105 if ($cached->isComplete()) {
106 // Suggestions ready, use cached data
107 $content .= self::suggestionsPresenter()->renderSuggestionsHTML(
108 $cached->getSuggestionsPacket(),
109 $urlRequest,
110 $options,
111 true
112 );
113 $content .= "\n<!-- " . ABJ404_PP . " - End 404 suggestions (cached) -->\n";
114 return $content;
115
116 } elseif ($cached->isPending()) {
117 // Still computing, show loading placeholder
118 self::enqueueAsyncPollingScript($urlRequest);
119 $content .= self::renderAsyncPlaceholder($urlRequest, $options);
120 $content .= "\n<!-- " . ABJ404_PP . " - Suggestions loading -->\n";
121 return $content;
122 }
123 }
124
125 // No async data - fall back to synchronous computation
126 $urlSlugOnly = $abj404logic->urlNormalization()->removeHomeDirectory($urlRequest);
127
128 // Try cache first (populated by processRedirect() for existing redirects)
129 $permalinkSuggestionsPacket = $abj404spellChecker->getFromPermalinkCache($urlSlugOnly);
130
131 // If cache miss, compute suggestions
132 if (empty($permalinkSuggestionsPacket) || empty($permalinkSuggestionsPacket[0])) {
133 $suggestCatsOpt = isset($options['suggest_cats']) && is_string($options['suggest_cats']) ? $options['suggest_cats'] : '1';
134 $suggestTagsOpt = isset($options['suggest_tags']) && is_string($options['suggest_tags']) ? $options['suggest_tags'] : '1';
135 $permalinkSuggestionsPacket = $abj404spellChecker->findMatchingPosts($urlSlugOnly,
136 $suggestCatsOpt, $suggestTagsOpt);
137 }
138
139 $content .= self::suggestionsPresenter()->renderSuggestionsHTML(
140 array_values($permalinkSuggestionsPacket),
141 $urlRequest,
142 $options,
143 true
144 );
145
146 $content .= "\n<!-- " . ABJ404_PP . " - End 404 suggestions for slug " . esc_html($urlSlugOnly) . " -->\n";
147
148 return $content;
149 } finally {
150 $localeScope->restore($didSwitchLocale);
151 }
152 }
153
154 /**
155 * Render suggestions HTML from pre-computed data (for AJAX polling response).
156 * This method is called by Ajax_SuggestionPolling when suggestions are ready.
157 *
158 * @param array<int, mixed> $suggestionsPacket The suggestions data from findMatchingPosts()
159 * @param string $requestedURL The original 404 URL (for debugging)
160 * @return string HTML content for suggestions
161 */
162 public static function renderSuggestionsHTML(array $suggestionsPacket, string $requestedURL = ''): string {
163 $localeScope = self::localeScope();
164 $didSwitchLocale = $localeScope->switchToFrontendLocale();
165 try {
166 return self::suggestionsPresenter()->renderSuggestionsHTML($suggestionsPacket, $requestedURL, null, false);
167 } finally {
168 $localeScope->restore($didSwitchLocale);
169 }
170 }
171
172 /**
173 * Render a loading placeholder for async suggestions.
174 * Shows skeleton loading animation while suggestions are being computed.
175 *
176 * @param string $requestedURL The 404 URL being looked up
177 * @param array<string, mixed> $options Plugin options
178 * @return string HTML placeholder with loading state
179 */
180 public static function renderAsyncPlaceholder(string $requestedURL, array $options): string {
181 $localeScope = self::localeScope();
182 $didSwitchLocale = $localeScope->switchToFrontendLocale();
183 try {
184 return self::suggestionsPresenter()->renderAsyncPlaceholder($requestedURL, $options);
185 } finally {
186 $localeScope->restore($didSwitchLocale);
187 }
188 }
189
190 /**
191 * Enqueue the async suggestion polling JavaScript.
192 *
193 * @param string $requestedURL The 404 URL for polling
194 * @return void
195 */
196 public static function enqueueAsyncPollingScript(string $requestedURL): void {
197 // Enqueue jQuery dependency
198 wp_enqueue_script('jquery');
199
200 // Enqueue polling script. Use ABJ404_URL (plugin root) rather than
201 // plugin_dir_url(__FILE__): this file lives in includes/frontend/ but
202 // the JS lives in includes/ajax/, so a relative-to-__FILE__ URL points
203 // at a non-existent path (i961).
204 wp_enqueue_script(
205 'abj404-suggestion-polling',
206 ABJ404_URL . 'includes/ajax/SuggestionPolling.js',
207 array('jquery'),
208 ABJ404_VERSION,
209 true // Load in footer
210 );
211
212 // Pass AJAX URL, nonce, and localized strings to JavaScript
213 wp_localize_script('abj404-suggestion-polling', 'abj404_suggestions', array(
214 'ajax_url' => admin_url('admin-ajax.php'),
215 'nonce' => wp_create_nonce('abj404_poll_suggestions'),
216 'no_suggestions_text' => __('No suggestions. :/ ', '404-solution')
217 ));
218
219 // Enqueue loading CSS. See note above on ABJ404_URL vs. plugin_dir_url.
220 wp_enqueue_style(
221 'abj404-suggestions-loading',
222 ABJ404_URL . 'includes/css/suggestions-loading.css',
223 array(),
224 ABJ404_VERSION
225 );
226 }
227
228 }
229