PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.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 / SpellPostListeners.php

SpellPostListeners.php in 404 Solution 4.2.0, at includes/SpellPostListeners.php

293 lines 9.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 * WordPress post save/delete listeners and permalink/N-gram cache
9 * invalidation for the spell-checking subsystem.
10 *
11 * Extracted from SpellCheckerTrait_PostListeners as a standalone class
12 * with explicit dependency injection.
13 */
14 class ABJ_404_Solution_SpellPostListeners {
15
16 /** @var ABJ_404_Solution_Functions */
17 private $f;
18
19 /** @var ABJ_404_Solution_PluginLogic */
20 private $logic;
21
22 /** @var ABJ_404_Solution_Logging */
23 private $logger;
24
25 /** @var ABJ_404_Solution_ContentRepository */
26 private $contentRepository;
27
28 /** @var ABJ_404_Solution_PermalinkCache */
29 private $permalinkCache;
30
31 /** @var ABJ_404_Solution_NGramFilter */
32 private $ngramFilter;
33
34 /** @var ABJ_404_Solution_PublishedPostsProvider|null */
35 private ?ABJ_404_Solution_PublishedPostsProvider $publishedPostsProvider = null;
36
37 /**
38 * @var array<int, bool>
39 */
40 private static $processedSavePostIds = [];
41
42 /**
43 * @param ABJ_404_Solution_Functions $functions
44 * @param ABJ_404_Solution_PluginLogic $logic
45 * @param ABJ_404_Solution_Logging $logger
46 * @param ABJ_404_Solution_ContentRepository $contentRepository
47 * @param ABJ_404_Solution_PermalinkCache $permalinkCache
48 * @param ABJ_404_Solution_NGramFilter $ngramFilter
49 */
50 public function __construct($functions, $logic, $logger, $contentRepository, $permalinkCache, $ngramFilter) {
51 $this->f = $functions;
52 $this->logic = $logic;
53 $this->logger = $logger;
54 $this->contentRepository = $contentRepository;
55 $this->permalinkCache = $permalinkCache;
56 $this->ngramFilter = $ngramFilter;
57 }
58
59 /** @return ABJ_404_Solution_PublishedPostsProvider|null */
60 public function getPublishedPostsProvider(): ?ABJ_404_Solution_PublishedPostsProvider {
61 return $this->publishedPostsProvider;
62 }
63
64 /**
65 * @param int $post_id
66 * @param \WP_Post|null $post
67 * @param bool|null $update
68 */
69 function save_postListener($post_id, $post = null, $update = null): void {
70 if (isset(self::$processedSavePostIds[$post_id])) {
71 return;
72 }
73 self::$processedSavePostIds[$post_id] = true;
74
75 if ($post == null) {
76 $post = get_post($post_id);
77 }
78 if ($update == null) {
79 $update = true;
80 }
81
82 $this->savePostHandler($post_id, $post, $update, 'save');
83 }
84
85 /**
86 * @param int $post_id
87 * @param \WP_Post|null $post
88 */
89 function delete_postListener($post_id, $post = null): void {
90 if ($post == null) {
91 $post = get_post($post_id);
92 }
93
94 $this->savePostHandler($post_id, $post, true, 'delete');
95 }
96
97 /**
98 * @param int $post_id
99 * @param \WP_Post|mixed $post
100 * @param bool $update
101 * @param string $saveOrDelete
102 */
103 function savePostHandler($post_id, $post, $update, $saveOrDelete): void {
104 $options = $this->logic->getOptions();
105 if (!is_object($post) || !isset($post->post_type) || !isset($post->post_status) || !isset($post->post_name)) {
106 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
107 ": Invalid post object for ID: " . $post_id . " (skipped).");
108 return;
109 }
110 $postType = $post->post_type;
111
112 $recognizedPostTypesRaw = isset($options['recognized_post_types']) ? $options['recognized_post_types'] : '';
113 $acceptedPostTypes = $this->f->explodeNewline(is_string($recognizedPostTypesRaw) ? $recognizedPostTypesRaw : '');
114
115 $deleteSpellingCache = false;
116 $deleteFromPermalinkCache = false;
117 $invalidateNGramCache = false;
118 $reason = '';
119
120 /** @var array<string, mixed> $cacheRow */
121 $cacheRow = $this->contentRepository->getPermalinkEtcFromCache($post_id) ?: array();
122 $cacheUrlRaw = (array_key_exists('url', $cacheRow)) ? $cacheRow['url'] : null;
123 $oldSlug = (is_string($cacheUrlRaw)) ?
124 rtrim(ltrim($cacheUrlRaw, '/'), '/') : '(not found)';
125 $newSlug = $post->post_name;
126 $matches = array();
127 $metaRowRaw = array_key_exists('meta', $cacheRow) ? $cacheRow['meta'] : '';
128 $metaRow = is_string($metaRowRaw) ? $metaRowRaw : '';
129 preg_match('/s:(\\w+?),/', $metaRow, $matches);
130 $oldStatus = count($matches) > 1 ? $matches[1] : '(not found)';
131 preg_match('/t:(\\w+?),/', $metaRow, $matches);
132 $oldPostType = count($matches) > 1 ? $matches[1] : '(not found)';
133 if ($update && $saveOrDelete == 'save' &&
134 ($oldSlug != $newSlug ||
135 $oldStatus != $post->post_status ||
136 $oldPostType != $post->post_type)
137 ) {
138 $deleteSpellingCache = true; // TODO only delete where the page is referenced.
139 $deleteFromPermalinkCache = true;
140 $invalidateNGramCache = true;
141 $reason = 'change. slug (' . $oldSlug . '(to)' . $newSlug . '), status (' .
142 $oldStatus . '(to)' . $post->post_status . '), type (' . $oldPostType .
143 '(to)' . $post->post_type . ')';
144 }
145
146 if (!in_array($oldPostType, $acceptedPostTypes) &&
147 !in_array($post->post_type, $acceptedPostTypes)) {
148
149 $httpUserAgent = "(none)";
150 if (array_key_exists("HTTP_USER_AGENT", $_SERVER)) {
151 $httpUserAgent = $_SERVER['HTTP_USER_AGENT'];
152 }
153 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
154 ": Ignored savePost change (uninteresting post types). " .
155 "Action: " . $saveOrDelete . ", ID: " . $post_id . ", types: " .
156 $oldPostType . "/" . $post->post_type . ", agent: " .
157 $httpUserAgent);
158 return;
159 }
160
161 $interestingStatuses = array('publish', 'published');
162 if (!in_array($oldStatus, $interestingStatuses) &&
163 !in_array($post->post_status, $interestingStatuses)) {
164
165 $httpUserAgent = "(none)";
166 if (array_key_exists("HTTP_USER_AGENT", $_SERVER)) {
167 $httpUserAgent = $_SERVER['HTTP_USER_AGENT'];
168 }
169 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
170 ": Ignored savePost change (uninteresting post statuses). " .
171 "Action: " . $saveOrDelete . ", ID: " . $post_id . ", statuses: " .
172 $oldStatus . "/" . $post->post_status . ", agent: " .
173 $httpUserAgent);
174 return;
175 }
176
177 if (!$update && $saveOrDelete == 'save') {
178 $deleteSpellingCache = true;
179 $deleteFromPermalinkCache = false;
180 $invalidateNGramCache = false;
181 $reason = 'new page';
182 }
183
184 if ($saveOrDelete == 'delete') {
185 $deleteSpellingCache = true; // TODO only delete where the page is referenced.
186 $deleteFromPermalinkCache = true;
187 $invalidateNGramCache = true;
188 $reason = 'deleted page';
189 }
190
191 if ($deleteFromPermalinkCache) {
192 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
193 ": Delete from permalink cache: " . $post_id . ", action: " .
194 $saveOrDelete . ", reason: " . $reason);
195
196 try {
197 $this->contentRepository->removeFromPermalinkCache($post_id);
198 $this->permalinkCache->updatePermalinkCache(1);
199 } catch (Exception $e) {
200 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
201 ": Exception while updating permalink cache for post ID " . $post_id .
202 ": " . $e->getMessage());
203 }
204 }
205
206 if ($invalidateNGramCache) {
207 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
208 ": Invalidate N-gram cache entry: " . $post_id . ", action: " .
209 $saveOrDelete . ", reason: " . $reason);
210
211 try {
212 $result = $this->ngramFilter->invalidatePage($post_id);
213 if (!$result) {
214 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
215 ": Failed to invalidate N-gram cache for post ID " . $post_id .
216 ". The cache may be out of sync until next daily maintenance.");
217 }
218 } catch (Exception $e) {
219 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
220 ": Exception while invalidating N-gram cache for post ID " . $post_id .
221 ": " . $e->getMessage());
222 }
223 }
224
225 if ($deleteSpellingCache) {
226 // TODO only delete the items from the cache that refer
227 // to the post ID that was deleted?
228 try {
229 $this->contentRepository->deleteSpellingCache();
230 } catch (Exception $e) {
231 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
232 ": Exception while deleting spelling cache: " . $e->getMessage());
233 }
234
235 if ($this->logger->isDebug()) {
236 $httpUserAgent = "(none)";
237 if (array_key_exists("HTTP_USER_AGENT", $_SERVER)) {
238 $httpUserAgent = $_SERVER['HTTP_USER_AGENT'];
239 }
240
241 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
242 ": Spelling cache deleted (post change). Action: " . $saveOrDelete .
243 ", ID: " . $post_id . ", type: " . $postType . ", reason: " .
244 $reason . ", agent: " . $httpUserAgent);
245 }
246 }
247
248 if ($saveOrDelete == 'save' && in_array($post->post_status, array('publish', 'published'))) {
249 try {
250 $this->permalinkCache->updatePermalinkCache(1);
251
252 $stats = $this->ngramFilter->updateNGramsForPages(array($post_id));
253
254 if ($stats['success'] > 0) {
255 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ .
256 ": Incrementally updated N-grams for post ID: " . $post_id .
257 " (processed: {$stats['processed']}, success: {$stats['success']}, failed: {$stats['failed']})");
258 } else if ($stats['failed'] > 0) {
259 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
260 ": Failed to update N-grams for post ID: " . $post_id .
261 " (stats: " . json_encode($stats) . ")");
262 }
263 } catch (Exception $e) {
264 $this->logger->errorMessage(__CLASS__ . "/" . __FUNCTION__ .
265 ": Exception while updating N-grams for post ID " . $post_id .
266 ": " . $e->getMessage());
267 }
268 }
269 }
270
271 /**
272 * @param string $var1
273 * @param mixed $newStructure
274 */
275 function permalinkStructureChanged($var1, $newStructure): void {
276 if ($var1 != 'permalink_structure') {
277 return;
278 }
279
280 $structure = empty($newStructure) ? '(empty)' : $newStructure;
281 $this->contentRepository->deleteSpellingCache();
282 $this->logger->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Spelling cache deleted because the permalink structure changed " . "to " . $structure);
283 }
284
285 function initializePublishedPostsProvider(): void {
286 if ($this->publishedPostsProvider == null) {
287 $this->publishedPostsProvider = abj_service('published_posts_provider');
288 }
289 $this->permalinkCache->updatePermalinkCache(1);
290 }
291
292 }
293