fix(audit L6, L5): DesignLock atomar (INSERT IGNORE, Token, Übernahme per UPDATE, Release nur mit eigenem Token); Farben und Theme-Optionen teilen eine Sperre
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
522fc8ff4d
commit
b59a0c045f
@@ -9,9 +9,13 @@ use InvalidArgumentException;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* One confirmed change per design area at a time (final review I-5): an option lit_mcp_lock_<area> (never
|
||||
* autoloaded) holds the time the lock was taken. add_option() is atomic – it fails while the option exists. A lock
|
||||
* older than STALE_AFTER seconds (a request that died) is removed and taken once more.
|
||||
* One confirmed change per lock at a time (final review I-5). Colors and theme options share ONE lock (audit L5):
|
||||
* both rewrite the whole et_divi array through et_update_option(). The lock is a row lit_mcp_lock_<name> (never
|
||||
* autoloaded) in the options table holding "<time>:<token>" – taken with an atomic INSERT IGNORE like
|
||||
* DiviCheckRoute (add_option() reads first and writes after, so two requests could both win; audit L6). A lock
|
||||
* older than STALE_AFTER seconds (a request that died) is taken over with a conditional UPDATE on the value just
|
||||
* read, so only one taker wins; release() deletes the row only while it still holds this instance's token, so a
|
||||
* holder that lost a stale lock cannot free the new holder's.
|
||||
*/
|
||||
final class DesignLock
|
||||
{
|
||||
@@ -19,6 +23,12 @@ final class DesignLock
|
||||
|
||||
private const PREFIX = 'lit_mcp_lock_';
|
||||
|
||||
/** Areas that write the same Divi option share one lock. */
|
||||
private const SHARED = ['colors' => 'divi_options', 'theme_options' => 'divi_options'];
|
||||
|
||||
/** @var array<string, string> Lock option => token this instance holds */
|
||||
private array $tokens = [];
|
||||
|
||||
/** @throws InvalidArgumentException For an area outside SnapshotStore::AREAS. */
|
||||
public static function option(string $area): string
|
||||
{
|
||||
@@ -26,27 +36,63 @@ final class DesignLock
|
||||
throw new InvalidArgumentException(sprintf('Unknown design area "%s".', $area));
|
||||
}
|
||||
|
||||
return self::PREFIX . $area;
|
||||
return self::PREFIX . (self::SHARED[$area] ?? $area);
|
||||
}
|
||||
|
||||
public function acquire(string $area): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$option = self::option($area);
|
||||
if (add_option($option, time(), '', false)) {
|
||||
return true;
|
||||
$token = time() . ':' . bin2hex(random_bytes(4));
|
||||
$taken = $this->insert($wpdb, $option, $token);
|
||||
if (! $taken) {
|
||||
$since = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
|
||||
$option
|
||||
));
|
||||
if ($since === null) {
|
||||
// Released between our failed insert and this read; try once more.
|
||||
$taken = $this->insert($wpdb, $option, $token);
|
||||
} elseif (time() - (int) $since > self::STALE_AFTER) {
|
||||
$taken = (int) $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
|
||||
$token,
|
||||
$option,
|
||||
$since
|
||||
)) === 1;
|
||||
}
|
||||
}
|
||||
if ($taken) {
|
||||
$this->tokens[$option] = $token;
|
||||
}
|
||||
|
||||
$since = get_option($option, 0);
|
||||
if (is_numeric($since) && time() - (int) $since <= self::STALE_AFTER) {
|
||||
return false;
|
||||
}
|
||||
delete_option($option);
|
||||
|
||||
return add_option($option, time(), '', false);
|
||||
return $taken;
|
||||
}
|
||||
|
||||
public function release(string $area): void
|
||||
{
|
||||
delete_option(self::option($area));
|
||||
global $wpdb;
|
||||
|
||||
$option = self::option($area);
|
||||
$token = $this->tokens[$option] ?? null;
|
||||
unset($this->tokens[$option]);
|
||||
if ($token !== null) {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
|
||||
$option,
|
||||
$token
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/** INSERT IGNORE: 1 row affected = the lock did not exist and is now ours. */
|
||||
private function insert(\wpdb $wpdb, string $option, string $token): bool
|
||||
{
|
||||
return (int) $wpdb->query($wpdb->prepare(
|
||||
"INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
|
||||
$option,
|
||||
$token
|
||||
)) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user