@@ -265,10 +211,10 @@ public function get_bool(string $name, ?bool $default = null): ?bool
public function get_array(string $name, ?array $default = null): ?array
{
$val = $this->get($name);
- if(is_null($val)) {
+ if (is_null($val)) {
return $default;
}
- if(empty($val)) {
+ if (empty($val)) {
return [];
}
return explode(",", $val);
@@ -286,8 +232,6 @@ private function get(string $name, mixed $default = null): mixed
/**
- * Class DatabaseConfig
- *
* Loads the config list from a table in a given database, the table should
* be called config and have the schema:
*
@@ -298,7 +242,7 @@ private function get(string $name, mixed $default = null): mixed
* );
* \endcode
*/
-class DatabaseConfig extends BaseConfig
+class DatabaseConfig extends Config
{
private Database $database;
private string $table_name;
@@ -309,8 +253,8 @@ class DatabaseConfig extends BaseConfig
public function __construct(
Database $database,
string $table_name = "config",
- string $sub_column = null,
- string $sub_value = null
+ ?string $sub_column = null,
+ ?string $sub_value = null
) {
global $cache;
@@ -335,41 +279,41 @@ private function get_values(): mixed
}
foreach ($this->database->get_all($query, $args) as $row) {
- $values[$row["name"]] = $row["value"];
+ // versions prior to 2.12 would store null
+ // instead of deleting the row
+ if (!is_null($row["value"])) {
+ $values[$row["name"]] = $row["value"];
+ }
}
return $values;
}
- public function save(string $name = null): void
+ protected function save(string $name): void
{
global $cache;
- if (is_null($name)) {
- reset($this->values); // rewind the array to the first element
- foreach ($this->values as $name => $value) {
- $this->save($name);
- }
- } else {
- $query = "DELETE FROM {$this->table_name} WHERE name = :name";
- $args = ["name" => $name];
- $cols = ["name","value"];
- $params = [":name",":value"];
- if (!empty($this->sub_column) && !empty($this->sub_value)) {
- $query .= " AND $this->sub_column = :sub_value";
- $args["sub_value"] = $this->sub_value;
- $cols[] = $this->sub_column;
- $params[] = ":sub_value";
- }
+ $query = "DELETE FROM {$this->table_name} WHERE name = :name";
+ $args = ["name" => $name];
+ $cols = ["name","value"];
+ $params = [":name",":value"];
+ if (!empty($this->sub_column) && !empty($this->sub_value)) {
+ $query .= " AND $this->sub_column = :sub_value";
+ $args["sub_value"] = $this->sub_value;
+ $cols[] = $this->sub_column;
+ $params[] = ":sub_value";
+ }
- $this->database->execute($query, $args);
+ $this->database->execute($query, $args);
+ if (isset($this->values[$name])) {
$args["value"] = $this->values[$name];
$this->database->execute(
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
$args
);
}
+
// rather than deleting and having some other request(s) do a thundering
// herd of race-conditioned updates, just save the updated version once here
$cache->set($this->cache_name, $this->values);
diff --git a/core/database.php b/core/database.php
index acfdd904f..b4ab8bbb6 100644
--- a/core/database.php
+++ b/core/database.php
@@ -8,6 +8,7 @@
use FFSPHP\PDOStatement;
require_once __DIR__ . '/exceptions.php';
+require_once __DIR__ . '/stdlib_ex.php';
enum DatabaseDriverID: string
{
@@ -68,11 +69,13 @@ public function __construct(string $dsn)
private function get_db(): PDO
{
- if(is_null($this->db)) {
+ if (is_null($this->db)) {
$this->db = new PDO($this->dsn);
$this->connect_engine();
+ assert(!is_null($this->db));
$this->get_engine()->init($this->db);
$this->begin_transaction();
+ assert(!is_null($this->db));
}
return $this->db;
}
@@ -155,6 +158,7 @@ private function get_engine(): DBEngine
{
if (is_null($this->engine)) {
$this->connect_engine();
+ assert(!is_null($this->engine));
}
return $this->engine;
}
@@ -182,7 +186,7 @@ private function count_time(string $method, float $start, string $query, ?array
global $_tracer, $tracer_enabled;
$dur = ftime() - $start;
// trim whitespace
- $query = preg_replace('/[\n\t ]+/m', ' ', $query);
+ $query = preg_replace_ex('/[\n\t ]+/m', ' ', $query);
$query = trim($query);
if ($tracer_enabled) {
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query" => $query, "args" => $args, "method" => $method]);
@@ -452,6 +456,6 @@ public function seeded_random(int $seed, string $id_column): string
}
// As fallback, use MD5 as a DRBG.
- return "MD5(CONCAT($seed, CONCAT('+', $id_column)))";
+ return "MD5($seed || '+' || $id_column)";
}
}
diff --git a/core/dbengine.php b/core/dbengine.php
index 6376bd5bf..c17176f3c 100644
--- a/core/dbengine.php
+++ b/core/dbengine.php
@@ -44,6 +44,7 @@ class MySQL extends DBEngine
public function init(PDO $db): void
{
$db->exec("SET NAMES utf8;");
+ $db->exec("SET SESSION sql_mode='ANSI,TRADITIONAL';");
}
public function scoreql_to_sql(string $data): string
@@ -146,18 +147,10 @@ function _log(float $a, ?float $b = null): float
return log($b, $a);
}
}
-function _isnull(mixed $a): bool
-{
- return is_null($a);
-}
function _md5(string $a): string
{
return md5($a);
}
-function _concat(string $a, string $b): string
-{
- return $a . $b;
-}
function _lower(string $a): string
{
return strtolower($a);
@@ -183,9 +176,7 @@ public function init(PDO $db): void
$db->sqliteCreateFunction('now', 'Shimmie2\_now', 0);
$db->sqliteCreateFunction('floor', 'Shimmie2\_floor', 1);
$db->sqliteCreateFunction('log', 'Shimmie2\_log');
- $db->sqliteCreateFunction('isnull', 'Shimmie2\_isnull', 1);
$db->sqliteCreateFunction('md5', 'Shimmie2\_md5', 1);
- $db->sqliteCreateFunction('concat', 'Shimmie2\_concat', 2);
$db->sqliteCreateFunction('lower', 'Shimmie2\_lower', 1);
$db->sqliteCreateFunction('rand', 'Shimmie2\_rand', 0);
$db->sqliteCreateFunction('ln', 'Shimmie2\_ln', 1);
diff --git a/core/event.php b/core/event.php
index 5b545eed0..b8fe1ba6f 100644
--- a/core/event.php
+++ b/core/event.php
@@ -63,7 +63,6 @@ class PageRequestEvent extends Event
*/
private array $named_args = [];
public int $page_num;
- private bool $is_authed;
/**
* @param string $method The HTTP method used to make the request
@@ -87,10 +86,6 @@ public function __construct(string $method, string $path, array $get, array $pos
$this->path = $path;
$this->GET = $get;
$this->POST = $post;
- $this->is_authed = (
- defined("UNITTEST")
- || (isset($_POST["auth_token"]) && $_POST["auth_token"] == $user->get_auth_token())
- );
// break the path into parts
$this->args = explode('/', $path);
@@ -98,8 +93,8 @@ public function __construct(string $method, string $path, array $get, array $pos
public function get_GET(string $key): ?string
{
- if(array_key_exists($key, $this->GET)) {
- if(is_array($this->GET[$key])) {
+ if (array_key_exists($key, $this->GET)) {
+ if (is_array($this->GET[$key])) {
throw new UserError("GET parameter {$key} is an array, expected single value");
}
return $this->GET[$key];
@@ -111,7 +106,7 @@ public function get_GET(string $key): ?string
public function req_GET(string $key): string
{
$value = $this->get_GET($key);
- if($value === null) {
+ if ($value === null) {
throw new UserError("Missing GET parameter {$key}");
}
return $value;
@@ -119,8 +114,8 @@ public function req_GET(string $key): string
public function get_POST(string $key): ?string
{
- if(array_key_exists($key, $this->POST)) {
- if(is_array($this->POST[$key])) {
+ if (array_key_exists($key, $this->POST)) {
+ if (is_array($this->POST[$key])) {
throw new UserError("POST parameter {$key} is an array, expected single value");
}
return $this->POST[$key];
@@ -132,7 +127,7 @@ public function get_POST(string $key): ?string
public function req_POST(string $key): string
{
$value = $this->get_POST($key);
- if($value === null) {
+ if ($value === null) {
throw new UserError("Missing POST parameter {$key}");
}
return $value;
@@ -143,8 +138,8 @@ public function req_POST(string $key): string
*/
public function get_POST_array(string $key): ?array
{
- if(array_key_exists($key, $this->POST)) {
- if(!is_array($this->POST[$key])) {
+ if (array_key_exists($key, $this->POST)) {
+ if (!is_array($this->POST[$key])) {
throw new UserError("POST parameter {$key} is a single value, expected array");
}
return $this->POST[$key];
@@ -159,7 +154,7 @@ public function get_POST_array(string $key): ?array
public function req_POST_array(string $key): array
{
$value = $this->get_POST_array($key);
- if($value === null) {
+ if ($value === null) {
throw new UserError("Missing POST parameter {$key}");
}
return $value;
@@ -167,7 +162,7 @@ public function req_POST_array(string $key): array
public function page_starts_with(string $name): bool
{
- return (count($this->args) >= 1) && ($this->args[0] == $name);
+ return str_starts_with($this->path, $name);
}
/**
@@ -184,10 +179,10 @@ public function page_matches(
): bool {
global $user;
- if($paged) {
- if($this->page_matches("$name/{page_num}", $method, $authed, $permission, false)) {
+ if ($paged) {
+ if ($this->page_matches("$name/{page_num}", $method, $authed, $permission, false)) {
$pn = $this->get_arg("page_num");
- if(is_numberish($pn)) {
+ if (is_numberish($pn)) {
return true;
}
}
@@ -197,7 +192,7 @@ public function page_matches(
$authed = $authed ?? $method == "POST";
// method check is fast so do that first
- if($method !== null && $this->method !== $method) {
+ if ($method !== null && $this->method !== $method) {
return false;
}
@@ -218,10 +213,15 @@ public function page_matches(
// if we matched the method and the path, but the page requires
// authentication and the user is not authenticated, then complain
- if($authed && $this->is_authed === false) {
- throw new PermissionDenied("Permission Denied: Missing CSRF Token");
+ if ($authed && !defined("UNITTEST")) {
+ if (!isset($this->POST["auth_token"])) {
+ throw new PermissionDenied("Permission Denied: Missing CSRF Token");
+ }
+ if ($this->POST["auth_token"] != $user->get_auth_token()) {
+ throw new PermissionDenied("Permission Denied: Invalid CSRF Token (Go back, refresh the page, and try again?)");
+ }
}
- if($permission !== null && !$user->can($permission)) {
+ if ($permission !== null && !$user->can($permission)) {
throw new PermissionDenied("Permission Denied: {$user->name} lacks permission {$permission}");
}
@@ -233,9 +233,9 @@ public function page_matches(
*/
public function get_arg(string $n, ?string $default = null): string
{
- if(array_key_exists($n, $this->named_args)) {
+ if (array_key_exists($n, $this->named_args)) {
return rawurldecode($this->named_args[$n]);
- } elseif($default !== null) {
+ } elseif ($default !== null) {
return $default;
} else {
throw new UserError("Page argument {$n} is missing");
@@ -244,12 +244,12 @@ public function get_arg(string $n, ?string $default = null): string
public function get_iarg(string $n, ?int $default = null): int
{
- if(array_key_exists($n, $this->named_args)) {
- if(is_numberish($this->named_args[$n]) === false) {
+ if (array_key_exists($n, $this->named_args)) {
+ if (is_numberish($this->named_args[$n]) === false) {
throw new UserError("Page argument {$n} exists but is not numeric");
}
return int_escape($this->named_args[$n]);
- } elseif($default !== null) {
+ } elseif ($default !== null) {
return $default;
} else {
throw new UserError("Page argument {$n} is missing");
diff --git a/core/exceptions.php b/core/exceptions.php
index d0edc8394..9b096b2c0 100644
--- a/core/exceptions.php
+++ b/core/exceptions.php
@@ -57,7 +57,7 @@ class ObjectNotFound extends UserError
public int $http_code = 404;
}
-class ImageNotFound extends ObjectNotFound
+class PostNotFound extends ObjectNotFound
{
}
diff --git a/core/extension.php b/core/extension.php
index 06aed4274..e4823c461 100644
--- a/core/extension.php
+++ b/core/extension.php
@@ -28,33 +28,11 @@ abstract class Extension
public function __construct(?string $class = null)
{
$class = $class ?? get_called_class();
- $this->theme = $this->get_theme_object($class);
+ $this->theme = Themelet::get_for_extension_class($class);
$this->info = ExtensionInfo::get_for_extension_class($class);
$this->key = $this->info->key;
}
- /**
- * Find the theme object for a given extension.
- */
- private function get_theme_object(string $base): Themelet
- {
- $base = str_replace("Shimmie2\\", "", $base);
- $custom = "Shimmie2\Custom{$base}Theme";
- $normal = "Shimmie2\\{$base}Theme";
-
- if (class_exists($custom)) {
- $c = new $custom();
- assert(is_a($c, Themelet::class));
- return $c;
- } elseif (class_exists($normal)) {
- $n = new $normal();
- assert(is_a($n, Themelet::class));
- return $n;
- } else {
- return new Themelet();
- }
- }
-
/**
* Override this to change the priority of the extension,
* lower numbered ones will receive events first.
@@ -181,14 +159,16 @@ public function is_supported(): bool
{
if ($this->supported === null) {
$this->check_support();
+ assert(!is_null($this->supported));
}
return $this->supported;
}
public function get_support_info(): string
{
- if ($this->supported === null) {
+ if ($this->support_info === null) {
$this->check_support();
+ assert(!is_null($this->support_info));
}
return $this->support_info;
}
@@ -340,7 +320,7 @@ public function onDataUpload(DataUploadEvent $event): void
// Right now tags are the only thing that get merged, so
// we can just send a TagSetEvent - in the future we might
// want a dedicated MergeEvent?
- if(!empty($event->metadata['tags'])) {
+ if (!empty($event->metadata['tags'])) {
$tags = Tag::explode($existing->get_tag_list() . " " . $event->metadata['tags']);
send_event(new TagSetEvent($existing, $tags));
}
@@ -373,12 +353,10 @@ public function onDataUpload(DataUploadEvent $event): void
// If everything is OK, then move the file to the archive
$filename = warehouse_path(Image::IMAGE_DIR, $event->hash);
- if (!@copy($event->tmpname, $filename)) {
- $errors = error_get_last();
- throw new UploadException(
- "Failed to copy file from uploads ({$event->tmpname}) to archive ($filename): ".
- "{$errors['type']} / {$errors['message']}"
- );
+ try {
+ \Safe\copy($event->tmpname, $filename);
+ } catch (\Exception $e) {
+ throw new UploadException("Failed to copy file from uploads ({$event->tmpname}) to archive ($filename): ".$e->getMessage());
}
$event->images[] = $iae->image;
diff --git a/core/imageboard/image.php b/core/imageboard/image.php
index 6bab17638..8f2ae083a 100644
--- a/core/imageboard/image.php
+++ b/core/imageboard/image.php
@@ -86,11 +86,11 @@ public function __construct(?array $row = null)
// we only want the key=>value ones
if (is_numeric($name)) {
continue;
- } elseif(property_exists($this, $name)) {
+ } elseif (property_exists($this, $name)) {
$t = (new \ReflectionProperty($this, $name))->getType();
assert(!is_null($t));
- if(is_a($t, \ReflectionNamedType::class)) {
- if(is_null($value)) {
+ if (is_a($t, \ReflectionNamedType::class)) {
+ if (is_null($value)) {
$this->$name = null;
} else {
$this->$name = match($t->getName()) {
@@ -102,7 +102,7 @@ public function __construct(?array $row = null)
}
}
- } elseif(array_key_exists($name, static::$prop_types)) {
+ } elseif (array_key_exists($name, static::$prop_types)) {
if (is_null($value)) {
$value = null;
} else {
@@ -118,7 +118,7 @@ public function __construct(?array $row = null)
// it isn't static and it isn't a known prop_type -
// maybe from an old extension that has since been
// disabled? Just ignore it.
- if(defined('UNITTEST')) {
+ if (defined('UNITTEST')) {
throw new \Exception("Unknown column $name in images table");
}
}
@@ -135,7 +135,7 @@ public function offsetExists(mixed $offset): bool
public function offsetGet(mixed $offset): mixed
{
assert(is_string($offset));
- if(!$this->offsetExists($offset)) {
+ if (!$this->offsetExists($offset)) {
$known = implode(", ", array_keys(static::$prop_types));
throw new \OutOfBoundsException("Undefined dynamic property: $offset (Known: $known)");
}
@@ -178,10 +178,10 @@ public static function by_id(int $post_id): ?Image
public static function by_id_ex(int $post_id): Image
{
$maybe_post = static::by_id($post_id);
- if(!is_null($maybe_post)) {
+ if (!is_null($maybe_post)) {
return $maybe_post;
}
- throw new ImageNotFound("Image $post_id not found");
+ throw new PostNotFound("Image $post_id not found");
}
public static function by_hash(string $hash): ?Image
@@ -265,7 +265,6 @@ public function get_prev(array $tags = []): ?Image
public function get_owner(): User
{
$user = User::by_id($this->owner_id);
- assert(!is_null($user));
return $user;
}
@@ -450,7 +449,7 @@ public function get_info(): string
*/
public function get_image_filename(): string
{
- if(!is_null($this->tmp_file)) {
+ if (!is_null($this->tmp_file)) {
return $this->tmp_file;
}
return warehouse_path(self::IMAGE_DIR, $this->hash);
@@ -632,8 +631,8 @@ public function remove_image_only(bool $quiet = false): void
{
$img_del = @unlink($this->get_image_filename());
$thumb_del = @unlink($this->get_thumb_filename());
- if($img_del && $thumb_del) {
- if(!$quiet) {
+ if ($img_del && $thumb_del) {
+ if (!$quiet) {
log_info("core_image", "Deleted files for Post #{$this->id} ({$this->hash})");
}
} else {
diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
index d267aa396..1d1d1adbc 100644
--- a/core/imageboard/misc.php
+++ b/core/imageboard/misc.php
@@ -32,7 +32,7 @@ function add_dir(string $base, array $extra_tags = []): array
'tags' => Tag::implode($tags),
]));
$results = [];
- foreach($dae->images as $image) {
+ foreach ($dae->images as $image) {
$results[] = new UploadSuccess($filename, $image->id);
}
return $results;
@@ -136,7 +136,7 @@ function get_thumbnail_max_size_scaled(): array
}
-function create_image_thumb(Image $image, string $engine = null): void
+function create_image_thumb(Image $image, ?string $engine = null): void
{
global $config;
create_scaled_image(
diff --git a/core/imageboard/search.php b/core/imageboard/search.php
index b89b70a68..40143c835 100644
--- a/core/imageboard/search.php
+++ b/core/imageboard/search.php
@@ -84,7 +84,7 @@ class Search
*/
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags = []): \FFSPHP\PDOStatement
{
- global $database, $user;
+ global $config, $database, $user;
if ($start < 0) {
$start = 0;
@@ -93,9 +93,10 @@ private static function find_images_internal(int $start = 0, ?int $limit = null,
$limit = 1;
}
- if (SPEED_HAX) {
- if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
- throw new PermissionDenied("Anonymous users may only search for up to 3 tags at a time");
+ if (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_int(SpeedHaxConfig::BIG_SEARCH) > 0) {
+ $anon_limit = $config->get_int(SpeedHaxConfig::BIG_SEARCH);
+ if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > $anon_limit) {
+ throw new PermissionDenied("Anonymous users may only search for up to $anon_limit tags at a time");
}
}
@@ -146,7 +147,7 @@ public static function find_images_iterable(int $start = 0, ?int $limit = null,
public static function get_images(array $ids): array
{
$visible_images = [];
- foreach(Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) {
+ foreach (Search::find_images(tags: ["id=" . implode(",", $ids)]) as $image) {
$visible_images[$image->id] = $image;
}
$visible_ids = array_keys($visible_images);
@@ -182,15 +183,16 @@ private static function count_total_images(): int
*/
public static function count_images(array $tags = []): int
{
- global $cache, $database;
+ global $cache, $config, $database;
$tag_count = count($tags);
- // SPEED_HAX ignores the fact that extensions can add img_conditions
+ // speed_hax ignores the fact that extensions can add img_conditions
// even when there are no tags being searched for
- if (SPEED_HAX && $tag_count === 0) {
+ $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::LIMIT_COMPLEX));
+ if ($speed_hax && $tag_count === 0) {
// total number of images in the DB
$total = self::count_total_images();
- } elseif (SPEED_HAX && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
+ } elseif ($speed_hax && $tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
if (!str_starts_with($tags[0], "-")) {
// one positive tag - we can look that up directly
$total = self::count_tag($tags[0]);
@@ -207,7 +209,7 @@ public static function count_images(array $tags = []): int
[$tag_conditions, $img_conditions, $order] = self::terms_to_conditions($tags);
$querylet = self::build_search_querylet($tag_conditions, $img_conditions, null);
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
- if (SPEED_HAX && $total > 5000) {
+ if ($speed_hax && $total > 5000) {
// when we have a ton of images, the count
// won't change dramatically very often
$cache->set($cache_key, $total, 3600);
@@ -451,7 +453,7 @@ public static function build_search_querylet(
$query->append(new Querylet($img_sql, $img_vars));
}
- if(!is_null($order)) {
+ if (!is_null($order)) {
$query->append(new Querylet(" ORDER BY ".$order));
}
diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php
index fab6b1f2c..95c266ccb 100644
--- a/core/imageboard/tag.php
+++ b/core/imageboard/tag.php
@@ -196,13 +196,13 @@ public static function explode(string $tags, bool $tagme = true): array
public static function sanitize(string $tag): string
{
- $tag = preg_replace("/\s/", "", $tag); # whitespace
+ $tag = preg_replace_ex("/\s/", "", $tag); # whitespace
assert($tag !== null);
- $tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
+ $tag = preg_replace_ex('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
assert($tag !== null);
- $tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
+ $tag = preg_replace_ex("/\.+/", ".", $tag); # strings of dots?
assert($tag !== null);
- $tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
+ $tag = preg_replace_ex("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
assert($tag !== null);
$tag = trim($tag, ", \t\n\r\0\x0B");
diff --git a/core/install.php b/core/install.php
index d5326dbe3..77aa07c3e 100644
--- a/core/install.php
+++ b/core/install.php
@@ -314,7 +314,8 @@ class VARCHAR(32) NOT NULL DEFAULT 'user',
function write_config(string $dsn): void
{
- $file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n";
+ $secret = bin2hex(random_bytes(16));
+ $file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\ndefine('SECRET', '$secret');\n";
if (!file_exists("data/config")) {
mkdir("data/config", 0755, true);
diff --git a/core/microhtml.php b/core/microhtml.php
index 7dd8662e3..5847ffd89 100644
--- a/core/microhtml.php
+++ b/core/microhtml.php
@@ -8,11 +8,11 @@
use function MicroHTML\{emptyHTML};
use function MicroHTML\A;
+use function MicroHTML\CODE;
+use function MicroHTML\DIV;
use function MicroHTML\FORM;
use function MicroHTML\INPUT;
-use function MicroHTML\DIV;
use function MicroHTML\OPTION;
-use function MicroHTML\PRE;
use function MicroHTML\P;
use function MicroHTML\SELECT;
use function MicroHTML\SPAN;
@@ -87,7 +87,7 @@ function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
{
return DIV(
["class" => "command_example"],
- PRE($ex),
+ CODE($ex),
P($desc)
);
}
@@ -163,14 +163,14 @@ function SHM_POST_INFO(
HTMLElement|string|null $edit = null,
string|null $link = null,
): HTMLElement {
- if(!is_null($view) && !is_null($edit)) {
+ if (!is_null($view) && !is_null($edit)) {
$show = emptyHTML(
SPAN(["class" => "view"], $view),
SPAN(["class" => "edit"], $edit),
);
- } elseif(!is_null($edit)) {
+ } elseif (!is_null($edit)) {
$show = $edit;
- } elseif(!is_null($view)) {
+ } elseif (!is_null($view)) {
$show = $view;
} else {
$show = "???";
diff --git a/core/basepage.php b/core/page.php
similarity index 82%
rename from core/basepage.php
rename to core/page.php
index b8ffffc95..ba1bbba5d 100644
--- a/core/basepage.php
+++ b/core/page.php
@@ -6,7 +6,7 @@
use MicroHTML\HTMLElement;
-use function MicroHTML\{emptyHTML,rawHTML,HTML,HEAD,BODY};
+use function MicroHTML\{emptyHTML, rawHTML, HTML, HEAD, BODY, TITLE, LINK, SCRIPT, A, B, joinHTML, BR, H1, HEADER as HTML_HEADER, NAV, ARTICLE, FOOTER, SECTION, H3, DIV};
require_once "core/event.php";
@@ -36,14 +36,12 @@ public function __construct(string $name, string $value, int $time, string $path
}
/**
- * Class Page
- *
* A data structure for holding all the bits of data that make up a page.
*
* The various extensions all add whatever they want to this structure,
* then Layout turns it into HTML.
*/
-class BasePage
+class Page
{
public PageMode $mode = PageMode::PAGE;
private string $mime;
@@ -101,7 +99,7 @@ public function set_file(string $file, bool $delete = false): void
public function set_filename(string $filename, string $disposition = "attachment"): void
{
$max_len = 250;
- if(strlen($filename) > $max_len) {
+ if (strlen($filename) > $max_len) {
// remove extension, truncate filename, apply extension
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$filename = substr($filename, 0, $max_len - strlen($ext) - 1) . '.' . $ext;
@@ -131,7 +129,7 @@ public function set_redirect(string $redirect): void
public string $subheading = "";
public bool $left_enabled = true;
- /** @var string[] */
+ /** @var HTMLElement[] */
public array $html_headers = [];
/** @var string[] */
@@ -157,6 +155,9 @@ public function set_code(int $code): void
public function set_title(string $title): void
{
$this->title = $title;
+ if ($this->heading === "") {
+ $this->heading = $title;
+ }
}
public function set_heading(string $heading): void
@@ -179,17 +180,6 @@ public function disable_left(): void
$this->left_enabled = false;
}
- /**
- * Add a line to the HTML head section.
- */
- public function add_html_header(string $line, int $position = 50): void
- {
- while (isset($this->html_headers[$position])) {
- $position++;
- }
- $this->html_headers[$position] = $line;
- }
-
/**
* Add a http header to be sent to the client.
*/
@@ -222,17 +212,26 @@ public function get_cookie(string $name): ?string
}
}
+ /**
+ * Add a line to the HTML head section.
+ */
+ public function add_html_header(HTMLElement $line, int $position = 50): void
+ {
+ while (isset($this->html_headers[$position])) {
+ $position++;
+ }
+ $this->html_headers[$position] = $line;
+ }
+
/**
* Get all the HTML headers that are currently set and return as a string.
*/
- public function get_all_html_headers(): string
+ public function get_all_html_headers(): HTMLElement
{
- $data = '';
ksort($this->html_headers);
- foreach ($this->html_headers as $line) {
- $data .= "\t\t" . $line . "\n";
- }
- return $data;
+ return emptyHTML(
+ ...$this->html_headers
+ );
}
/**
@@ -247,14 +246,14 @@ public function add_block(Block $block): void
* Find a block which contains the given text
* (Useful for unit tests)
*/
- public function find_block(string $text): ?Block
+ public function find_block(string $text): Block
{
foreach ($this->blocks as $block) {
if ($block->header == $text) {
return $block;
}
}
- return null;
+ throw new \Exception("Block not found: $text");
}
// ==============================================
@@ -305,7 +304,7 @@ public function display(): void
if (!is_null($this->filename)) {
header('Content-Disposition: ' . $this->disposition . '; filename=' . $this->filename);
}
- assert($this->file, "file should not be null with PageMode::FILE");
+ assert(!is_null($this->file), "file should not be null with PageMode::FILE");
// https://gist.github.com/codler/3906826
$size = \Safe\filesize($this->file); // File size
@@ -384,8 +383,15 @@ public function add_auto_html_headers(): void
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
# static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico
- $this->add_html_header("", 41);
- $this->add_html_header("", 42);
+ $this->add_html_header(LINK([
+ 'rel' => 'icon',
+ 'type' => 'image/x-icon',
+ 'href' => "$data_href/favicon.ico"
+ ]), 41);
+ $this->add_html_header(LINK([
+ 'rel' => 'apple-touch-icon',
+ 'href' => "$data_href/apple-touch-icon.png"
+ ]), 42);
//We use $config_latest to make sure cache is reset if config is ever updated.
$config_latest = 0;
@@ -394,13 +400,24 @@ public function add_auto_html_headers(): void
}
$css_cache_file = $this->get_css_cache_file($theme_name, $config_latest);
- $this->add_html_header("", 43);
+ $this->add_html_header(LINK([
+ 'rel' => 'stylesheet',
+ 'href' => "$data_href/$css_cache_file",
+ 'type' => 'text/css'
+ ]), 43);
$initjs_cache_file = $this->get_initjs_cache_file($theme_name, $config_latest);
- $this->add_html_header("", 44);
+ $this->add_html_header(SCRIPT([
+ 'src' => "$data_href/$initjs_cache_file",
+ 'type' => 'text/javascript'
+ ]));
$js_cache_file = $this->get_js_cache_file($theme_name, $config_latest);
- $this->add_html_header("", 44);
+ $this->add_html_header(SCRIPT([
+ 'defer' => true,
+ 'src' => "$data_href/$js_cache_file",
+ 'type' => 'text/javascript'
+ ]));
}
private function get_css_cache_file(string $theme_name, int $config_latest): string
@@ -417,7 +434,7 @@ private function get_css_cache_file(string $theme_name, int $config_latest): str
$css_cache_file = data_path("cache/style/{$theme_name}.{$css_latest}.{$css_md5}.css");
if (!file_exists($css_cache_file)) {
$mcss = new \MicroBundler\MicroBundler();
- foreach($css_files as $css) {
+ foreach ($css_files as $css) {
$mcss->addSource($css);
}
$mcss->save($css_cache_file);
@@ -440,7 +457,7 @@ private function get_initjs_cache_file(string $theme_name, int $config_latest):
$js_cache_file = data_path("cache/initscript/{$theme_name}.{$js_latest}.{$js_md5}.js");
if (!file_exists($js_cache_file)) {
$mcss = new \MicroBundler\MicroBundler();
- foreach($js_files as $js) {
+ foreach ($js_files as $js) {
$mcss->addSource($js);
}
$mcss->save($js_cache_file);
@@ -468,7 +485,7 @@ private function get_js_cache_file(string $theme_name, int $config_latest): stri
$js_cache_file = data_path("cache/script/{$theme_name}.{$js_latest}.{$js_md5}.js");
if (!file_exists($js_cache_file)) {
$mcss = new \MicroBundler\MicroBundler();
- foreach($js_files as $js) {
+ foreach ($js_files as $js) {
$mcss->addSource($js);
}
$mcss->save($js_cache_file);
@@ -549,52 +566,56 @@ protected function get_nav_links(): array
*/
public function render(): void
{
- global $config, $user;
+ print (string)$this->html_html(
+ $this->head_html(),
+ $this->body_html()
+ );
+ }
- $head = $this->head_html();
- $body = $this->body_html();
+ public function html_html(HTMLElement $head, string|HTMLElement $body): HTMLElement
+ {
+ global $user;
$body_attrs = [
"data-userclass" => $user->class->name,
"data-base-href" => get_base_href(),
+ "data-base-link" => make_link(""),
];
- print emptyHTML(
+ return emptyHTML(
rawHTML(""),
HTML(
["lang" => "en"],
- HEAD(rawHTML($head)),
- BODY($body_attrs, rawHTML($body))
+ HEAD($head),
+ BODY($body_attrs, $body)
)
);
}
- protected function head_html(): string
+ protected function head_html(): HTMLElement
{
- $html_header_html = $this->get_all_html_headers();
-
- return "
- {$this->title}
- $html_header_html
- ";
+ return emptyHTML(
+ TITLE($this->title),
+ $this->get_all_html_headers(),
+ );
}
- protected function body_html(): string
+ protected function body_html(): HTMLElement
{
- $left_block_html = "";
- $main_block_html = "";
- $sub_block_html = "";
+ $left_block_html = [];
+ $main_block_html = [];
+ $sub_block_html = [];
foreach ($this->blocks as $block) {
switch ($block->section) {
case "left":
- $left_block_html .= $block->get_html(true);
+ $left_block_html[] = $this->block_html($block, true);
break;
case "main":
- $main_block_html .= $block->get_html(false);
+ $main_block_html[] = $this->block_html($block, false);
break;
case "subheading":
- $sub_block_html .= $block->get_html(false);
+ $sub_block_html[] = $this->block_html($block, false);
break;
default:
print "error: {$block->header} using an unknown section ({$block->section})";
@@ -603,41 +624,60 @@ protected function body_html(): string
}
$footer_html = $this->footer_html();
- $flash_html = $this->flash ? "".nl2br(html_escape(implode("\n", $this->flash)))."" : "";
- return "
-
- {$this->heading}
- $sub_block_html
-
-
-
- $flash_html
- $main_block_html
-
-
- ";
+ )
+ );
+ }
+
+ protected function block_html(Block $block, bool $hidable): HTMLElement
+ {
+ $html = SECTION(['id' => $block->id]);
+ if (!empty($block->header)) {
+ $html->appendChild(H3(["data-toggle-sel" => "#{$block->id}", "class" => $hidable ? "shm-toggler" : ""], $block->header));
+ }
+ if (!empty($block->body)) {
+ $html->appendChild(DIV(['class' => "blockbody"], $block->body));
+ }
+ return $html;
+ }
+
+ protected function flash_html(): HTMLElement
+ {
+ if ($this->flash) {
+ return B(["id" => "flash"], rawHTML(nl2br(html_escape(implode("\n", $this->flash)))));
+ }
+ return emptyHTML();
}
- protected function footer_html(): string
+ protected function footer_html(): HTMLElement
{
$debug = get_debug_info();
$contact_link = contact_link();
- $contact = empty($contact_link) ? "" : "
Contact";
-
- return "
- Media © their respective owners,
- Shimmie ©
- Shish &
- The Team
- 2007-2024,
- based on the Danbooru concept.
- $debug
- $contact
- ";
+ return joinHTML("", [
+ "Media © their respective owners, ",
+ A(["href" => "https://code.shishnet.org/shimmie2/"], "Shimmie"),
+ " © ",
+ A(["href" => "https://www.shishnet.org/"], "Shish"),
+ " & ",
+ A(["href" => "https://github.com/shish/shimmie2/graphs/contributors"], "The Team"),
+ " 2007-2024, based on the Danbooru concept.",
+ BR(), $debug,
+ $contact_link ? emptyHTML(BR(), A(["href" => $contact_link], "Contact")) : ""
+ ]);
}
}
@@ -711,7 +751,7 @@ public function __construct(string $name, Link $link, string|HTMLElement $descri
/**
* @param string[] $pages_matched
*/
- public static function is_active(array $pages_matched, string $url = null): bool
+ public static function is_active(array $pages_matched, ?string $url = null): bool
{
/**
* Woo! We can actually SEE THE CURRENT PAGE!! (well... see it highlighted in the menu.)
diff --git a/core/permissions.php b/core/permissions.php
index f0592f5ef..7a0814be1 100644
--- a/core/permissions.php
+++ b/core/permissions.php
@@ -20,7 +20,7 @@ abstract class Permissions
public const CHANGE_USER_SETTING = "change_user_setting";
public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting";
- /** search for more than 3 tags at once (only applies if SPEED_HAX is active) */
+ /** search for more than 3 tags at once (only applies if Speed Hax is active) */
public const BIG_SEARCH = "big_search";
/** enable or disable extensions */
diff --git a/core/polyfills.php b/core/polyfills.php
index 4e11f1677..6d6515221 100644
--- a/core/polyfills.php
+++ b/core/polyfills.php
@@ -40,7 +40,7 @@ function array_iunique(array $array): array
function ip_in_range(string $IP, string $CIDR): bool
{
$parts = explode("/", $CIDR);
- if(count($parts) == 1) {
+ if (count($parts) == 1) {
$parts[1] = "32";
}
list($net, $mask) = $parts;
@@ -159,7 +159,7 @@ function flush_output(): void
function stream_file(string $file, int $start, int $end): void
{
$fp = fopen($file, 'r');
- if(!$fp) {
+ if (!$fp) {
throw new \Exception("Failed to open $file");
}
try {
@@ -168,7 +168,7 @@ function stream_file(string $file, int $start, int $end): void
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
- assert($buffer >= 0);
+ assert($buffer >= 1);
}
echo fread($fp, $buffer);
flush_output();
@@ -423,7 +423,7 @@ function truncate(string $string, int $limit, string $break = " ", string $pad =
assert($limit > $padlen, "Can't truncate to a length less than the padding length");
// if string is shorter or equal to limit, leave it alone
- if($strlen <= $limit) {
+ if ($strlen <= $limit) {
return $string;
}
@@ -628,7 +628,9 @@ function validate_input(array $inputs): array
if (in_array('user_id', $flags)) {
$id = int_escape($value);
if (in_array('exists', $flags)) {
- if (is_null(User::by_id($id))) {
+ try {
+ User::by_id($id);
+ } catch (UserNotFound $e) {
throw new InvalidInput("User #$id does not exist");
}
}
@@ -646,7 +648,7 @@ function validate_input(array $inputs): array
$outputs[$key] = $value;
} elseif (in_array('user_class', $flags)) {
if (!array_key_exists($value, UserClass::$known_classes)) {
- throw new InvalidInput("Invalid user class: ".html_escape($value));
+ throw new InvalidInput("Invalid user class: $value");
}
$outputs[$key] = $value;
} elseif (in_array('email', $flags)) {
@@ -656,7 +658,7 @@ function validate_input(array $inputs): array
} elseif (in_array('int', $flags)) {
$value = trim($value);
if (empty($value) || !is_numeric($value)) {
- throw new InvalidInput("Invalid int: ".html_escape($value));
+ throw new InvalidInput("Invalid int: $value");
}
$outputs[$key] = (int)$value;
} elseif (in_array('bool', $flags)) {
@@ -693,7 +695,8 @@ function validate_input(array $inputs): array
*/
function sanitize_path(string $path): string
{
- return preg_replace('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path);
+ $r = preg_replace_ex('|[\\\\/]+|S', DIRECTORY_SEPARATOR, $path);
+ return $r;
}
/**
diff --git a/core/send_event.php b/core/send_event.php
index 553a4b15c..bd7a18afb 100644
--- a/core/send_event.php
+++ b/core/send_event.php
@@ -19,18 +19,19 @@ class TimeoutException extends \RuntimeException
function _load_event_listeners(): void
{
- global $_shm_event_listeners;
+ global $_shm_event_listeners, $config;
- $ver = preg_replace("/[^a-zA-Z0-9\.]/", "_", VERSION);
+ $ver = preg_replace_ex("/[^a-zA-Z0-9\.]/", "_", VERSION);
$key = md5(Extension::get_enabled_extensions_as_string());
+ $speed_hax = (Extension::is_enabled(SpeedHaxInfo::KEY) && $config->get_bool(SpeedHaxConfig::CACHE_EVENT_LISTENERS));
$cache_path = data_path("cache/event_listeners/el.$ver.$key.php");
- if (SPEED_HAX && file_exists($cache_path)) {
+ if ($speed_hax && file_exists($cache_path)) {
require_once($cache_path);
} else {
_set_event_listeners();
- if (SPEED_HAX) {
+ if ($speed_hax) {
_dump_event_listeners($_shm_event_listeners, $cache_path);
}
}
diff --git a/core/stdlib_ex.php b/core/stdlib_ex.php
index 3f9514ac6..69c87b33f 100644
--- a/core/stdlib_ex.php
+++ b/core/stdlib_ex.php
@@ -7,9 +7,9 @@
*/
function false_throws(mixed $x, ?callable $errorgen = null): mixed
{
- if($x === false) {
+ if ($x === false) {
$msg = "Unexpected false";
- if($errorgen) {
+ if ($errorgen) {
$msg = $errorgen();
}
throw new \Exception($msg);
@@ -32,3 +32,12 @@ function filter_var_ex(mixed $variable, int $filter = FILTER_DEFAULT, mixed $opt
{
return false_throws(filter_var($variable, $filter, $options));
}
+
+function preg_replace_ex(string $pattern, string $replacement, string $subject, int $limit = -1, ?int &$count = null): string
+{
+ $res = preg_replace($pattern, $replacement, $subject, $limit, $count);
+ if (is_null($res)) {
+ throw new \Exception("preg_replace failed");
+ }
+ return $res;
+}
diff --git a/core/sys_config.php b/core/sys_config.php
index 2af3ac692..a36eb24c0 100644
--- a/core/sys_config.php
+++ b/core/sys_config.php
@@ -14,7 +14,7 @@
* Do NOT change them in this file. These are the defaults only!
*
* Example:
- * define("SPEED_HAX", true);
+ * define("DEBUG", true);
*/
function _d(string $name, mixed $value): void
@@ -29,12 +29,12 @@ function _d(string $name, mixed $value): void
_d("CACHE_DSN", null); // string cache connection details
_d("DEBUG", false); // boolean print various debugging details
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
-_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
-_d("VERSION", "2.11.0-alpha"); // string shimmie version
+_d("VERSION", "2.12.0-alpha"); // string shimmie version
_d("TIMEZONE", null); // string timezone
_d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
_d("TRACE_FILE", null); // string file to log performance data into
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
_d("TRUSTED_PROXIES", []); // array trust "X-Real-IP" / "X-Forwarded-For" / "X-Forwarded-Proto" headers from these IP ranges
+_d("SECRET", DATABASE_DSN); // string A secret bit of data used to salt some hashes
diff --git a/core/testcase.php b/core/testcase.php
index 9764896bf..e1e420ab3 100644
--- a/core/testcase.php
+++ b/core/testcase.php
@@ -4,7 +4,7 @@
namespace Shimmie2;
-if(class_exists("\\PHPUnit\\Framework\\TestCase")) {
+if (class_exists("\\PHPUnit\\Framework\\TestCase")) {
abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase
{
protected static string $anon_name = "anonymous";
@@ -28,7 +28,7 @@ public static function setUpBeforeClass(): void
*/
public function setUp(): void
{
- global $database, $_tracer;
+ global $database, $_tracer, $page;
$_tracer->begin($this->name());
$_tracer->begin("setUp");
$class = str_replace("Test", "", get_class($this));
@@ -44,10 +44,11 @@ public function setUp(): void
$database->execute("SAVEPOINT test_start");
self::log_out();
foreach ($database->get_col("SELECT id FROM images") as $image_id) {
- send_event(new ImageDeletionEvent(Image::by_id((int)$image_id), true));
+ send_event(new ImageDeletionEvent(Image::by_id_ex((int)$image_id), true));
}
// Reload users from the database in case they were modified
UserClass::loadClasses();
+ $page = new Page();
$_tracer->end(); # setUp
$_tracer->begin("test");
@@ -93,12 +94,14 @@ private static function check_args(array $args): array
/**
* @param array $get_args
* @param array $post_args
+ * @param array $cookies
*/
protected static function request(
string $method,
string $page_name,
array $get_args = [],
- array $post_args = []
+ array $post_args = [],
+ array $cookies = ["shm_accepted_terms" => "true"],
): Page {
// use a fresh page
global $page;
@@ -112,6 +115,7 @@ protected static function request(
$_SERVER['REQUEST_URI'] = make_link($page_name, http_build_query($get_args));
$_GET = $get_args;
$_POST = $post_args;
+ $_COOKIE = $cookies;
$page = new Page();
send_event(new PageRequestEvent($method, $page_name, $get_args, $post_args));
if ($page->mode == PageMode::REDIRECT) {
@@ -161,34 +165,46 @@ protected function assert_response(int $code): void
$this->assertEquals($code, $page->code);
}
- protected function page_to_text(string $section = null): string
+ /**
+ * @param array $blocks
+ * @param ?string $section
+ * @return string
+ */
+ private function blocks_to_text(array $blocks, ?string $section): string
{
- global $page;
- if ($page->mode == PageMode::PAGE) {
- $text = $page->title . "\n";
- foreach ($page->blocks as $block) {
- if (is_null($section) || $section == $block->section) {
- $text .= $block->header . "\n";
- $text .= $block->body . "\n\n";
- }
+ $text = "";
+ foreach ($blocks as $block) {
+ if (is_null($section) || $section == $block->section) {
+ $text .= $block->header . "\n";
+ $text .= $block->body . "\n\n";
}
- return $text;
- } elseif ($page->mode == PageMode::DATA) {
- return $page->data;
- } else {
- $this->fail("Page mode is {$page->mode->name} (only PAGE and DATA are supported)");
}
+ return $text;
+ }
+
+ protected function page_to_text(?string $section = null): string
+ {
+ global $page;
+
+ return match($page->mode) {
+ PageMode::PAGE => $page->title . "\n" . $this->blocks_to_text($page->blocks, $section),
+ PageMode::DATA => $page->data,
+ PageMode::REDIRECT => $this->fail("Page mode is REDIRECT ($page->redirect) (only PAGE and DATA are supported)"),
+ PageMode::FILE => $this->fail("Page mode is FILE ($page->file) (only PAGE and DATA are supported)"),
+ PageMode::MANUAL => $this->fail("Page mode is MANUAL (only PAGE and DATA are supported)"),
+ default => $this->fail("Unknown page mode {$page->mode->name}"), // just for phpstan
+ };
}
/**
* Assert that the page contains the given text somewhere in the blocks
*/
- protected function assert_text(string $text, string $section = null): void
+ protected function assert_text(string $text, ?string $section = null): void
{
$this->assertStringContainsString($text, $this->page_to_text($section));
}
- protected function assert_no_text(string $text, string $section = null): void
+ protected function assert_no_text(string $text, ?string $section = null): void
{
$this->assertStringNotContainsString($text, $this->page_to_text($section));
}
@@ -222,21 +238,19 @@ protected function assert_search_results(array $tags, array $results): void
$this->assertEquals($results, $ids);
}
- protected function assertException(string $type, callable $function): \Exception|null
+ protected function assertException(string $type, callable $function): \Exception
{
- $exception = null;
try {
call_user_func($function);
- } catch (\Exception $e) {
- $exception = $e;
+ self::fail("Expected exception of type $type, but none was thrown");
+ } catch (\Exception $exception) {
+ self::assertThat(
+ $exception,
+ new \PHPUnit\Framework\Constraint\Exception($type),
+ "Expected exception of type $type, but got " . get_class($exception)
+ );
+ return $exception;
}
-
- self::assertThat(
- $exception,
- new \PHPUnit\Framework\Constraint\Exception($type),
- "Expected exception of type $type, but got " . ($exception ? get_class($exception) : "none")
- );
- return $exception;
}
// user things
@@ -263,7 +277,7 @@ protected function post_image(string $filename, string $tags): int
"filename" => $filename,
"tags" => $tags,
]));
- if(count($dae->images) == 0) {
+ if (count($dae->images) == 0) {
throw new \Exception("Upload failed :(");
}
return $dae->images[0]->id;
diff --git a/core/tests/BlockTest.php b/core/tests/BlockTest.php
deleted file mode 100644
index b8ae0b78a..000000000
--- a/core/tests/BlockTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-assertEquals(
- "\n",
- $b->get_html()
- );
- }
-}
diff --git a/core/tests/BasePageTest.php b/core/tests/PageTest.php
similarity index 87%
rename from core/tests/BasePageTest.php
rename to core/tests/PageTest.php
index 64ed41e5f..4b76b5774 100644
--- a/core/tests/BasePageTest.php
+++ b/core/tests/PageTest.php
@@ -6,13 +6,13 @@
use PHPUnit\Framework\TestCase;
-require_once "core/basepage.php";
+require_once "core/page.php";
-class BasePageTest extends TestCase
+class PageTest extends TestCase
{
public function test_page(): void
{
- $page = new BasePage();
+ $page = new Page();
$page->set_mode(PageMode::PAGE);
ob_start();
$page->display();
@@ -22,7 +22,7 @@ public function test_page(): void
public function test_file(): void
{
- $page = new BasePage();
+ $page = new Page();
$page->set_mode(PageMode::FILE);
$page->set_file("tests/pbx_screenshot.jpg");
ob_start();
@@ -33,7 +33,7 @@ public function test_file(): void
public function test_data(): void
{
- $page = new BasePage();
+ $page = new Page();
$page->set_mode(PageMode::DATA);
$page->set_data("hello world");
ob_start();
@@ -44,7 +44,7 @@ public function test_data(): void
public function test_redirect(): void
{
- $page = new BasePage();
+ $page = new Page();
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect("/new/page");
ob_start();
diff --git a/core/tests/PolyfillsTest.php b/core/tests/PolyfillsTest.php
index 03f95fe6c..06ea0212b 100644
--- a/core/tests/PolyfillsTest.php
+++ b/core/tests/PolyfillsTest.php
@@ -46,6 +46,8 @@ public function test_bool_escape(): void
$this->assertTrue(bool_escape(true));
$this->assertFalse(bool_escape(false));
+ $this->assertFalse(bool_escape(null));
+
$this->assertTrue(bool_escape("true"));
$this->assertFalse(bool_escape("false"));
diff --git a/core/tests/SQLTest.php b/core/tests/SQLTest.php
new file mode 100644
index 000000000..630435b58
--- /dev/null
+++ b/core/tests/SQLTest.php
@@ -0,0 +1,23 @@
+assertEquals(
+ "foobar",
+ $database->get_one("SELECT 'foo' || 'bar'")
+ );
+ }
+}
diff --git a/core/tests/SearchTest.php b/core/tests/SearchTest.php
index 9a346d617..5abafe1ad 100644
--- a/core/tests/SearchTest.php
+++ b/core/tests/SearchTest.php
@@ -190,7 +190,7 @@ private function assert_BSQ(
int $limit = 9999,
int $start = 0,
array $res = [],
- array $path = null,
+ ?array $path = null,
): void {
global $database;
diff --git a/core/tests/UrlsTest.php b/core/tests/UrlsTest.php
index 598533630..a9ab22689 100644
--- a/core/tests/UrlsTest.php
+++ b/core/tests/UrlsTest.php
@@ -32,7 +32,7 @@ public function test_get_search_terms_from_search_link(): void
};
global $config;
- foreach([true, false] as $nice_urls) {
+ foreach ([true, false] as $nice_urls) {
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
$this->assertEquals(
@@ -54,7 +54,7 @@ public function test_get_search_terms_from_search_link(): void
public function test_make_link(): void
{
global $config;
- foreach([true, false] as $nice_urls) {
+ foreach ([true, false] as $nice_urls) {
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
// basic
@@ -93,7 +93,7 @@ public function test_make_link(): void
public function test_search_link(): void
{
global $config;
- foreach([true, false] as $nice_urls) {
+ foreach ([true, false] as $nice_urls) {
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
$this->assertEquals(
@@ -128,6 +128,20 @@ public function test_get_query(): void
'http://$SERVER/$INSTALL_DIR/index.php?q=$PATH should return $PATH'
);
+ // even when we are /test/... publicly, and generating /test/... URLs,
+ // we should still be able to handle URLs at the root because that's
+ // what apache sends us when it is reverse-proxying a subdirectory
+ $this->assertEquals(
+ "tasty/cake",
+ _get_query("/tasty/cake"),
+ 'http://$SERVER/$INSTALL_DIR/$PATH should return $PATH'
+ );
+ $this->assertEquals(
+ "tasty/cake",
+ _get_query("/index.php?q=tasty/cake"),
+ 'http://$SERVER/$INSTALL_DIR/index.php?q=$PATH should return $PATH'
+ );
+
$this->assertEquals(
"tasty/cake%20pie",
_get_query("/test/index.php?q=tasty/cake%20pie"),
diff --git a/core/tests/UtilTest.php b/core/tests/UtilTest.php
index d99b1fa53..7846d2923 100644
--- a/core/tests/UtilTest.php
+++ b/core/tests/UtilTest.php
@@ -105,21 +105,57 @@ public function test_warehouse_path(): void
);
}
- public function test_load_balance_url(): void
+ public function test_load_balancing_parse(): void
+ {
+ $this->assertEquals(
+ ["foo" => 10, "bar" => 5, "baz" => 5, "quux" => 0],
+ parse_load_balancer_config("foo=10,bar=5,baz=5,quux=0")
+ );
+ }
+
+ public function test_load_balancing_choose(): void
+ {
+ $string_config = "foo=10,bar=5,baz=5,quux=0";
+ $array_config = ["foo" => 10, "bar" => 5, "baz" => 5, "quux" => 0];
+ $hash = "7ac19c10d6859415";
+
+ $this->assertEquals(
+ $array_config,
+ parse_load_balancer_config($string_config)
+ );
+ $this->assertEquals(
+ "foo",
+ choose_load_balancer_node($array_config, $hash)
+ );
+
+ // Check that the balancing gives results in approximately
+ // the right ratio (compatible implmentations should give
+ // exactly these results)
+ $results = ["foo" => 0, "bar" => 0, "baz" => 0, "quux" => 0];
+ for ($i = 0; $i < 2000; $i++) {
+ $results[choose_load_balancer_node($array_config, (string)$i)]++;
+ }
+ $this->assertEquals(
+ ["foo" => 1001, "bar" => 502, "baz" => 497, "quux" => 0],
+ $results
+ );
+ }
+
+ public function test_load_balancing_url(): void
{
$hash = "7ac19c10d6859415";
$ext = "jpg";
// pseudo-randomly select one of the image servers, balanced in given ratio
$this->assertEquals(
- "https://baz.mycdn.com/7ac19c10d6859415.jpg",
- load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash)
+ "https://foo.mycdn.com/7ac19c10d6859415.jpg",
+ load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash)
);
// N'th and N+1'th results should be different
$this->assertNotEquals(
- load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 0),
- load_balance_url("https://{foo=10,bar=5,baz=5}.mycdn.com/$hash.$ext", $hash, 1)
+ load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash, 0),
+ load_balance_url("https://{foo=10,bar=5,baz=5,quux=0}.mycdn.com/$hash.$ext", $hash, 1)
);
}
diff --git a/core/themelet.php b/core/themelet.php
new file mode 100644
index 000000000..3195564b8
--- /dev/null
+++ b/core/themelet.php
@@ -0,0 +1,44 @@
+build_thumb_html($image);
+ }
+
+ public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void
+ {
+ $c = self::get_common();
+ assert(is_a($c, CommonElementsTheme::class));
+ $c->display_paginator($page, $base, $query, $page_number, $total_pages, $show_random);
+ }
+}
diff --git a/core/urls.php b/core/urls.php
index b6ecca4d4..8208dd5b9 100644
--- a/core/urls.php
+++ b/core/urls.php
@@ -6,10 +6,10 @@
class Link
{
- public ?string $page;
+ public string $page;
public ?string $query;
- public function __construct(?string $page = null, ?string $query = null)
+ public function __construct(string $page, ?string $query = null)
{
$this->page = $page;
$this->query = $query;
@@ -29,7 +29,7 @@ public function make_link(): string
*/
function search_link(array $terms = [], int $page = 1): string
{
- if($terms) {
+ if ($terms) {
$q = url_escape(Tag::implode($terms));
return make_link("post/list/$q/$page");
} else {
@@ -55,14 +55,18 @@ function make_link(?string $page = null, ?string $query = null, ?string $fragmen
$parts = [];
$install_dir = get_base_href();
- if (SPEED_HAX || $config->get_bool(SetupConfig::NICE_URLS, false)) {
+ if ($config->get_bool(SetupConfig::NICE_URLS, false)) {
$parts['path'] = "$install_dir/$page";
} else {
$parts['path'] = "$install_dir/index.php";
$query = empty($query) ? "q=$page" : "q=$page&$query";
}
- $parts['query'] = $query; // http_build_query($query);
- $parts['fragment'] = $fragment; // http_build_query($hash);
+ if (!is_null($query)) {
+ $parts['query'] = $query; // http_build_query($query);
+ }
+ if (!is_null($fragment)) {
+ $parts['fragment'] = $fragment; // http_build_query($hash);
+ }
return unparse_url($parts);
}
@@ -83,6 +87,9 @@ function make_link(?string $page = null, ?string $query = null, ?string $fragmen
* can parse it for ourselves
* - generates
* q=post%2Flist
+ * - When apache is reverse-proxying https://external.com/img/index.php
+ * to http://internal:8000/index.php, get_base_href() should return
+ * /img, however the URL in REQUEST_URI is /index.php, not /img/index.php
*
* This function should always return strings with no leading slashes
*/
@@ -90,23 +97,23 @@ function _get_query(?string $uri = null): string
{
$parsed_url = parse_url($uri ?? $_SERVER['REQUEST_URI'] ?? "");
- // if we're looking at http://site.com/$INSTALL_DIR/index.php,
+ // if we're looking at http://site.com/.../index.php,
// then get the query from the "q" parameter
- if(($parsed_url["path"] ?? "") == (get_base_href() . "/index.php")) {
- // $q = $_GET["q"] ?? "";
+ if (str_ends_with($parsed_url["path"] ?? "", "/index.php")) {
// default to looking at the root
$q = "";
- // (we need to manually parse the query string because PHP's $_GET
- // does an extra round of URL decoding, which we don't want)
- foreach(explode('&', $parsed_url['query'] ?? "") as $z) {
+ // We can't just do `$q = $_GET["q"] ?? "";`, we need to manually
+ // parse the query string because PHP's $_GET does an extra round
+ // of URL decoding, which we don't want
+ foreach (explode('&', $parsed_url['query'] ?? "") as $z) {
$qps = explode('=', $z, 2);
- if(count($qps) == 2 && $qps[0] == "q") {
+ if (count($qps) == 2 && $qps[0] == "q") {
$q = $qps[1];
}
}
// if we have no slashes, but do have an encoded
// slash, then we _probably_ encoded too much
- if(!str_contains($q, "/") && str_contains($q, "%2F")) {
+ if (!str_contains($q, "/") && str_contains($q, "%2F")) {
$q = rawurldecode($q);
}
}
@@ -114,7 +121,19 @@ function _get_query(?string $uri = null): string
// if we're looking at http://site.com/$INSTALL_DIR/$PAGE,
// then get the query from the path
else {
- $q = substr($parsed_url["path"] ?? "", strlen(get_base_href() . "/"));
+ $base = get_base_href();
+ $q = $parsed_url["path"] ?? "";
+
+ // sometimes our public URL is /img/foo/bar but after
+ // reverse-proxying shimmie only sees /foo/bar, so only
+ // strip off the /img if it's actually there
+ if (str_starts_with($q, $base)) {
+ $q = substr($q, strlen($base));
+ }
+
+ // whether we are /img/foo/bar or /foo/bar, we still
+ // want to remove the leading slash
+ $q = ltrim($q, "/");
}
assert(!str_starts_with($q, "/"));
@@ -140,9 +159,9 @@ function get_base_href(?array $server_settings = null): string
return BASE_HREF;
}
$server_settings = $server_settings ?? $_SERVER;
- if(str_ends_with($server_settings['PHP_SELF'], 'index.php')) {
+ if (str_ends_with($server_settings['PHP_SELF'], 'index.php')) {
$self = $server_settings['PHP_SELF'];
- } elseif(isset($server_settings['SCRIPT_FILENAME']) && isset($server_settings['DOCUMENT_ROOT'])) {
+ } elseif (isset($server_settings['SCRIPT_FILENAME']) && isset($server_settings['DOCUMENT_ROOT'])) {
$self = substr($server_settings['SCRIPT_FILENAME'], strlen(rtrim($server_settings['DOCUMENT_ROOT'], "/")));
} else {
die("PHP_SELF or SCRIPT_FILENAME need to be set");
diff --git a/core/user.php b/core/user.php
index ea0c3cafb..65ac3ec61 100644
--- a/core/user.php
+++ b/core/user.php
@@ -83,20 +83,27 @@ public function graphql_guid(): string
public static function by_session(string $name, string $session): ?User
{
global $cache, $config, $database;
- $row = $cache->get("user-session:$name-$session");
- if (is_null($row)) {
- if ($database->get_driver_id() === DatabaseDriverID::MYSQL) {
- $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess";
- } else {
- $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
+ $user = $cache->get("user-session-obj:$name-$session");
+ if (is_null($user)) {
+ try {
+ $user_by_name = User::by_name($name);
+ } catch (UserNotFound $e) {
+ return null;
+ }
+ if ($user_by_name->get_session_id() === $session) {
+ $user = $user_by_name;
+ }
+ // For 2.12, check old session IDs and convert to new IDs
+ if (md5($user_by_name->passhash . get_session_ip($config)) === $session) {
+ $user = $user_by_name;
+ $user->set_login_cookie();
}
- $row = $database->get_row($query, ["name" => $name, "ip" => get_session_ip($config), "sess" => $session]);
- $cache->set("user-session:$name-$session", $row, 600);
+ $cache->set("user-session-obj:$name-$session", $user, 600);
}
- return is_null($row) ? null : new User($row);
+ return $user;
}
- public static function by_id(int $id): ?User
+ public static function by_id(int $id): User
{
global $cache, $database;
if ($id === 1) {
@@ -109,51 +116,55 @@ public static function by_id(int $id): ?User
if ($id === 1) {
$cache->set('user-id:'.$id, $row, 600);
}
- return is_null($row) ? null : new User($row);
+ if (is_null($row)) {
+ throw new UserNotFound("Can't find any user with ID $id");
+ }
+ return new User($row);
}
#[Query(name: "user")]
- public static function by_name(string $name): ?User
+ public static function by_name(string $name): User
{
global $database;
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name" => $name]);
- return is_null($row) ? null : new User($row);
- }
-
- public static function name_to_id(string $name): int
- {
- $u = User::by_name($name);
- if (is_null($u)) {
+ if (is_null($row)) {
throw new UserNotFound("Can't find any user named $name");
} else {
- return $u->id;
+ return new User($row);
}
}
- public static function by_name_and_pass(string $name, string $pass): ?User
+ public static function name_to_id(string $name): int
{
- $my_user = User::by_name($name);
+ return User::by_name($name)->id;
+ }
- // If user tried to log in as "foo bar" and failed, try "foo_bar"
- if (!$my_user && str_contains($name, " ")) {
- $my_user = User::by_name(str_replace(" ", "_", $name));
+ public static function by_name_and_pass(string $name, string $pass): User
+ {
+ try {
+ $my_user = User::by_name($name);
+ } catch (UserNotFound $e) {
+ // If user tried to log in as "foo bar" and failed, try "foo_bar"
+ try {
+ $my_user = User::by_name(str_replace(" ", "_", $name));
+ } catch (UserNotFound $e) {
+ log_warning("core-user", "Failed to log in as $name (Invalid username)");
+ throw $e;
+ }
}
- if ($my_user) {
- if ($my_user->passhash == md5(strtolower($name) . $pass)) {
- log_info("core-user", "Migrating from md5 to bcrypt for $name");
- $my_user->set_password($pass);
- }
- if (password_verify($pass, $my_user->passhash)) {
- log_info("core-user", "Logged in as $name ({$my_user->class->name})");
- return $my_user;
- } else {
- log_warning("core-user", "Failed to log in as $name (Invalid password)");
- }
+ if ($my_user->passhash == md5(strtolower($name) . $pass)) {
+ log_info("core-user", "Migrating from md5 to bcrypt for $name");
+ $my_user->set_password($pass);
+ }
+ assert(!is_null($my_user->passhash));
+ if (password_verify($pass, $my_user->passhash)) {
+ log_info("core-user", "Logged in as $name ({$my_user->class->name})");
+ return $my_user;
} else {
- log_warning("core-user", "Failed to log in as $name (Invalid username)");
+ log_warning("core-user", "Failed to log in as $name (Invalid password)");
+ throw new UserNotFound("Can't find anybody with that username and password");
}
- return null;
}
@@ -171,12 +182,6 @@ public function is_anonymous(): bool
return ($this->id === $config->get_int('anon_id'));
}
- public function is_logged_in(): bool
- {
- global $config;
- return ($this->id !== $config->get_int('anon_id'));
- }
-
public function set_class(string $class): void
{
global $database;
@@ -187,8 +192,11 @@ public function set_class(string $class): void
public function set_name(string $name): void
{
global $database;
- if (User::by_name($name)) {
+ try {
+ User::by_name($name);
throw new InvalidInput("Desired username is already in use");
+ } catch (UserNotFound $e) {
+ // if user is not found, we're good
}
$old_name = $this->name;
$this->name = $name;
@@ -246,19 +254,41 @@ public function get_avatar_url(): ?string
/**
* Get an auth token to be used in POST forms
*
- * password = secret, avoid storing directly
- * passhash = bcrypt(password), so someone who gets to the database can't get passwords
- * sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP,
- * and it can't be used to get the passhash to generate new sesskeys
- * authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that
- * the form was generated within the session. Salted and re-hashed so that
- * reading a web page from the user's cache doesn't give access to the session key
+ * the token is based on
+ * - the user's password, so that only this user can use the token
+ * - the session IP, to reduce the blast radius of guessed passwords
+ * - a salt known only to the server, so that clients or attackers
+ * can't generate their own tokens even if they know the first two
*/
public function get_auth_token(): string
{
global $config;
- $salt = DATABASE_DSN;
- $addr = get_session_ip($config);
- return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
+ return hash("sha3-256", $this->passhash . get_session_ip($config) . SECRET);
+ }
+
+
+ public function get_session_id(): string
+ {
+ global $config;
+ return hash("sha3-256", $this->passhash . get_session_ip($config) . SECRET);
+ }
+
+ public function set_login_cookie(): void
+ {
+ global $config, $page;
+
+ $page->add_cookie(
+ "user",
+ $this->name,
+ time() + 60 * 60 * 24 * 365,
+ '/'
+ );
+ $page->add_cookie(
+ "session",
+ $this->get_session_id(),
+ time() + 60 * 60 * 24 * $config->get_int('login_memory'),
+ '/'
+ );
}
+
}
diff --git a/core/userclass.php b/core/userclass.php
index 44c2b956b..b1cdda78f 100644
--- a/core/userclass.php
+++ b/core/userclass.php
@@ -19,7 +19,7 @@ class UserClass
public static array $known_classes = [];
#[Field]
- public ?string $name = null;
+ public string $name;
public ?UserClass $parent = null;
public bool $core = false;
diff --git a/core/util.php b/core/util.php
index aa4efb598..23f3a3190 100644
--- a/core/util.php
+++ b/core/util.php
@@ -21,6 +21,23 @@ function get_theme(): string
return $theme;
}
+function get_theme_class(string $class): ?object
+{
+ $theme = ucfirst(get_theme());
+ $options = [
+ "\\Shimmie2\\$theme$class",
+ "\\Shimmie2\\Custom$class",
+ "\\Shimmie2\\$class",
+ ];
+ foreach ($options as $option) {
+ if (class_exists($option)) {
+ return new $option();
+ }
+ }
+ return null;
+}
+
+
function contact_link(?string $contact = null): ?string
{
global $config;
@@ -41,7 +58,7 @@ function contact_link(?string $contact = null): ?string
return "mailto:$text";
}
- if (str_contains($text, "/")) {
+ if (str_contains($text, "/") && mb_substr($text, 0, 1) != "/") {
return "https://$text";
}
@@ -151,21 +168,34 @@ function check_im_version(): int
function is_trusted_proxy(): bool
{
$ra = $_SERVER['REMOTE_ADDR'] ?? "0.0.0.0";
- if(!defined("TRUSTED_PROXIES")) {
+ if (!defined("TRUSTED_PROXIES")) {
return false;
}
// @phpstan-ignore-next-line - TRUSTED_PROXIES is defined in config
- foreach(TRUSTED_PROXIES as $proxy) {
- if($ra === $proxy) { // check for "unix:" before checking IPs
+ foreach (TRUSTED_PROXIES as $proxy) {
+ // @phpstan-ignore-next-line - TRUSTED_PROXIES is defined in config
+ if ($ra === $proxy) { // check for "unix:" before checking IPs
return true;
}
- if(ip_in_range($ra, $proxy)) {
+ if (ip_in_range($ra, $proxy)) {
return true;
}
}
return false;
}
+function is_bot(): bool
+{
+ $ua = $_SERVER["HTTP_USER_AGENT"] ?? "No UA";
+ return (
+ str_contains($ua, "Googlebot")
+ || str_contains($ua, "YandexBot")
+ || str_contains($ua, "bingbot")
+ || str_contains($ua, "msnbot")
+ || str_contains($ua, "PetalBot")
+ );
+}
+
/**
* Get real IP if behind a reverse proxy
*/
@@ -173,13 +203,13 @@ function get_real_ip(): string
{
$ip = $_SERVER['REMOTE_ADDR'];
- if($ip == "unix:") {
+ if ($ip == "unix:") {
$ip = "0.0.0.0";
}
- if(is_trusted_proxy()) {
+ if (is_trusted_proxy()) {
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
- if(filter_var_ex($ip, FILTER_VALIDATE_IP)) {
+ if (filter_var_ex($ip, FILTER_VALIDATE_IP)) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
}
@@ -187,7 +217,7 @@ function get_real_ip(): string
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$last_ip = $ips[count($ips) - 1];
- if(filter_var_ex($last_ip, FILTER_VALIDATE_IP)) {
+ if (filter_var_ex($last_ip, FILTER_VALIDATE_IP)) {
$ip = $last_ip;
}
}
@@ -273,44 +303,6 @@ function data_path(string $filename, bool $create = true): string
return $filename;
}
-function load_balance_url(string $tmpl, string $hash, int $n = 0): string
-{
- static $flexihashes = [];
- $matches = [];
- if (preg_match("/(.*){(.*)}(.*)/", $tmpl, $matches)) {
- $pre = $matches[1];
- $opts = $matches[2];
- $post = $matches[3];
-
- if (isset($flexihashes[$opts])) {
- $flexihash = $flexihashes[$opts];
- } else {
- $flexihash = new \Flexihash\Flexihash();
- foreach (explode(",", $opts) as $opt) {
- $parts = explode("=", $opt);
- $parts_count = count($parts);
- $opt_val = "";
- $opt_weight = 0;
- if ($parts_count === 2) {
- $opt_val = $parts[0];
- $opt_weight = (int)$parts[1];
- } elseif ($parts_count === 1) {
- $opt_val = $parts[0];
- $opt_weight = 1;
- }
- $flexihash->addTarget($opt_val, $opt_weight);
- }
- $flexihashes[$opts] = $flexihash;
- }
-
- // $choice = $flexihash->lookup($pre.$post);
- $choices = $flexihash->lookupList($hash, $n + 1); // hash doesn't change
- $choice = $choices[$n];
- $tmpl = $pre . $choice . $post;
- }
- return $tmpl;
-}
-
class FetchException extends \Exception
{
}
@@ -354,7 +346,7 @@ function fetch_url(string $url, string $mfile): array
$s_url = escapeshellarg($url);
$s_mfile = escapeshellarg($mfile);
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
- if(!file_exists($mfile)) {
+ if (!file_exists($mfile)) {
throw new FetchException("wget failed");
}
$headers = [];
@@ -549,7 +541,7 @@ function get_debug_info(): string
{
$d = get_debug_info_arr();
- $debug = "
Took {$d['time']} seconds (db:{$d['dbtime']}) and {$d['mem_mb']}MB of RAM";
+ $debug = "Took {$d['time']} seconds (db:{$d['dbtime']}) and {$d['mem_mb']}MB of RAM";
$debug .= "; Used {$d['files']} files and {$d['query_count']} queries";
$debug .= "; Sent {$d['event_count']} events";
$debug .= "; {$d['cache_hits']} cache hits and {$d['cache_misses']} misses";
@@ -622,7 +614,6 @@ function _load_theme_files(): void
{
$theme = get_theme();
require_once('themes/'.$theme.'/page.class.php');
- require_once('themes/'.$theme.'/themelet.class.php');
require_all(zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php"));
require_all(zglob('themes/'.$theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php'));
}
@@ -689,7 +680,7 @@ function _fatal_error(\Exception $e): void
$code = is_a($e, SCoreException::class) ? $e->http_code : 500;
$q = "";
- if(is_a($e, DatabaseException::class)) {
+ if (is_a($e, DatabaseException::class)) {
$q .= "Query: " . html_escape($query);
$q .= "
Args: " . html_escape(var_export($e->args, true));
}
@@ -708,7 +699,7 @@ function _fatal_error(\Exception $e): void
Message: '.html_escape($message).'
'.$q.'
Version: '.$version.' (on '.$phpver.')
-
Stack Trace:
'.$e->getTraceAsString().'
+ Stack Trace:
'.$e->getTraceAsString().'