PluginProbe
404 Solution / 4.1.13
404 Solution v4.1.13
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 / SynchronizationUtils.php

SynchronizationUtils.php in 404 Solution 4.1.13, at includes/SynchronizationUtils.php

408 lines 12.9 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 class ABJ_404_Solution_SynchronizationUtils {
9
10 /** A prefix for keys used for synchronization methods.
11 * @var string */
12 const SYNC_KEY_PREFIX = 'SYNC_';
13
14 /** @var bool|null */
15 static $usingFileMode = null;
16
17 /** @var self|null */
18 private static $instance = null;
19
20 /** @return self */
21 public static function getInstance() {
22 if (self::$instance == null) {
23 self::$instance = new ABJ_404_Solution_SynchronizationUtils();
24 }
25
26 return self::$instance;
27 }
28
29 /** @return string */
30 private function getFileModePath() {
31 return abj404_getUploadsDir() . 'sync_mode_file.txt';
32 }
33
34 /** @return string */
35 private function getOptionsModePath() {
36 return abj404_getUploadsDir() . 'sync_mode_options.txt';
37 }
38
39 /** @return bool */
40 private function isFileMode() {
41 if (self::$usingFileMode == null) {
42 $fileModePath = $this->getFileModePath();
43 $optionsModePath = $this->getOptionsModePath();
44 if (file_exists($fileModePath) && file_exists($optionsModePath)) {
45 $fileUtils = abj_service('functions');
46 $fileUtils->safeUnlink($fileModePath);
47 $fileUtils->safeUnlink($optionsModePath);
48 }
49
50 if (file_exists($fileModePath)) {
51 $usingFileMode = true;
52
53 } else if (file_exists($optionsModePath)) {
54 $usingFileMode = false;
55
56 } else {
57 // initialize
58 $pass = true;
59 $keyForTesting = ABJ404_PP . "_" . self::SYNC_KEY_PREFIX . 'testing';
60 $uniqueID = $this->createUniqueID('testing');
61
62 // test saving.
63 update_option($keyForTesting, $uniqueID);
64 $result = get_option($keyForTesting);
65 if ($result != $uniqueID) {
66 $pass = false;
67 }
68
69 // test deleting.
70 delete_option($keyForTesting);
71 $result = get_option($keyForTesting);
72 if ($result != null && $result != '') {
73 $pass = false;
74 }
75
76 $f = abj_service('functions');
77 $f->createDirectoryWithErrorMessages(dirname($optionsModePath));
78 if ($pass) {
79 $usingFileMode = false;
80 touch($optionsModePath);
81 } else {
82 $usingFileMode = true;
83 touch($fileModePath);
84 }
85 }
86 self::$usingFileMode = $usingFileMode;
87 }
88
89 return self::$usingFileMode;
90 }
91
92 /** @return void */
93 function switchToFileSyncMode() {
94 $f = abj_service('functions');
95 $fileUtils = abj_service('functions');
96
97 self::$usingFileMode = true;
98 $optionsModePath = $this->getOptionsModePath();
99 $fileUtils->safeUnlink($optionsModePath);
100
101 $fileModePath = $this->getFileModePath();
102 $f->createDirectoryWithErrorMessages(dirname($fileModePath));
103 touch($fileModePath);
104 }
105
106 /**
107 * @param string $keyFromUser
108 * @return string
109 */
110 private function createInternalKey($keyFromUser) {
111 return ABJ404_PP . "_" . self::SYNC_KEY_PREFIX . $keyFromUser;
112 }
113
114 /**
115 * @param string $keyFromUser
116 * @return string
117 */
118 private function createUniqueID($keyFromUser) {
119 return microtime(true) . "_" . $keyFromUser . '_' . $this->uniqidReal() . uniqid('', true);
120 }
121
122 /** Returns an empty string if the lock is not acquired.
123 * @param string $synchronizedKeyFromUser
124 * @return string the unique ID that was used. This is needed to release the lock. Or an empty string if
125 * the lock wasn't acquired.
126 */
127 function synchronizerAcquireLockTry($synchronizedKeyFromUser) {
128 $uniqueID = $this->createUniqueID($synchronizedKeyFromUser);
129 $internalSynchronizedKey = $this->createInternalKey($synchronizedKeyFromUser);
130
131 // don't let anyone hold the lock for too long.
132 $this->fixAnUnforeseenIssue($synchronizedKeyFromUser);
133
134 // acquire the lock.
135 $currentOwner = $this->readOwner($internalSynchronizedKey);
136 // only write the value if it's empty.
137 if (empty($currentOwner)) {
138 $this->writeOwner($internalSynchronizedKey, $uniqueID);
139 }
140 // give a different thread that ran at the same time a chance to overwrite our value.
141 time_nanosleep(0, 10000000 * 30); // 10000000 is 1/100 of a second.
142 // check and see if we're the owner yet.
143 $currentOwner = $this->readOwner($internalSynchronizedKey);
144
145 if ($currentOwner == $uniqueID) {
146 return $uniqueID;
147 }
148
149 return '';
150 }
151
152 /** Remove the lock if it's been in place for too long.
153 * @param string $synchronizedKeyFromUser
154 * @return void
155 */
156 function fixAnUnforeseenIssue($synchronizedKeyFromUser) {
157 $internalSynchronizedKey = $this->createInternalKey($synchronizedKeyFromUser);
158
159 $uniqueID = $this->readOwner($internalSynchronizedKey);
160
161 if (empty($uniqueID)) {
162 return;
163 }
164
165 $uniqueIDInfo = explode("_", $uniqueID);
166
167 $createTime = $uniqueIDInfo[0];
168
169 $timePassed = microtime(true) - (float)$createTime;
170
171 $maxExecutionTime = ini_get('max_execution_time');
172 if (empty($maxExecutionTime) || $maxExecutionTime < 1) {
173 $maxExecutionTime = 60;
174 } else {
175 $maxExecutionTime *= 2;
176 }
177
178 // it should have been released by now.
179 if ($timePassed > $maxExecutionTime) {
180 $this->deleteOwner($uniqueID, $internalSynchronizedKey);
181 $valueAfterDelete = $this->readOwner($internalSynchronizedKey);
182
183 // if options mode failed for some reason then switch to file sync mode.
184 if ($valueAfterDelete != null && $valueAfterDelete != '' &&
185 !$this->isFileMode()) {
186 $this->switchToFileSyncMode();
187 return;
188 }
189
190 $uniqueIDForDebugging = $this->createUniqueID('DEBUG_KEY');
191 $logger = abj_service('logging');
192 $logger->errorMessage("Forcibly removed synchronization after " .
193 $timePassed . " seconds for the " . "key " . $internalSynchronizedKey .
194 " with value: " . $uniqueID . ', value after delete: ' . $valueAfterDelete .
195 ", microtime: " . microtime(true) . ", unique ID for debugging: " .
196 $uniqueIDForDebugging . ", File sync mode: " . json_encode($this->isFileMode()));
197 }
198 }
199
200 /** Waits until the lock can be acquired and then returns the unique ID.
201 * @param string $synchronizedKeyFromUser
202 * @return string the unique ID that was used. This is needed to release the lock.
203 */
204 function synchronizerAcquireLockWithWait($synchronizedKeyFromUser) {
205 $uniqueID = $this->createUniqueID($synchronizedKeyFromUser);
206 $internalSynchronizedKey = $this->createInternalKey($synchronizedKeyFromUser);
207
208 $this->fixAnUnforeseenIssue($synchronizedKeyFromUser);
209 $iterations = 0;
210
211 // acquire the lock.
212 $currentOwner = $this->readOwner($internalSynchronizedKey);
213 while ($currentOwner != $uniqueID) {
214 // only write the value if it's empty.
215 if (empty($currentOwner)) {
216 $this->writeOwner($internalSynchronizedKey, $uniqueID);
217 }
218 // give a different thread that ran at the same time a chance to overwrite our value.
219 time_nanosleep(0, 500000000); // 10000000 is 1/100 of a second. 500000000 is 1/2 of a second.
220 // check and see if we're the owner yet.
221 $currentOwner = $this->readOwner($internalSynchronizedKey);
222
223 $iterations++;
224 if ($iterations % 500 == 0) {
225 $this->fixAnUnforeseenIssue($synchronizedKeyFromUser);
226 }
227 }
228
229 return $uniqueID;
230 }
231
232 /** Release the lock for a synchronized block. Should be done in a finally block.
233 * @param string $uniqueID
234 * @param string $synchronizedKeyFromUser
235 * @return void
236 * @throws Exception
237 */
238 function synchronizerReleaseLock($uniqueID, $synchronizedKeyFromUser) {
239 $internalSynchronizedKey = $this->createInternalKey($synchronizedKeyFromUser);
240
241 $currentLockHolder = $this->readOwner($internalSynchronizedKey);
242
243 if ($uniqueID == $currentLockHolder) {
244 $this->deleteOwner($uniqueID, $internalSynchronizedKey);
245
246 } else {
247 // Fail silently instead of throwing fatal exception.
248 $logger = abj_service('logging');
249 $logger->debugMessage("Synchronization lock release mismatch. " .
250 "Synchronized key: $synchronizedKeyFromUser, current holder: $currentLockHolder, " .
251 "attempted release by: $uniqueID");
252 }
253 }
254
255 /**
256 * @param string $key
257 * @return string
258 */
259 function readOwner($key) {
260 $owner = '';
261 if ($this->isFileMode()) {
262 $fileSync = ABJ_404_Solution_FileSync::getInstance();
263 $owner = $fileSync->getOwnerFromFile($key);
264
265 } else {
266 // MULTISITE: Use network-aware option for N-gram locks
267 $ownerRaw = $this->getNetworkAwareOption($key);
268 $owner = is_string($ownerRaw) ? $ownerRaw : '';
269 }
270
271 return $owner;
272 }
273 /**
274 * @param string $key
275 * @param string $owner
276 * @return void
277 */
278 function writeOwner($key, $owner) {
279 if ($this->isFileMode()) {
280 $fileSync = ABJ_404_Solution_FileSync::getInstance();
281 $fileSync->writeOwnerToFile($key, $owner);
282 } else {
283 // MULTISITE: Use network-aware option for N-gram locks
284 $this->updateNetworkAwareOption($key, $owner);
285 }
286 }
287 /**
288 * @param string $owner
289 * @param string $key
290 * @return void
291 */
292 function deleteOwner($owner, $key) {
293 if ($this->isFileMode()) {
294 $fileSync = ABJ_404_Solution_FileSync::getInstance();
295 $fileSync->releaseLock($owner, $key);
296 } else {
297 // MULTISITE: Use network-aware option for N-gram locks
298 $this->deleteNetworkAwareOption($key);
299 }
300 }
301
302 /**
303 * Check if the plugin is network-activated in a multisite environment.
304 *
305 * @return bool True if network-activated, false otherwise
306 */
307 private function isNetworkActivated() {
308 if (!is_multisite()) {
309 return false;
310 }
311
312 if (!function_exists('is_plugin_active_for_network')) {
313 require_once ABSPATH . '/wp-admin/includes/plugin.php';
314 }
315
316 return is_plugin_active_for_network(plugin_basename(ABJ404_FILE));
317 }
318
319 /**
320 * Determine if this lock key should use network-wide storage.
321 *
322 * N-gram rebuild locks (ngram_rebuild, ngram_schedule) must be network-wide
323 * to coordinate across all sites. Other locks remain site-specific.
324 *
325 * @param string $key The lock key
326 * @return bool True if should use network-wide storage
327 */
328 private function shouldUseNetworkStorage($key) {
329 // Extract the user-provided key from the internal key format
330 $userKey = str_replace(ABJ404_PP . "_" . self::SYNC_KEY_PREFIX, '', $key);
331
332 // N-gram locks must be network-wide when network-activated
333 $networkWideLocks = ['ngram_rebuild', 'ngram_schedule'];
334
335 return $this->isNetworkActivated() && in_array($userKey, $networkWideLocks);
336 }
337
338 /**
339 * Get an option value, using network-wide storage for N-gram locks.
340 *
341 * @param string $key The option key
342 * @param mixed $default Default value if option doesn't exist
343 * @return mixed The option value
344 */
345 private function getNetworkAwareOption($key, $default = false) {
346 if ($this->shouldUseNetworkStorage($key)) {
347 return get_site_option($key, $default);
348 }
349 return get_option($key, $default);
350 }
351
352 /**
353 * Update an option value, using network-wide storage for N-gram locks.
354 *
355 * @param string $key The option key
356 * @param mixed $value The value to store
357 * @return bool True if updated successfully
358 */
359 private function updateNetworkAwareOption($key, $value) {
360 if ($this->shouldUseNetworkStorage($key)) {
361 return update_site_option($key, $value);
362 }
363 return update_option($key, $value);
364 }
365
366 /**
367 * Delete an option, using network-wide storage for N-gram locks.
368 *
369 * @param string $key The option key
370 * @return bool True if deleted successfully
371 */
372 private function deleteNetworkAwareOption($key) {
373 if ($this->shouldUseNetworkStorage($key)) {
374 return delete_site_option($key);
375 }
376 return delete_option($key);
377 }
378
379 /**
380 * @return string a random string of characters.
381 * @throws Exception
382 */
383 function uniqidReal() {
384 $bytes = null;
385 if (function_exists("random_bytes")) {
386 try {
387 $bytes = random_bytes(max(1, (int)ceil(13 / 2)));
388 } catch (Exception $e) {
389 $bytes = null; // don't care.
390 }
391 }
392
393 if ($bytes == null && function_exists("openssl_random_pseudo_bytes")) {
394 try {
395 $bytes = openssl_random_pseudo_bytes((int)ceil(13 / 2));
396 } catch (Exception $e) {
397 $bytes = null;
398 }
399 }
400
401 if ($bytes != null) {
402 return bin2hex($bytes);
403 }
404 return uniqid("", true);
405 }
406
407 }
408