array(), 'queue' => array(), 'editors' => array(), 'persistent_sessions' => array()); foreach (array('xxx', 'queue') as $table) self::$tables[$table] = array_map(array(self::class, 'story'), self::$tables[$table]); self::$lastInsertId = 0; $stories = '(xxx|queue)'; FakeDb::on("/^SELECT \\* FROM $stories\\b/", array(self::class, 'selectStories')); FakeDb::on("/^SELECT COUNT\\(\\*\\) FROM $stories\\b/", function ($sql, $params) { return array(array('COUNT(*)' => (string)count(self::filterStories($sql, $params)))); }); FakeDb::on('/^SELECT MAX\(`date`\) FROM xxx/', function () { $dates = array_column(self::$tables['xxx'], 'date'); return array(array('MAX(`date`)' => $dates ? max($dates) : NULL)); }); FakeDb::on("/^SELECT img, mime(, hidden)? FROM $stories\\b/i", function ($sql, $params) { $rows = self::filterStories($sql, $params); $columns = str_starts_with($sql, 'SELECT img, mime, hidden') ? array('img', 'mime', 'hidden') : array('img', 'mime'); return array_map(function ($row) use ($columns) { return array_intersect_key($row, array_flip($columns)); }, array_slice($rows, 0, 1)); }); FakeDb::on("/^INSERT INTO $stories SET /", array(self::class, 'insertStory')); FakeDb::on("/^UPDATE $stories SET /", array(self::class, 'updateStory')); FakeDb::on("/^DELETE FROM $stories WHERE `id` = \\? LIMIT 1/", function ($sql, $params) { $table = self::tableOf($sql); self::$tables[$table] = array_values(array_filter(self::$tables[$table], function ($row) use ($params) { return (int)$row['id'] !== (int)$params[0]; })); return TRUE; }); FakeDb::on('/^SELECT LAST_INSERT_ID\(\)/', function () { return array(array('LAST_INSERT_ID()' => (string)self::$lastInsertId)); }); FakeDb::on('/^SELECT (\*|username|password) FROM editors WHERE username = \?/', function ($sql, $params) { return array_slice(array_values(array_filter(self::$tables['editors'], function ($row) use ($params) { return $row['username'] === $params[0]; })), 0, 1); }); FakeDb::on('/^UPDATE editors SET password = \? WHERE username = \?/', function ($sql, $params) { foreach (self::$tables['editors'] as &$row) { if ($row['username'] === $params[1]) $row['password'] = self::cut($params[0]); } return TRUE; }); FakeDb::on('/^INSERT INTO editors SET `username` = \?, `password` = \?, `email` = \?/', function ($sql, $params) { self::$tables['editors'][] = array('username' => $params[0], 'password' => self::cut($params[1]), 'email' => $params[2]); return TRUE; }); FakeDb::on('/^DELETE FROM editors WHERE username = \?/', function ($sql, $params) { self::$tables['editors'] = array_values(array_filter(self::$tables['editors'], function ($row) use ($params) { return $row['username'] !== $params[0]; })); return TRUE; }); FakeDb::on('/^SELECT \* FROM persistent_sessions WHERE username = \? AND session = \?/', function ($sql, $params) { return array_slice(array_values(array_filter(self::$tables['persistent_sessions'], function ($row) use ($params) { return $row['username'] === $params[0] && $row['session'] === $params[1]; })), 0, 1); }); FakeDb::on('/^SELECT p\.username, p\.session FROM persistent_sessions p JOIN editors e ON e\.username = p\.username\s+WHERE p\.username = \? AND p\.session = \? AND p\.created > DATE_SUB\(now\(\), INTERVAL 1 YEAR\)/', function ($sql, $params) { $editors = array_column(self::$tables['editors'], 'username'); $limit = gmdate('Y-m-d H:i:s', strtotime('-1 year')); return array_slice(array_values(array_map(function ($row) { return array('username' => $row['username'], 'session' => $row['session']); }, array_filter(self::$tables['persistent_sessions'], function ($row) use ($params, $editors, $limit) { return $row['username'] === $params[0] && $row['session'] === $params[1] && in_array($row['username'], $editors, TRUE) && ($row['created'] ?? $limit) > $limit; }))), 0, 1); }); FakeDb::on('/^DELETE FROM persistent_sessions WHERE username = \?$/', function ($sql, $params) { self::$tables['persistent_sessions'] = array_values(array_filter(self::$tables['persistent_sessions'], function ($row) use ($params) { return $row['username'] !== $params[0]; })); return TRUE; }); FakeDb::on('/^INSERT INTO persistent_sessions \(username, session\) VALUES \(\?, \?\)/', function ($sql, $params) { self::$tables['persistent_sessions'][] = array('username' => $params[0], 'session' => $params[1], 'created' => gmdate('Y-m-d H:i:s')); return TRUE; }); FakeDb::on('/^DELETE FROM persistent_sessions WHERE username = \? AND session = \?/', function ($sql, $params) { self::$tables['persistent_sessions'] = array_values(array_filter(self::$tables['persistent_sessions'], function ($row) use ($params) { return !($row['username'] === $params[0] && $row['session'] === $params[1]); })); return TRUE; }); FakeDb::on('/^DELETE FROM persistent_sessions\s+WHERE created < DATE_SUB/', function () { $limit = gmdate('Y-m-d H:i:s', strtotime('-1 year')); self::$tables['persistent_sessions'] = array_values(array_filter(self::$tables['persistent_sessions'], function ($row) use ($limit) { return strlen($row['session']) === 64 && ($row['created'] ?? $limit) >= $limit; })); return TRUE; }); } // A story row with every column, from the columns given public static function story(array $row): array { static $next = 1; $row += array('id' => $next, 'title' => 'Title', 'author' => 'Author', 'email' => '', 'date' => '2020-01-01 10:00:00', 'story' => '

Story.

', 'hidden' => '0', 'img' => NULL, 'mime' => NULL, 'comments' => '', 'editor' => 'original author'); $next = max($next, (int)$row['id']) + 1; $row['id'] = (string)$row['id']; return $row; } public static function tableOf(string $sql): string { preg_match('/\b(FROM|INTO|UPDATE)\s+`?(xxx|queue)\b/', $sql, $match); return $match[2]; } // The rows of the story table in $sql that its WHERE clause selects public static function filterStories(string $sql, array $params): array { $rows = self::$tables[self::tableOf($sql)]; if (stripos($sql, 'hidden IS NOT TRUE') !== FALSE) $rows = array_filter($rows, function ($row) { return !$row['hidden']; }); if (preg_match('/\bid = (\d+)/i', $sql, $match)) $rows = array_filter($rows, function ($row) use ($match) { return (int)$row['id'] === (int)$match[1]; }); else if (preg_match('/\bid = \?/i', $sql)) $rows = array_filter($rows, function ($row) use ($params) { return (int)$row['id'] === (int)$params[0]; }); if (preg_match('/title LIKE \?/', $sql)) { $term = strtolower(trim($params[0], '%')); $rows = array_filter($rows, function ($row) use ($term) { foreach (array('title', 'author', 'date', 'story') as $column) { if (str_contains(strtolower((string)$row[$column]), $term)) return TRUE; } return FALSE; }); } return array_values($rows); } public static function selectStories(string $sql, array $params): array { $rows = self::filterStories($sql, $params); if (preg_match('/ORDER BY (\w+) (asc|desc)/i', $sql, $match)) { $column = $match[1]; usort($rows, function ($a, $b) use ($column) { return strcmp((string)$a[$column], (string)$b[$column]); }); if (strtolower($match[2]) === 'desc') $rows = array_reverse($rows); } if (preg_match('/LIMIT (\d+)\s*,\s*(\d+)/', $sql, $match)) $rows = array_slice($rows, (int)$match[1], (int)$match[2]); return $rows; } // The column => value pairs of "SET `a` = ?, `b` = ?" private static function assignments(string $sql, array $params): array { preg_match_all('/`(\w+)` = \?/', explode(' WHERE ', $sql)[0], $matches); return array_combine($matches[1], array_slice($params, 0, count($matches[1]))); } public static function insertStory(string $sql, array $params) { $ids = array_merge(array_column(self::$tables['xxx'], 'id'), array_column(self::$tables['queue'], 'id'), array(0)); $row = self::story(array('id' => max(array_map('intval', $ids)) + 1) + self::assignments($sql, $params)); self::$tables[self::tableOf($sql)][] = $row; self::$lastInsertId = (int)$row['id']; return TRUE; } public static function updateStory(string $sql, array $params) { $id = (int)end($params); $table = self::tableOf($sql); foreach (self::$tables[$table] as &$row) { if ((int)$row['id'] === $id) $row = array_merge($row, self::assignments($sql, $params)); } return TRUE; } private static function cut(string $value): string { return self::$passwordLength === NULL ? $value : substr($value, 0, self::$passwordLength); } // The rows of a story table, found by id public static function find(string $table, $id): ?array { foreach (self::$tables[$table] as $row) { if ((int)$row['id'] === (int)$id) return $row; } return NULL; } }