queryAndGetResults( "DELETE FROM {wp_options} WHERE option_name = %s AND option_value = %s", array('query_params' => array($claim['optionName'], $claim['expectedValue'])) ); if (!empty($result['last_error'])) { return false; } $deleted = ABJ_404_Solution_ExactInteger::readOr($result['rows_affected'] ?? null, 0, 0) > 0; if ($deleted) { self::invalidateCaches($claim['optionName']); } return $deleted; } /** * The value stored for the row, read from the database and not from * WordPress's option cache. * * @param string $optionName * @return string|null null when the row does not exist or could not be read. */ public static function read(string $optionName): ?string { $dbCore = self::dbCore(); if ($dbCore === null) { return null; } $row = self::readRow($dbCore, $optionName); return $row['status'] === 'found' ? $row['value'] : null; } /** * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore * @param string $optionName * @return array{status: string, value: string} status is found, absent or failed. */ private static function readRow($dbCore, string $optionName): array { $result = $dbCore->queryAndGetResults( "SELECT option_value FROM {wp_options} WHERE option_name = %s LIMIT 1", array('query_params' => array($optionName)) ); if (!empty($result['last_error'])) { return array('status' => 'failed', 'value' => ''); } $rows = isset($result['rows']) && is_array($result['rows']) ? $result['rows'] : array(); if ($rows === array() || !is_array($rows[0])) { return array('status' => 'absent', 'value' => ''); } // Case-insensitive: MySQL drivers vary the case of returned column names. $row = array_change_key_case($rows[0], CASE_LOWER); $value = $row['option_value'] ?? null; return array('status' => 'found', 'value' => is_scalar($value) ? (string)$value : ''); } /** * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore * @return bool|null true when written, false when the row changed underneath, null on a database error. */ private static function replaceIfUnchanged($dbCore, string $optionName, string $current, string $next): ?bool { $result = $dbCore->queryAndGetResults( "UPDATE {wp_options} SET option_value = %s WHERE option_name = %s AND option_value = %s", array('query_params' => array($next, $optionName, $current)) ); return self::affectedOne($result); } /** * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore * @return bool|null true when created, false when the row appeared underneath, null on a database error. */ private static function insertIfAbsent($dbCore, string $optionName, string $next): ?bool { $result = $dbCore->queryAndGetResults( "INSERT IGNORE INTO {wp_options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')", array('query_params' => array($optionName, $next)) ); return self::affectedOne($result); } /** * @param array $result * @return bool|null */ private static function affectedOne(array $result): ?bool { if (!empty($result['last_error'])) { return null; } return ABJ_404_Solution_ExactInteger::readOr($result['rows_affected'] ?? null, 0, 0) === 1; } /** * A direct SQL write leaves WordPress's option caches describing the row as * it was, so the next get_option() would serve a value that no longer * exists. Same three keys CanonicalHookCensusStore::refreshReads() drops, * plus alloptions for a row that was autoloaded. */ private static function invalidateCaches(string $optionName): void { if (!function_exists('wp_cache_delete')) { return; } wp_cache_delete($optionName, 'options'); wp_cache_delete('notoptions', 'options'); wp_cache_delete('alloptions', 'options'); } /** * One durable WARN. Never throws: this runs in shutdown and fatal handlers. */ private static function warn(string $why, string $optionName): void { $message = 'Could not update the options row "' . $optionName . '": ' . $why . '.'; try { $logger = function_exists('abj_service') ? abj_service('logging') : null; if (is_object($logger) && method_exists($logger, 'warn')) { $logger->warn($message); return; } } catch (Throwable $e) { $message .= ' The logger failed too (' . get_class($e) . ' ' . $e->getCode() . '): ' . $e->getMessage(); } if (function_exists('abj404_logPhpFallback')) { abj404_logPhpFallback('option-row-compare-and-swap', $message); } } /** * Read straight from the container, as ABJ_404_Solution_SameSiteRequestRegistry does. * * @return ABJ_404_Solution_DatabaseQueryInterface|null */ private static function dbCore() { if (!class_exists('ABJ_404_Solution_ServiceContainer')) { return null; } $dbCore = ABJ_404_Solution_ServiceContainer::safeGet('db_core'); return ($dbCore instanceof ABJ_404_Solution_DatabaseQueryInterface) ? $dbCore : null; } }