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

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

232 lines 9.1 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_404_Solution_SuggestionTransient::normalizedUrl($urlRequest);
100 $transientKey = ABJ_404_Solution_SuggestionTransient::transientKeyForNormalizedUrl($urlForCacheKey);
101 $cached = ABJ_404_Solution_SuggestionTransient::fromRaw(get_transient($transientKey));
102
103 if ($cached !== null) {
104 if ($cached->isComplete()) {
105 // Suggestions ready, use cached data
106 $content .= self::suggestionsPresenter()->renderSuggestionsHTML(
107 $cached->getSuggestionsPacket(),
108 $urlRequest,
109 $options,
110 true
111 );
112 $content .= "\n<!-- " . ABJ404_PP . " - End 404 suggestions (cached) -->\n";
113 return $content;
114
115 } elseif ($cached->isPending()) {
116 // Still computing, show loading placeholder
117 self::enqueueAsyncPollingScript($urlRequest);
118 $content .= self::renderAsyncPlaceholder($urlRequest, $options);
119 $content .= "\n<!-- " . ABJ404_PP . " - Suggestions loading -->\n";
120 return $content;
121 }
122 }
123
124 // No async data - fall back to synchronous computation
125 $urlSlugOnly = $abj404logic->urlNormalization()->removeHomeDirectory($urlRequest);
126
127 // Try cache first (populated by processRedirect() for existing redirects)
128 $permalinkSuggestionsPacket = $abj404spellChecker->getFromPermalinkCache($urlSlugOnly);
129
130 // If cache miss, compute suggestions
131 if (empty($permalinkSuggestionsPacket) || empty($permalinkSuggestionsPacket[0])) {
132 $suggestCatsOpt = isset($options['suggest_cats']) && is_string($options['suggest_cats']) ? $options['suggest_cats'] : '1';
133 $suggestTagsOpt = isset($options['suggest_tags']) && is_string($options['suggest_tags']) ? $options['suggest_tags'] : '1';
134 $permalinkSuggestionsPacket = $abj404spellChecker->findMatchingPosts($urlSlugOnly,
135 $suggestCatsOpt, $suggestTagsOpt);
136 }
137
138 $content .= self::suggestionsPresenter()->renderSuggestionsHTML(
139 array_values($permalinkSuggestionsPacket),
140 $urlRequest,
141 $options,
142 true
143 );
144
145 $content .= "\n<!-- " . ABJ404_PP . " - End 404 suggestions for slug " . esc_html($urlSlugOnly) . " -->\n";
146
147 return $content;
148 } finally {
149 $localeScope->restore($didSwitchLocale);
150 }
151 }
152
153 /**
154 * Render suggestions HTML from pre-computed data (for AJAX polling response).
155 * This method is called by Ajax_SuggestionPolling when suggestions are ready.
156 *
157 * @param array<int, mixed> $suggestionsPacket The suggestions data from findMatchingPosts()
158 * @param string $requestedURL The original 404 URL (for debugging)
159 * @return string HTML content for suggestions
160 */
161 public static function renderSuggestionsHTML(array $suggestionsPacket, string $requestedURL = ''): string {
162 $localeScope = self::localeScope();
163 $didSwitchLocale = $localeScope->switchToFrontendLocale();
164 try {
165 return self::suggestionsPresenter()->renderSuggestionsHTML($suggestionsPacket, $requestedURL, null, false);
166 } finally {
167 $localeScope->restore($didSwitchLocale);
168 }
169 }
170
171 /**
172 * Render a loading placeholder for async suggestions.
173 * Shows skeleton loading animation while suggestions are being computed.
174 *
175 * @param string $requestedURL The 404 URL being looked up
176 * @param array<string, mixed> $options Plugin options
177 * @return string HTML placeholder with loading state
178 */
179 public static function renderAsyncPlaceholder(string $requestedURL, array $options): string {
180 $localeScope = self::localeScope();
181 $didSwitchLocale = $localeScope->switchToFrontendLocale();
182 try {
183 return self::suggestionsPresenter()->renderAsyncPlaceholder($requestedURL, $options);
184 } finally {
185 $localeScope->restore($didSwitchLocale);
186 }
187 }
188
189 /**
190 * Enqueue the async suggestion polling JavaScript.
191 *
192 * @param string $requestedURL The 404 URL for polling
193 * @return void
194 */
195 public static function enqueueAsyncPollingScript(string $requestedURL): void {
196 // Enqueue jQuery dependency
197 wp_enqueue_script('jquery');
198
199 // Enqueue polling script. Use ABJ404_URL (plugin root) rather than
200 // plugin_dir_url(__FILE__): this file lives in includes/frontend/ but
201 // the JS lives in includes/ajax/, so a relative-to-__FILE__ URL points
202 // at a non-existent path (i961).
203 wp_enqueue_script(
204 'abj404-suggestion-polling',
205 ABJ404_URL . 'includes/ajax/SuggestionPolling.js',
206 array('jquery'),
207 ABJ404_VERSION,
208 true // Load in footer
209 );
210
211 // Pass AJAX URL, nonce, and localized strings to JavaScript
212 wp_localize_script('abj404-suggestion-polling', 'abj404_suggestions', array(
213 'ajax_url' => admin_url('admin-ajax.php'),
214 'nonce' => wp_create_nonce('abj404_poll_suggestions'),
215 'no_suggestions_text' => __('No suggestions. :/ ', '404-solution')
216 ));
217
218 // Enqueue the front-end suggestions CSS (loading skeleton + the
219 // admin-only note the finished list carries). See note above on
220 // ABJ404_URL vs. plugin_dir_url. The handle comes from the note
221 // presenter, which enqueues the same sheet on the synchronous path,
222 // so the two can never drift into two <link> tags for one file.
223 wp_enqueue_style(
224 ABJ_404_Solution_ShortcodeSuggestionsAdminNotePresenter::STYLE_HANDLE,
225 ABJ404_URL . 'includes/css/suggestions-loading.css',
226 array(),
227 ABJ404_VERSION
228 );
229 }
230
231 }
232