udpate core to match master
This commit is contained in:
@@ -6,6 +6,8 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{emptyHTML,rawHTML,HTML,HEAD,BODY};
|
||||
|
||||
require_once "core/event.php";
|
||||
|
||||
enum PageMode: string
|
||||
@@ -17,6 +19,22 @@ enum PageMode: string
|
||||
case MANUAL = 'manual';
|
||||
}
|
||||
|
||||
class Cookie
|
||||
{
|
||||
public string $name;
|
||||
public string $value;
|
||||
public int $time;
|
||||
public string $path;
|
||||
|
||||
public function __construct(string $name, string $value, int $time, string $path)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->time = $time;
|
||||
$this->path = $path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Page
|
||||
*
|
||||
@@ -111,6 +129,7 @@ class BasePage
|
||||
public string $title = "";
|
||||
public string $heading = "";
|
||||
public string $subheading = "";
|
||||
public bool $left_enabled = true;
|
||||
|
||||
/** @var string[] */
|
||||
public array $html_headers = [];
|
||||
@@ -118,7 +137,7 @@ class BasePage
|
||||
/** @var string[] */
|
||||
public array $http_headers = [];
|
||||
|
||||
/** @var string[][] */
|
||||
/** @var Cookie[] */
|
||||
public array $cookies = [];
|
||||
|
||||
/** @var Block[] */
|
||||
@@ -155,6 +174,11 @@ class BasePage
|
||||
$this->flash[] = $message;
|
||||
}
|
||||
|
||||
public function disable_left(): void
|
||||
{
|
||||
$this->left_enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a line to the HTML head section.
|
||||
*/
|
||||
@@ -185,7 +209,7 @@ class BasePage
|
||||
public function add_cookie(string $name, string $value, int $time, string $path): void
|
||||
{
|
||||
$full_name = COOKIE_PREFIX . "_" . $name;
|
||||
$this->cookies[] = [$full_name, $value, $time, $path];
|
||||
$this->cookies[] = new Cookie($full_name, $value, $time, $path);
|
||||
}
|
||||
|
||||
public function get_cookie(string $name): ?string
|
||||
@@ -246,7 +270,7 @@ class BasePage
|
||||
header($head);
|
||||
}
|
||||
foreach ($this->cookies as $c) {
|
||||
setcookie($c[0], $c[1], $c[2], $c[3]);
|
||||
setcookie($c->name, $c->value, $c->time, $c->path);
|
||||
}
|
||||
} else {
|
||||
print "Error: Headers have already been sent to the client.";
|
||||
@@ -258,7 +282,7 @@ class BasePage
|
||||
*/
|
||||
public function display(): void
|
||||
{
|
||||
if ($this->mode!=PageMode::MANUAL) {
|
||||
if ($this->mode != PageMode::MANUAL) {
|
||||
$this->send_headers();
|
||||
}
|
||||
|
||||
@@ -359,8 +383,6 @@ class BasePage
|
||||
$data_href = get_base_href();
|
||||
$theme_name = $config->get_string(SetupConfig::THEME, 'default');
|
||||
|
||||
$this->add_html_header("<script type='text/javascript'>base_href = '$data_href';</script>", 40);
|
||||
|
||||
# static handler will map these to themes/foo/static/bar.ico or ext/static_files/static/bar.ico
|
||||
$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);
|
||||
@@ -371,7 +393,18 @@ class BasePage
|
||||
$config_latest = max($config_latest, filemtime($conf));
|
||||
}
|
||||
|
||||
/*** Generate CSS cache files ***/
|
||||
$css_cache_file = $this->get_css_cache_file($theme_name, $config_latest);
|
||||
$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("<script src='$data_href/$initjs_cache_file' type='text/javascript'></script>", 44);
|
||||
|
||||
$js_cache_file = $this->get_js_cache_file($theme_name, $config_latest);
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
}
|
||||
|
||||
private function get_css_cache_file(string $theme_name, int $config_latest): string
|
||||
{
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/style.css"),
|
||||
@@ -387,9 +420,8 @@ class BasePage
|
||||
foreach ($css_files as $css) {
|
||||
$mcss->addSource($css);
|
||||
}
|
||||
file_put_contents($css_cache_file, $css_data);
|
||||
$mcss->save($css_cache_file);
|
||||
}
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 43);
|
||||
|
||||
return $css_cache_file;
|
||||
}
|
||||
@@ -425,7 +457,6 @@ class BasePage
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
|
||||
"vendor/bower-asset/js-cookie/src/js.cookie.js",
|
||||
"ext/static_files/modernizr-3.3.1.custom.js",
|
||||
],
|
||||
zglob("ext/{" . Extension::get_enabled_extensions_as_string() . "}/script.js"),
|
||||
zglob("themes/$theme_name/{" . implode(",", $this->get_theme_scripts()) . "}")
|
||||
@@ -440,14 +471,14 @@ class BasePage
|
||||
foreach ($js_files as $js) {
|
||||
$mcss->addSource($js);
|
||||
}
|
||||
file_put_contents($js_cache_file, $js_data);
|
||||
$mcss->save($js_cache_file);
|
||||
}
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
|
||||
return $js_cache_file;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array A list of stylesheets relative to the theme root.
|
||||
* @return string[] A list of stylesheets relative to the theme root.
|
||||
*/
|
||||
protected function get_theme_stylesheets(): array
|
||||
{
|
||||
@@ -456,13 +487,16 @@ class BasePage
|
||||
|
||||
|
||||
/**
|
||||
* @return array A list of script files relative to the theme root.
|
||||
* @return string[] A list of script files relative to the theme root.
|
||||
*/
|
||||
protected function get_theme_scripts(): array
|
||||
{
|
||||
return ["script.js"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: NavLink[], 1: NavLink[]}
|
||||
*/
|
||||
protected function get_nav_links(): array
|
||||
{
|
||||
$pnbe = send_event(new PageNavBuildingEvent());
|
||||
@@ -472,14 +506,14 @@ class BasePage
|
||||
$active_link = null;
|
||||
// To save on event calls, we check if one of the top-level links has already been marked as active
|
||||
foreach ($nav_links as $link) {
|
||||
if ($link->active===true) {
|
||||
if ($link->active === true) {
|
||||
$active_link = $link;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$sub_links = null;
|
||||
// If one is, we just query for sub-menu options under that one tab
|
||||
if ($active_link!==null) {
|
||||
if ($active_link !== null) {
|
||||
$psnbe = send_event(new PageSubNavBuildingEvent($active_link->name));
|
||||
$sub_links = $psnbe->links;
|
||||
} else {
|
||||
@@ -489,22 +523,23 @@ class BasePage
|
||||
|
||||
// Now we check for a current link so we can identify the sub-links to show
|
||||
foreach ($psnbe->links as $sub_link) {
|
||||
if ($sub_link->active===true) {
|
||||
if ($sub_link->active === true) {
|
||||
$sub_links = $psnbe->links;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the active link has been detected, we break out
|
||||
if ($sub_links!==null) {
|
||||
if ($sub_links !== null) {
|
||||
$link->active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sub_links = $sub_links??[];
|
||||
usort($nav_links, "Shimmie2\sort_nav_links");
|
||||
usort($sub_links, "Shimmie2\sort_nav_links");
|
||||
$sub_links = $sub_links ?? [];
|
||||
|
||||
usort($nav_links, fn (NavLink $a, NavLink $b) => $a->order - $b->order);
|
||||
usort($sub_links, fn (NavLink $a, NavLink $b) => $a->order - $b->order);
|
||||
|
||||
return [$nav_links, $sub_links];
|
||||
}
|
||||
@@ -512,10 +547,9 @@ class BasePage
|
||||
/**
|
||||
* turns the Page into HTML
|
||||
*/
|
||||
public function render()
|
||||
public function render(): void
|
||||
{
|
||||
$head_html = $this->head_html();
|
||||
$body_html = $this->body_html();
|
||||
global $config, $user;
|
||||
|
||||
$head = $this->head_html();
|
||||
$body = $this->body_html();
|
||||
@@ -541,10 +575,8 @@ class BasePage
|
||||
$html_header_html = $this->get_all_html_headers();
|
||||
|
||||
return "
|
||||
<head>
|
||||
<title>{$this->title}</title>
|
||||
$html_header_html
|
||||
</head>
|
||||
<title>{$this->title}</title>
|
||||
$html_header_html
|
||||
";
|
||||
}
|
||||
|
||||
@@ -571,30 +603,23 @@ class BasePage
|
||||
}
|
||||
}
|
||||
|
||||
$wrapper = "";
|
||||
if (strlen($this->heading) > 100) {
|
||||
$wrapper = ' style="height: 3em; overflow: auto;"';
|
||||
}
|
||||
|
||||
$footer_html = $this->footer_html();
|
||||
$flash_html = $this->flash ? "<b id='flash'>".nl2br(html_escape(implode("\n", $this->flash)))."</b>" : "";
|
||||
return "
|
||||
<body>
|
||||
<header>
|
||||
<h1$wrapper>{$this->heading}</h1>
|
||||
$sub_block_html
|
||||
</header>
|
||||
<nav>
|
||||
$left_block_html
|
||||
</nav>
|
||||
<article>
|
||||
$flash_html
|
||||
$main_block_html
|
||||
</article>
|
||||
<footer>
|
||||
$footer_html
|
||||
</footer>
|
||||
</body>
|
||||
<header>
|
||||
<h1>{$this->heading}</h1>
|
||||
$sub_block_html
|
||||
</header>
|
||||
<nav>
|
||||
$left_block_html
|
||||
</nav>
|
||||
<article>
|
||||
$flash_html
|
||||
$main_block_html
|
||||
</article>
|
||||
<footer>
|
||||
$footer_html
|
||||
</footer>
|
||||
";
|
||||
}
|
||||
|
||||
@@ -609,7 +634,7 @@ class BasePage
|
||||
<a href=\"https://code.shishnet.org/shimmie2/\">Shimmie</a> ©
|
||||
<a href=\"https://www.shishnet.org/\">Shish</a> &
|
||||
<a href=\"https://github.com/shish/shimmie2/graphs/contributors\">The Team</a>
|
||||
2007-2023,
|
||||
2007-2024,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
@@ -619,9 +644,10 @@ class BasePage
|
||||
|
||||
class PageNavBuildingEvent extends Event
|
||||
{
|
||||
/** @var NavLink[] */
|
||||
public array $links = [];
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50): void
|
||||
{
|
||||
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
|
||||
}
|
||||
@@ -631,15 +657,16 @@ class PageSubNavBuildingEvent extends Event
|
||||
{
|
||||
public string $parent;
|
||||
|
||||
/** @var NavLink[] */
|
||||
public array $links = [];
|
||||
|
||||
public function __construct(string $parent)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->parent= $parent;
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string|HTMLElement $desc, ?bool $active = null, int $order = 50)
|
||||
public function add_nav_link(string $name, Link $link, string|HTMLElement $desc, ?bool $active = null, int $order = 50): void
|
||||
{
|
||||
$this->links[] = new NavLink($name, $link, $desc, $active, $order);
|
||||
}
|
||||
@@ -661,8 +688,8 @@ class NavLink
|
||||
$this->link = $link;
|
||||
$this->description = $description;
|
||||
$this->order = $order;
|
||||
if ($active==null) {
|
||||
$query = ltrim(_get_query(), "/");
|
||||
if ($active == null) {
|
||||
$query = _get_query();
|
||||
if ($query === "") {
|
||||
// This indicates the front page, so we check what's set as the front page
|
||||
$front_page = trim($config->get_string(SetupConfig::FRONT_PAGE), "/");
|
||||
@@ -672,7 +699,7 @@ class NavLink
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page], $front_page);
|
||||
}
|
||||
} elseif ($query===$link->page) {
|
||||
} elseif ($query === $link->page) {
|
||||
$this->active = true;
|
||||
} else {
|
||||
$this->active = self::is_active([$link->page]);
|
||||
@@ -682,23 +709,26 @@ class NavLink
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $pages_matched
|
||||
*/
|
||||
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.)
|
||||
*/
|
||||
$url = $url??ltrim(_get_query(), "/");
|
||||
$url = $url ?? _get_query();
|
||||
|
||||
$re1='.*?';
|
||||
$re2='((?:[a-z][a-z_]+))';
|
||||
$re1 = '.*?';
|
||||
$re2 = '((?:[a-z][a-z_]+))';
|
||||
|
||||
if (preg_match_all("/".$re1.$re2."/is", $url, $matches)) {
|
||||
$url=$matches[1][0];
|
||||
$url = $matches[1][0];
|
||||
}
|
||||
|
||||
$count_pages_matched = count($pages_matched);
|
||||
|
||||
for ($i=0; $i < $count_pages_matched; $i++) {
|
||||
for ($i = 0; $i < $count_pages_matched; $i++) {
|
||||
if ($url == $pages_matched[$i]) {
|
||||
return true;
|
||||
}
|
||||
@@ -707,8 +737,3 @@ class NavLink
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sort_nav_links(NavLink $a, NavLink $b): int
|
||||
{
|
||||
return $a->order - $b->order;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\{A,B,BR,IMG,OPTION,SELECT,emptyHTML};
|
||||
use function MicroHTML\{A,B,BR,IMG,emptyHTML,joinHTML};
|
||||
|
||||
/**
|
||||
* Class BaseThemelet
|
||||
@@ -61,11 +61,11 @@ class BaseThemelet
|
||||
}
|
||||
|
||||
$custom_classes = "";
|
||||
if (class_exists("Shimmie2\Relationships")) {
|
||||
if (property_exists($image, 'parent_id') && $image->parent_id !== null) {
|
||||
if (Extension::is_enabled(RelationshipsInfo::KEY)) {
|
||||
if ($image['parent_id'] !== null) {
|
||||
$custom_classes .= "shm-thumb-has_parent ";
|
||||
}
|
||||
if (property_exists($image, 'has_children') && bool_escape($image->has_children)) {
|
||||
if ($image['has_children']) {
|
||||
$custom_classes .= "shm-thumb-has_child ";
|
||||
}
|
||||
}
|
||||
@@ -84,29 +84,21 @@ class BaseThemelet
|
||||
}
|
||||
|
||||
return A(
|
||||
[
|
||||
"href"=>$view_link,
|
||||
"class"=>"thumb shm-thumb shm-thumb-link $custom_classes",
|
||||
"data-tags"=>$tags,
|
||||
"data-height"=>$image->height,
|
||||
"data-width"=>$image->width,
|
||||
"data-mime"=>$image->get_mime(),
|
||||
"data-post-id"=>$id,
|
||||
],
|
||||
$attrs,
|
||||
IMG(
|
||||
[
|
||||
"id"=>"thumb_$id",
|
||||
"title"=>$tip,
|
||||
"alt"=>$tip,
|
||||
"height"=>$tsize[1],
|
||||
"width"=>$tsize[0],
|
||||
"src"=>$thumb_link,
|
||||
"id" => "thumb_$id",
|
||||
"title" => $tip,
|
||||
"alt" => $tip,
|
||||
"height" => $tsize[1],
|
||||
"width" => $tsize[0],
|
||||
"src" => $thumb_link,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false)
|
||||
public function display_paginator(Page $page, string $base, ?string $query, int $page_number, int $total_pages, bool $show_random = false): void
|
||||
{
|
||||
if ($total_pages == 0) {
|
||||
$total_pages = 1;
|
||||
@@ -116,18 +108,18 @@ class BaseThemelet
|
||||
|
||||
$page->add_html_header("<link rel='first' href='".make_http(make_link($base.'/1', $query))."'>");
|
||||
if ($page_number < $total_pages) {
|
||||
$page->add_html_header("<link rel='prefetch' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
|
||||
$page->add_html_header("<link rel='next' href='".make_http(make_link($base.'/'.($page_number+1), $query))."'>");
|
||||
$page->add_html_header("<link rel='prefetch' href='".make_http(make_link($base.'/'.($page_number + 1), $query))."'>");
|
||||
$page->add_html_header("<link rel='next' href='".make_http(make_link($base.'/'.($page_number + 1), $query))."'>");
|
||||
}
|
||||
if ($page_number > 1) {
|
||||
$page->add_html_header("<link rel='previous' href='".make_http(make_link($base.'/'.($page_number-1), $query))."'>");
|
||||
$page->add_html_header("<link rel='previous' href='".make_http(make_link($base.'/'.($page_number - 1), $query))."'>");
|
||||
}
|
||||
$page->add_html_header("<link rel='last' href='".make_http(make_link($base.'/'.$total_pages, $query))."'>");
|
||||
}
|
||||
|
||||
private function gen_page_link(string $base_url, ?string $query, int $page, string $name): HTMLElement
|
||||
{
|
||||
return A(["href"=>make_link($base_url.'/'.$page, $query)], $name);
|
||||
return A(["href" => make_link($base_url.'/'.$page, $query)], $name);
|
||||
}
|
||||
|
||||
private function gen_page_link_block(string $base_url, ?string $query, int $page, int $current_page, string $name): HTMLElement
|
||||
@@ -139,19 +131,6 @@ class BaseThemelet
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
protected function implode(string|HTMLElement $glue, array $pieces): HTMLElement
|
||||
{
|
||||
$out = emptyHTML();
|
||||
$n = 0;
|
||||
foreach ($pieces as $piece) {
|
||||
if ($n++ > 0) {
|
||||
$out->appendChild($glue);
|
||||
}
|
||||
$out->appendChild($piece);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function build_paginator(int $current_page, int $total_pages, string $base_url, ?string $query, bool $show_random): HTMLElement
|
||||
{
|
||||
$next = $current_page + 1;
|
||||
@@ -179,10 +158,10 @@ class BaseThemelet
|
||||
foreach (range($start, $end) as $i) {
|
||||
$pages[] = $this->gen_page_link_block($base_url, $query, $i, $current_page, (string)$i);
|
||||
}
|
||||
$pages_html = $this->implode(" | ", $pages);
|
||||
$pages_html = joinHTML(" | ", $pages);
|
||||
|
||||
return emptyHTML(
|
||||
$this->implode(" | ", [
|
||||
joinHTML(" | ", [
|
||||
$first_html,
|
||||
$prev_html,
|
||||
$random_html,
|
||||
|
||||
@@ -45,7 +45,7 @@ class Block
|
||||
*/
|
||||
public bool $is_content = true;
|
||||
|
||||
public function __construct(string $header=null, string|\MicroHTML\HTMLElement $body=null, string $section="main", int $position=50, string $id=null)
|
||||
public function __construct(string $header = null, string|\MicroHTML\HTMLElement $body = null, string $section = "main", int $position = 50, string $id = null)
|
||||
{
|
||||
$this->header = $header;
|
||||
$this->body = (string)$body;
|
||||
@@ -63,7 +63,7 @@ class Block
|
||||
/**
|
||||
* Get the HTML for this block.
|
||||
*/
|
||||
public function get_html(bool $hidable=false): string
|
||||
public function get_html(bool $hidable = false): string
|
||||
{
|
||||
$h = $this->header;
|
||||
$b = $this->body;
|
||||
|
||||
@@ -10,8 +10,8 @@ class EventTracingCache implements CacheInterface
|
||||
{
|
||||
private CacheInterface $engine;
|
||||
private \EventTracer $tracer;
|
||||
private int $hits=0;
|
||||
private int $misses=0;
|
||||
private int $hits = 0;
|
||||
private int $misses = 0;
|
||||
|
||||
public function __construct(CacheInterface $engine, \EventTracer $tracer)
|
||||
{
|
||||
@@ -19,7 +19,7 @@ class EventTracingCache implements CacheInterface
|
||||
$this->tracer = $tracer;
|
||||
}
|
||||
|
||||
public function get($key, $default=null)
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
if ($key === "__etc_cache_hits") {
|
||||
return $this->hits;
|
||||
@@ -29,7 +29,7 @@ class EventTracingCache implements CacheInterface
|
||||
}
|
||||
|
||||
$sentinel = "__etc_sentinel";
|
||||
$this->tracer->begin("Cache Get", ["key"=>$key]);
|
||||
$this->tracer->begin("Cache Get", ["key" => $key]);
|
||||
$val = $this->engine->get($key, $sentinel);
|
||||
if ($val != $sentinel) {
|
||||
$res = "hit";
|
||||
@@ -39,13 +39,13 @@ class EventTracingCache implements CacheInterface
|
||||
$val = $default;
|
||||
$this->misses++;
|
||||
}
|
||||
$this->tracer->end(null, ["result"=>$res]);
|
||||
$this->tracer->end(null, ["result" => $res]);
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function set($key, $value, $ttl = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Set", ["key"=>$key, "ttl"=>$ttl]);
|
||||
$this->tracer->begin("Cache Set", ["key" => $key, "ttl" => $ttl]);
|
||||
$val = $this->engine->set($key, $value, $ttl);
|
||||
$this->tracer->end();
|
||||
return $val;
|
||||
@@ -53,7 +53,7 @@ class EventTracingCache implements CacheInterface
|
||||
|
||||
public function delete($key)
|
||||
{
|
||||
$this->tracer->begin("Cache Delete", ["key"=>$key]);
|
||||
$this->tracer->begin("Cache Delete", ["key" => $key]);
|
||||
$val = $this->engine->delete($key);
|
||||
$this->tracer->end();
|
||||
return $val;
|
||||
@@ -67,6 +67,11 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param mixed $default
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
public function getMultiple($keys, $default = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Get Multiple", ["keys" => $keys]);
|
||||
@@ -75,6 +80,9 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null)
|
||||
{
|
||||
$this->tracer->begin("Cache Set Multiple", ["keys" => array_keys($values)]);
|
||||
@@ -83,6 +91,9 @@ class EventTracingCache implements CacheInterface
|
||||
return $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
*/
|
||||
public function deleteMultiple($keys)
|
||||
{
|
||||
$this->tracer->begin("Cache Delete Multiple", ["keys" => $keys]);
|
||||
@@ -93,16 +104,15 @@ class EventTracingCache implements CacheInterface
|
||||
|
||||
public function has($key)
|
||||
{
|
||||
$this->tracer->begin("Cache Has", ["key"=>$key]);
|
||||
$this->tracer->begin("Cache Has", ["key" => $key]);
|
||||
$val = $this->engine->has($key);
|
||||
$this->tracer->end(null, ["exists"=>$val]);
|
||||
$this->tracer->end(null, ["exists" => $val]);
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCache(?string $dsn): CacheInterface
|
||||
{
|
||||
$matches = [];
|
||||
$c = null;
|
||||
if ($dsn && !isset($_GET['DISABLE_CACHE'])) {
|
||||
$url = parse_url($dsn);
|
||||
|
||||
66
core/cli_app.php
Normal file
66
core/cli_app.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use Symfony\Component\Console\Input\{ArgvInput,InputOption,InputDefinition,InputInterface};
|
||||
use Symfony\Component\Console\Output\{OutputInterface,ConsoleOutput};
|
||||
|
||||
class CliApp extends \Symfony\Component\Console\Application
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Shimmie', VERSION);
|
||||
$this->setAutoExit(false);
|
||||
}
|
||||
|
||||
protected function getDefaultInputDefinition(): InputDefinition
|
||||
{
|
||||
$definition = parent::getDefaultInputDefinition();
|
||||
$definition->addOption(new InputOption(
|
||||
'--user',
|
||||
'-u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Log in as the given user'
|
||||
));
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null): int
|
||||
{
|
||||
global $user;
|
||||
|
||||
$input ??= new ArgvInput();
|
||||
$output ??= new ConsoleOutput();
|
||||
|
||||
if ($input->hasParameterOption(['--user', '-u'])) {
|
||||
$name = $input->getParameterOption(['--user', '-u']);
|
||||
$user = User::by_name($name);
|
||||
if (is_null($user)) {
|
||||
die("Unknown user '$name'\n");
|
||||
} else {
|
||||
send_event(new UserLoginEvent($user));
|
||||
}
|
||||
}
|
||||
|
||||
$log_level = SCORE_LOG_WARNING;
|
||||
if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
|
||||
$log_level = SCORE_LOG_ERROR;
|
||||
} else {
|
||||
if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_DEBUG;
|
||||
} elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_DEBUG;
|
||||
} elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
|
||||
$log_level = SCORE_LOG_INFO;
|
||||
}
|
||||
}
|
||||
if (!defined("CLI_LOG_LEVEL")) {
|
||||
define("CLI_LOG_LEVEL", $log_level);
|
||||
}
|
||||
|
||||
return parent::run($input, $output);
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,12 @@ namespace Shimmie2;
|
||||
class CommandBuilder
|
||||
{
|
||||
private string $executable;
|
||||
/** @var string[] */
|
||||
private array $args = [];
|
||||
/** @var string[] */
|
||||
public array $output;
|
||||
|
||||
public function __construct(String $executable)
|
||||
public function __construct(string $executable)
|
||||
{
|
||||
if (empty($executable)) {
|
||||
throw new \InvalidArgumentException("executable cannot be empty");
|
||||
|
||||
109
core/config.php
109
core/config.php
@@ -16,7 +16,7 @@ interface Config
|
||||
* so that the next time a page is loaded it will use the new
|
||||
* configuration.
|
||||
*/
|
||||
public function save(string $name=null): void;
|
||||
public function save(string $name = null): void;
|
||||
|
||||
//@{ /*--------------------------------- SET ------------------------------------------------------*/
|
||||
/**
|
||||
@@ -41,6 +41,8 @@ interface Config
|
||||
|
||||
/**
|
||||
* Set a configuration option to a new value, regardless of what the value is at the moment.
|
||||
*
|
||||
* @param mixed[] $value
|
||||
*/
|
||||
public function set_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
@@ -93,6 +95,8 @@ interface Config
|
||||
* This has the advantage that the values will show up in the "advanced" setup
|
||||
* page where they can be modified, while calling get_* with a "default"
|
||||
* parameter won't show up.
|
||||
*
|
||||
* @param mixed[] $value
|
||||
*/
|
||||
public function set_default_array(string $name, array $value): void;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
@@ -101,27 +105,30 @@ interface Config
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_int(string $name, ?int $default=null): ?int;
|
||||
public function get_int(string $name, ?int $default = null): ?int;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_float(string $name, ?float $default=null): ?float;
|
||||
public function get_float(string $name, ?float $default = null): ?float;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_string(string $name, ?string $default=null): ?string;
|
||||
public function get_string(string $name, ?string $default = null): ?string;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*/
|
||||
public function get_bool(string $name, ?bool $default=null): ?bool;
|
||||
public function get_bool(string $name, ?bool $default = null): ?bool;
|
||||
|
||||
/**
|
||||
* Pick a value out of the table by name, cast to the appropriate data type.
|
||||
*
|
||||
* @param mixed[] $default
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function get_array(string $name, ?array $default=[]): ?array;
|
||||
public function get_array(string $name, ?array $default = []): ?array;
|
||||
//@} /*--------------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
@@ -134,6 +141,7 @@ interface Config
|
||||
*/
|
||||
abstract class BaseConfig implements Config
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
public array $values = [];
|
||||
|
||||
public function set_int(string $name, ?int $value): void
|
||||
@@ -162,7 +170,7 @@ abstract class BaseConfig implements Config
|
||||
|
||||
public function set_array(string $name, ?array $value): void
|
||||
{
|
||||
if ($value!=null) {
|
||||
if ($value != null) {
|
||||
$this->values[$name] = implode(",", $value);
|
||||
} else {
|
||||
$this->values[$name] = null;
|
||||
@@ -205,17 +213,32 @@ abstract class BaseConfig implements Config
|
||||
}
|
||||
}
|
||||
|
||||
public function get_int(string $name, ?int $default=null): ?int
|
||||
/**
|
||||
* @template T of int|null
|
||||
* @param T $default
|
||||
* @return T|int
|
||||
*/
|
||||
public function get_int(string $name, ?int $default = null): ?int
|
||||
{
|
||||
return (int)($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_float(string $name, ?float $default=null): ?float
|
||||
/**
|
||||
* @template T of float|null
|
||||
* @param T $default
|
||||
* @return T|float
|
||||
*/
|
||||
public function get_float(string $name, ?float $default = null): ?float
|
||||
{
|
||||
return (float)($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_string(string $name, ?string $default=null): ?string
|
||||
/**
|
||||
* @template T of string|null
|
||||
* @param T $default
|
||||
* @return T|string
|
||||
*/
|
||||
public function get_string(string $name, ?string $default = null): ?string
|
||||
{
|
||||
$val = $this->get($name, $default);
|
||||
if (!is_string($val) && !is_null($val)) {
|
||||
@@ -224,12 +247,22 @@ abstract class BaseConfig implements Config
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function get_bool(string $name, ?bool $default=null): ?bool
|
||||
/**
|
||||
* @template T of bool|null
|
||||
* @param T $default
|
||||
* @return T|bool
|
||||
*/
|
||||
public function get_bool(string $name, ?bool $default = null): ?bool
|
||||
{
|
||||
return bool_escape($this->get($name, $default));
|
||||
}
|
||||
|
||||
public function get_array(string $name, ?array $default=[]): ?array
|
||||
/**
|
||||
* @template T of array<string>|null
|
||||
* @param T $default
|
||||
* @return T|array<string>
|
||||
*/
|
||||
public function get_array(string $name, ?array $default = null): ?array
|
||||
{
|
||||
$val = $this->get($name);
|
||||
if (is_null($val)) {
|
||||
@@ -241,7 +274,7 @@ abstract class BaseConfig implements Config
|
||||
return explode(",", $val);
|
||||
}
|
||||
|
||||
private function get(string $name, $default=null)
|
||||
private function get(string $name, mixed $default = null): mixed
|
||||
{
|
||||
if (isset($this->values[$name])) {
|
||||
return $this->values[$name];
|
||||
@@ -285,30 +318,30 @@ class DatabaseConfig extends BaseConfig
|
||||
$this->table_name = $table_name;
|
||||
$this->sub_value = $sub_value;
|
||||
$this->sub_column = $sub_column;
|
||||
$this->cache_name = empty($sub_value) ? "config" : "config_{$sub_value}";
|
||||
|
||||
$cached = $cache->get($this->cache_name);
|
||||
if (!is_null($cached)) {
|
||||
$this->values = $cached;
|
||||
} else {
|
||||
$this->values = [];
|
||||
|
||||
$query = "SELECT name, value FROM {$this->table_name}";
|
||||
$args = [];
|
||||
|
||||
if (!empty($sub_column)&&!empty($sub_value)) {
|
||||
$query .= " WHERE $sub_column = :sub_value";
|
||||
$args["sub_value"] = $sub_value;
|
||||
}
|
||||
|
||||
foreach ($this->database->get_all($query, $args) as $row) {
|
||||
$this->values[$row["name"]] = $row["value"];
|
||||
}
|
||||
$cache->set($this->cache_name, $this->values);
|
||||
}
|
||||
$this->cache_name = empty($sub_value) ? "config" : "config_{$sub_column}_{$sub_value}";
|
||||
$this->values = cache_get_or_set($this->cache_name, fn () => $this->get_values());
|
||||
}
|
||||
|
||||
public function save(string $name=null): void
|
||||
private function get_values(): mixed
|
||||
{
|
||||
$values = [];
|
||||
|
||||
$query = "SELECT name, value FROM {$this->table_name}";
|
||||
$args = [];
|
||||
|
||||
if (!empty($this->sub_column) && !empty($this->sub_value)) {
|
||||
$query .= " WHERE {$this->sub_column} = :sub_value";
|
||||
$args["sub_value"] = $this->sub_value;
|
||||
}
|
||||
|
||||
foreach ($this->database->get_all($query, $args) as $row) {
|
||||
$values[$row["name"]] = $row["value"];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function save(string $name = null): void
|
||||
{
|
||||
global $cache;
|
||||
|
||||
@@ -319,10 +352,10 @@ class DatabaseConfig extends BaseConfig
|
||||
}
|
||||
} else {
|
||||
$query = "DELETE FROM {$this->table_name} WHERE name = :name";
|
||||
$args = ["name"=>$name];
|
||||
$args = ["name" => $name];
|
||||
$cols = ["name","value"];
|
||||
$params = [":name",":value"];
|
||||
if (!empty($this->sub_column)&&!empty($this->sub_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;
|
||||
@@ -331,7 +364,7 @@ class DatabaseConfig extends BaseConfig
|
||||
|
||||
$this->database->execute($query, $args);
|
||||
|
||||
$args["value"] =$this->values[$name];
|
||||
$args["value"] = $this->values[$name];
|
||||
$this->database->execute(
|
||||
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
|
||||
$args
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Shimmie2;
|
||||
use FFSPHP\PDO;
|
||||
use FFSPHP\PDOStatement;
|
||||
|
||||
require_once __DIR__ . '/exceptions.php';
|
||||
|
||||
enum DatabaseDriverID: string
|
||||
{
|
||||
case MYSQL = "mysql";
|
||||
@@ -14,8 +16,28 @@ enum DatabaseDriverID: string
|
||||
case SQLITE = "sqlite";
|
||||
}
|
||||
|
||||
class DatabaseException extends SCoreException
|
||||
{
|
||||
public string $query;
|
||||
/** @var array<string, mixed> */
|
||||
public array $args;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
public function __construct(string $msg, string $query, array $args)
|
||||
{
|
||||
parent::__construct($msg);
|
||||
$this->error = $msg;
|
||||
$this->query = $query;
|
||||
$this->args = $args;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*
|
||||
* @phpstan-type QueryArgs array<string, string|int|bool|null>
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
@@ -36,6 +58,7 @@ class Database
|
||||
* How many queries this DB object has run
|
||||
*/
|
||||
public int $query_count = 0;
|
||||
/** @var string[] */
|
||||
public array $queries = [];
|
||||
|
||||
public function __construct(string $dsn)
|
||||
@@ -57,7 +80,7 @@ class Database
|
||||
private function connect_engine(): void
|
||||
{
|
||||
if (preg_match("/^([^:]*)/", $this->dsn, $matches)) {
|
||||
$db_proto=$matches[1];
|
||||
$db_proto = $matches[1];
|
||||
} else {
|
||||
throw new ServerError("Can't figure out database engine");
|
||||
}
|
||||
@@ -106,6 +129,28 @@ class Database
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param callable():T $callback
|
||||
* @return T
|
||||
*/
|
||||
public function with_savepoint(callable $callback, string $name = "sp"): mixed
|
||||
{
|
||||
global $_tracer;
|
||||
try {
|
||||
$_tracer->begin("Savepoint $name");
|
||||
$this->execute("SAVEPOINT $name");
|
||||
$ret = $callback();
|
||||
$this->execute("RELEASE SAVEPOINT $name");
|
||||
$_tracer->end();
|
||||
return $ret;
|
||||
} catch (\Exception $e) {
|
||||
$this->execute("ROLLBACK TO SAVEPOINT $name");
|
||||
$_tracer->end();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function get_engine(): DBEngine
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
@@ -129,16 +174,18 @@ class Database
|
||||
return $this->get_engine()->get_version($this->get_db());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
private function count_time(string $method, float $start, string $query, ?array $args): void
|
||||
{
|
||||
global $_tracer, $tracer_enabled;
|
||||
$dur = ftime() - $start;
|
||||
// trim whitespace
|
||||
$query = preg_replace('/[\n\t ]/m', ' ', $query);
|
||||
$query = preg_replace('/ +/m', ' ', $query);
|
||||
$query = preg_replace('/[\n\t ]+/m', ' ', $query);
|
||||
$query = trim($query);
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
|
||||
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query" => $query, "args" => $args, "method" => $method]);
|
||||
}
|
||||
$this->queries[] = $query;
|
||||
$this->query_count++;
|
||||
@@ -150,31 +197,32 @@ class Database
|
||||
$this->get_engine()->set_timeout($this->get_db(), $time);
|
||||
}
|
||||
|
||||
public function notify(string $channel, ?string $data=null): void
|
||||
public function notify(string $channel, ?string $data = null): void
|
||||
{
|
||||
$this->get_engine()->notify($this->get_db(), $channel, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function _execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
try {
|
||||
$ret = $this->get_db()->execute(
|
||||
"-- " . str_replace("%2F", "/", urlencode($_GET['q'] ?? '')). "\n" .
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? "unknown uri";
|
||||
return $this->get_db()->execute(
|
||||
"-- $uri\n" .
|
||||
$query,
|
||||
$args
|
||||
);
|
||||
if ($ret === false) {
|
||||
throw new SCoreException("Query failed", $query);
|
||||
}
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $ret;
|
||||
} catch (\PDOException $pdoe) {
|
||||
throw new SCoreException($pdoe->getMessage(), $query);
|
||||
throw new DatabaseException($pdoe->getMessage(), $query, $args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query with no return
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
@@ -186,6 +234,9 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<array<string, mixed>>
|
||||
*/
|
||||
public function get_all(string $query, array $args = []): array
|
||||
{
|
||||
@@ -197,6 +248,8 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a iterable object for use with generators.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_all_iterable(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
@@ -208,6 +261,9 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_row(string $query, array $args = []): ?array
|
||||
{
|
||||
@@ -219,6 +275,9 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function get_col(string $query, array $args = []): array
|
||||
{
|
||||
@@ -230,6 +289,8 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row as a single iterable object.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_col_iterable(string $query, array $args = []): \Generator
|
||||
{
|
||||
@@ -243,6 +304,9 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get_pairs(string $query, array $args = []): array
|
||||
{
|
||||
@@ -255,6 +319,8 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column as an iterable object.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_pairs_iterable(string $query, array $args = []): \Generator
|
||||
{
|
||||
@@ -268,8 +334,10 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value, or null.
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function get_one(string $query, array $args = [])
|
||||
public function get_one(string $query, array $args = []): mixed
|
||||
{
|
||||
$_start = ftime();
|
||||
$row = $this->_execute($query, $args)->fetch();
|
||||
@@ -279,13 +347,15 @@ class Database
|
||||
|
||||
/**
|
||||
* Execute an SQL query and returns a bool indicating if any data was returned
|
||||
*
|
||||
* @param QueryArgs $args
|
||||
*/
|
||||
public function exists(string $query, array $args = []): bool
|
||||
{
|
||||
$_start = ftime();
|
||||
$row = $this->_execute($query, $args)->fetch();
|
||||
$this->count_time("exists", $_start, $query, $args);
|
||||
if ($row==null) {
|
||||
if ($row == null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -345,7 +415,7 @@ class Database
|
||||
return $this->get_db();
|
||||
}
|
||||
|
||||
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void
|
||||
public function standardise_boolean(string $table, string $column, bool $include_postgres = false): void
|
||||
{
|
||||
$d = $this->get_driver_id();
|
||||
if ($d == DatabaseDriverID::MYSQL) {
|
||||
|
||||
@@ -16,7 +16,7 @@ abstract class DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id;
|
||||
|
||||
public function init(PDO $db)
|
||||
public function init(PDO $db): void
|
||||
{
|
||||
}
|
||||
|
||||
@@ -30,18 +30,18 @@ abstract class DBEngine
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
|
||||
abstract public function set_timeout(PDO $db, ?int $time);
|
||||
abstract public function set_timeout(PDO $db, ?int $time): void;
|
||||
|
||||
abstract public function get_version(PDO $db): string;
|
||||
|
||||
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
|
||||
abstract public function notify(PDO $db, string $channel, ?string $data = null): void;
|
||||
}
|
||||
|
||||
class MySQL extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::MYSQL;
|
||||
|
||||
public function init(PDO $db)
|
||||
public function init(PDO $db): void
|
||||
{
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class MySQL extends DBEngine
|
||||
// $db->exec("SET SESSION MAX_EXECUTION_TIME=".$time.";");
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
{
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class PostgreSQL extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::PGSQL;
|
||||
|
||||
public function init(PDO $db)
|
||||
public function init(PDO $db): void
|
||||
{
|
||||
$addr = array_key_exists('REMOTE_ADDR', $_SERVER) ? get_real_ip() : 'local';
|
||||
$db->exec("SET application_name TO 'shimmie [$addr]';");
|
||||
@@ -110,7 +110,7 @@ class PostgreSQL extends DBEngine
|
||||
$db->exec("SET statement_timeout TO ".$time.";");
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
{
|
||||
if ($data) {
|
||||
$db->exec("NOTIFY $channel, '$data';");
|
||||
@@ -126,7 +126,7 @@ class PostgreSQL extends DBEngine
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date): int
|
||||
function _unix_timestamp(string $date): int
|
||||
{
|
||||
return \Safe\strtotime($date);
|
||||
}
|
||||
@@ -134,31 +134,31 @@ function _now(): string
|
||||
{
|
||||
return date("Y-m-d H:i:s");
|
||||
}
|
||||
function _floor($a): float
|
||||
function _floor(float|int $a): float
|
||||
{
|
||||
return floor($a);
|
||||
}
|
||||
function _log($a, $b=null): float
|
||||
function _log(float $a, ?float $b = null): float
|
||||
{
|
||||
if (is_null($b)) {
|
||||
return log($a);
|
||||
} else {
|
||||
return log($a, $b);
|
||||
return log($b, $a);
|
||||
}
|
||||
}
|
||||
function _isnull($a): bool
|
||||
function _isnull(mixed $a): bool
|
||||
{
|
||||
return is_null($a);
|
||||
}
|
||||
function _md5($a): string
|
||||
function _md5(string $a): string
|
||||
{
|
||||
return md5($a);
|
||||
}
|
||||
function _concat($a, $b): string
|
||||
function _concat(string $a, string $b): string
|
||||
{
|
||||
return $a . $b;
|
||||
}
|
||||
function _lower($a): string
|
||||
function _lower(string $a): string
|
||||
{
|
||||
return strtolower($a);
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function _rand(): int
|
||||
{
|
||||
return rand();
|
||||
}
|
||||
function _ln($n): float
|
||||
function _ln(float $n): float
|
||||
{
|
||||
return log($n);
|
||||
}
|
||||
@@ -175,7 +175,7 @@ class SQLite extends DBEngine
|
||||
{
|
||||
public DatabaseDriverID $id = DatabaseDriverID::SQLITE;
|
||||
|
||||
public function init(PDO $db)
|
||||
public function init(PDO $db): void
|
||||
{
|
||||
ini_set('sqlite.assoc_case', '0');
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
@@ -222,7 +222,7 @@ class SQLite extends DBEngine
|
||||
// There doesn't seem to be such a thing for SQLite, so it does nothing
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
public function notify(PDO $db, string $channel, ?string $data = null): void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ class PageRequestEvent extends Event
|
||||
parent::__construct();
|
||||
global $config;
|
||||
|
||||
// trim starting slashes
|
||||
$path = ltrim($path, "/");
|
||||
$this->method = $method;
|
||||
|
||||
// if path is not specified, use the default front page
|
||||
if (empty($path)) { /* empty is faster than strlen */
|
||||
// if we're looking at the root of the install,
|
||||
// use the default front page
|
||||
if ($path == "") {
|
||||
$path = $config->get_string(SetupConfig::FRONT_PAGE);
|
||||
}
|
||||
$this->path = $path;
|
||||
@@ -258,73 +258,12 @@ class PageRequestEvent extends Event
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sent when index.php is called from the command line
|
||||
*/
|
||||
class CommandEvent extends Event
|
||||
class CliGenEvent extends Event
|
||||
{
|
||||
public string $cmd = "help";
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public array $args = [];
|
||||
|
||||
/**
|
||||
* #param string[] $args
|
||||
*/
|
||||
public function __construct(array $args)
|
||||
{
|
||||
public function __construct(
|
||||
public \Symfony\Component\Console\Application $app
|
||||
) {
|
||||
parent::__construct();
|
||||
global $user;
|
||||
|
||||
$opts = [];
|
||||
$log_level = SCORE_LOG_WARNING;
|
||||
$arg_count = count($args);
|
||||
|
||||
for ($i=1; $i<$arg_count; $i++) {
|
||||
switch ($args[$i]) {
|
||||
case '-u':
|
||||
$user = User::by_name($args[++$i]);
|
||||
if (is_null($user)) {
|
||||
die("Unknown user");
|
||||
} else {
|
||||
send_event(new UserLoginEvent($user));
|
||||
}
|
||||
break;
|
||||
case '-q':
|
||||
$log_level += 10;
|
||||
break;
|
||||
case '-v':
|
||||
$log_level -= 10;
|
||||
break;
|
||||
default:
|
||||
$opts[] = $args[$i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined("CLI_LOG_LEVEL")) {
|
||||
define("CLI_LOG_LEVEL", $log_level);
|
||||
}
|
||||
|
||||
if (count($opts) > 0) {
|
||||
$this->cmd = $opts[0];
|
||||
$this->args = array_slice($opts, 1);
|
||||
} else {
|
||||
print "\n";
|
||||
print "Usage: php {$args[0]} [flags] [command]\n";
|
||||
print "\n";
|
||||
print "Flags:\n";
|
||||
print "\t-u [username]\n";
|
||||
print "\t\tLog in as the specified user\n";
|
||||
print "\t-q / -v\n";
|
||||
print "\t\tBe quieter / more verbose\n";
|
||||
print "\t\tScale is debug - info - warning - error - critical\n";
|
||||
print "\t\tDefault is to show warnings and above\n";
|
||||
print "\n";
|
||||
print "Currently known commands:\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,13 @@ namespace Shimmie2;
|
||||
*/
|
||||
class SCoreException extends \RuntimeException
|
||||
{
|
||||
public ?string $query;
|
||||
public string $error;
|
||||
public int $http_code = 500;
|
||||
|
||||
public function __construct(string $msg, ?string $query=null)
|
||||
public function __construct(string $msg)
|
||||
{
|
||||
parent::__construct($msg);
|
||||
$this->error = $msg;
|
||||
$this->query = $query;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@ abstract class Extension
|
||||
protected Themelet $theme;
|
||||
public ExtensionInfo $info;
|
||||
|
||||
/** @var string[] */
|
||||
private static array $enabled_extensions = [];
|
||||
|
||||
public function __construct($class = null)
|
||||
public function __construct(?string $class = null)
|
||||
{
|
||||
$class = $class ?? get_called_class();
|
||||
$this->theme = $this->get_theme_object($class);
|
||||
@@ -42,9 +43,13 @@ abstract class Extension
|
||||
$normal = "Shimmie2\\{$base}Theme";
|
||||
|
||||
if (class_exists($custom)) {
|
||||
return new $custom();
|
||||
$c = new $custom();
|
||||
assert(is_a($c, Themelet::class));
|
||||
return $c;
|
||||
} elseif (class_exists($normal)) {
|
||||
return new $normal();
|
||||
$n = new $normal();
|
||||
assert(is_a($n, Themelet::class));
|
||||
return $n;
|
||||
} else {
|
||||
return new Themelet();
|
||||
}
|
||||
@@ -69,7 +74,7 @@ abstract class Extension
|
||||
$extras
|
||||
) as $key) {
|
||||
$ext = ExtensionInfo::get_by_key($key);
|
||||
if ($ext===null || !$ext->is_supported()) {
|
||||
if ($ext === null || !$ext->is_supported()) {
|
||||
continue;
|
||||
}
|
||||
// FIXME: error if one of our dependencies isn't supported
|
||||
@@ -82,11 +87,14 @@ abstract class Extension
|
||||
}
|
||||
}
|
||||
|
||||
public static function is_enabled(string $key): ?bool
|
||||
public static function is_enabled(string $key): bool
|
||||
{
|
||||
return in_array($key, self::$enabled_extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_enabled_extensions(): array
|
||||
{
|
||||
return self::$enabled_extensions;
|
||||
@@ -102,7 +110,7 @@ abstract class Extension
|
||||
return $config->get_int($name, 0);
|
||||
}
|
||||
|
||||
protected function set_version(string $name, int $ver)
|
||||
protected function set_version(string $name, int $ver): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_int($name, $ver);
|
||||
@@ -110,6 +118,10 @@ abstract class Extension
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionNotFound extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
enum ExtensionVisibility
|
||||
{
|
||||
case DEFAULT;
|
||||
@@ -135,7 +147,7 @@ abstract class ExtensionInfo
|
||||
public const SHISH_NAME = "Shish";
|
||||
public const SHISH_EMAIL = "webmaster@shishnet.org";
|
||||
public const SHIMMIE_URL = "https://code.shishnet.org/shimmie2/";
|
||||
public const SHISH_AUTHOR = [self::SHISH_NAME=>self::SHISH_EMAIL];
|
||||
public const SHISH_AUTHOR = [self::SHISH_NAME => self::SHISH_EMAIL];
|
||||
|
||||
public const LICENSE_GPLV2 = "GPLv2";
|
||||
public const LICENSE_MIT = "MIT";
|
||||
@@ -149,8 +161,11 @@ abstract class ExtensionInfo
|
||||
public string $name;
|
||||
public string $license;
|
||||
public string $description;
|
||||
/** @var array<string, string|null> */
|
||||
public array $authors = [];
|
||||
/** @var string[] */
|
||||
public array $dependencies = [];
|
||||
/** @var string[] */
|
||||
public array $conflicts = [];
|
||||
public ExtensionVisibility $visibility = ExtensionVisibility::DEFAULT;
|
||||
public ExtensionCategory $category = ExtensionCategory::GENERAL;
|
||||
@@ -164,7 +179,7 @@ abstract class ExtensionInfo
|
||||
|
||||
public function is_supported(): bool
|
||||
{
|
||||
if ($this->supported===null) {
|
||||
if ($this->supported === null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->supported;
|
||||
@@ -172,14 +187,17 @@ abstract class ExtensionInfo
|
||||
|
||||
public function get_support_info(): string
|
||||
{
|
||||
if ($this->supported===null) {
|
||||
if ($this->supported === null) {
|
||||
$this->check_support();
|
||||
}
|
||||
return $this->support_info;
|
||||
}
|
||||
|
||||
/** @var array<string, ExtensionInfo> */
|
||||
private static array $all_info_by_key = [];
|
||||
/** @var array<string, ExtensionInfo> */
|
||||
private static array $all_info_by_class = [];
|
||||
/** @var string[] */
|
||||
private static array $core_extensions = [];
|
||||
|
||||
protected function __construct()
|
||||
@@ -196,7 +214,7 @@ abstract class ExtensionInfo
|
||||
return Extension::is_enabled($this->key);
|
||||
}
|
||||
|
||||
private function check_support()
|
||||
private function check_support(): void
|
||||
{
|
||||
global $database;
|
||||
$this->support_info = "";
|
||||
@@ -215,16 +233,25 @@ abstract class ExtensionInfo
|
||||
$this->supported = empty($this->support_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ExtensionInfo[]
|
||||
*/
|
||||
public static function get_all(): array
|
||||
{
|
||||
return array_values(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_keys(): array
|
||||
{
|
||||
return array_keys(self::$all_info_by_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_core_extensions(): array
|
||||
{
|
||||
return self::$core_extensions;
|
||||
@@ -247,21 +274,22 @@ abstract class ExtensionInfo
|
||||
return self::$all_info_by_class[$normal];
|
||||
} else {
|
||||
$infos = print_r(array_keys(self::$all_info_by_class), true);
|
||||
throw new SCoreException("$normal not found in {$infos}");
|
||||
throw new ExtensionNotFound("$normal not found in {$infos}");
|
||||
}
|
||||
}
|
||||
|
||||
public static function load_all_extension_info()
|
||||
public static function load_all_extension_info(): void
|
||||
{
|
||||
foreach (get_subclasses_of("Shimmie2\ExtensionInfo") as $class) {
|
||||
foreach (get_subclasses_of(ExtensionInfo::class) as $class) {
|
||||
$extension_info = new $class();
|
||||
assert(is_a($extension_info, ExtensionInfo::class));
|
||||
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
|
||||
throw new ServerError("Extension Info $class with key $extension_info->key has already been loaded");
|
||||
}
|
||||
|
||||
self::$all_info_by_key[$extension_info->key] = $extension_info;
|
||||
self::$all_info_by_class[$class] = $extension_info;
|
||||
if ($extension_info->core===true) {
|
||||
if ($extension_info->core === true) {
|
||||
self::$core_extensions[] = $extension_info->key;
|
||||
}
|
||||
}
|
||||
@@ -275,7 +303,7 @@ abstract class ExtensionInfo
|
||||
*/
|
||||
abstract class FormatterExtension extends Extension
|
||||
{
|
||||
public function onTextFormatting(TextFormattingEvent $event)
|
||||
public function onTextFormatting(TextFormattingEvent $event): void
|
||||
{
|
||||
$event->formatted = $this->format($event->formatted);
|
||||
$event->stripped = $this->strip($event->stripped);
|
||||
@@ -293,9 +321,10 @@ abstract class FormatterExtension extends Extension
|
||||
*/
|
||||
abstract class DataHandlerExtension extends Extension
|
||||
{
|
||||
/** @var string[] */
|
||||
protected array $SUPPORTED_MIME = [];
|
||||
|
||||
protected function move_upload_to_archive(DataUploadEvent $event)
|
||||
public function onDataUpload(DataUploadEvent $event): void
|
||||
{
|
||||
global $config;
|
||||
|
||||
@@ -356,72 +385,18 @@ abstract class DataHandlerExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onDataUpload(DataUploadEvent $event)
|
||||
{
|
||||
$supported_mime = $this->supported_mime($event->mime);
|
||||
$check_contents = $this->check_contents($event->tmpname);
|
||||
if ($supported_mime && $check_contents) {
|
||||
$this->move_upload_to_archive($event);
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime));
|
||||
|
||||
/* Check if we are replacing an image */
|
||||
if (!is_null($event->replace_id)) {
|
||||
/* hax: This seems like such a dirty way to do this.. */
|
||||
|
||||
/* Check to make sure the image exists. */
|
||||
$existing = Image::by_id($event->replace_id);
|
||||
|
||||
if (is_null($existing)) {
|
||||
throw new UploadException("Post to replace does not exist!");
|
||||
}
|
||||
if ($existing->hash === $event->hash) {
|
||||
throw new UploadException("The uploaded post is the same as the one to replace.");
|
||||
}
|
||||
|
||||
// even more hax..
|
||||
$event->metadata['tags'] = $existing->get_tag_list();
|
||||
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
|
||||
send_event(new ImageReplaceEvent($event->replace_id, $image));
|
||||
$_id = $event->replace_id;
|
||||
assert(!is_null($_id));
|
||||
$event->image_id = $_id;
|
||||
} else {
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
|
||||
$iae = send_event(new ImageAdditionEvent($image));
|
||||
$event->image_id = $iae->image->id;
|
||||
$event->merged = $iae->merged;
|
||||
|
||||
// Rating Stuff.
|
||||
if (!empty($event->metadata['rating'])) {
|
||||
$rating = $event->metadata['rating'];
|
||||
send_event(new RatingSetEvent($image, $rating));
|
||||
}
|
||||
|
||||
// Locked Stuff.
|
||||
if (!empty($event->metadata['locked'])) {
|
||||
$locked = $event->metadata['locked'];
|
||||
send_event(new LockSetEvent($image, $locked));
|
||||
}
|
||||
}
|
||||
} elseif ($supported_mime && !$check_contents) {
|
||||
// We DO support this extension - but the file looks corrupt
|
||||
throw new UploadException("Invalid or corrupted file");
|
||||
}
|
||||
}
|
||||
|
||||
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
|
||||
public function onThumbnailGeneration(ThumbnailGenerationEvent $event): void
|
||||
{
|
||||
$result = false;
|
||||
if ($this->supported_mime($event->mime)) {
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
if ($event->force) {
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
$result = $this->create_thumb($event->image);
|
||||
} else {
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
|
||||
$outname = $event->image->get_thumb_filename();
|
||||
if (file_exists($outname)) {
|
||||
return;
|
||||
}
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
$result = $this->create_thumb($event->image);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
@@ -429,65 +404,48 @@ abstract class DataHandlerExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
public function onDisplayingImage(DisplayingImageEvent $event): void
|
||||
{
|
||||
global $page;
|
||||
global $config, $page;
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->theme->display_image($page, $event->image);
|
||||
$this->theme->display_image($event->image);
|
||||
if ($config->get_bool(ImageConfig::SHOW_META) && method_exists($this->theme, "display_metadata")) {
|
||||
$this->theme->display_metadata($event->image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event): void
|
||||
{
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
$this->media_check_properties($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function create_image_from_data(string $filename, array $metadata): Image
|
||||
{
|
||||
$image = new Image();
|
||||
|
||||
assert(is_readable($filename));
|
||||
$image->filesize = filesize($filename);
|
||||
$image->hash = md5_file($filename);
|
||||
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
|
||||
$image->set_mime(MimeType::get_for_file($filename, get_file_ext($metadata["filename"]) ?? null));
|
||||
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
|
||||
$image->source = $metadata['source'];
|
||||
|
||||
if (empty($image->get_mime())) {
|
||||
throw new UploadException("Unable to determine MIME for $filename");
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
} catch (MediaException $e) {
|
||||
throw new UploadException("Unable to scan media properties $filename / $image->filename / $image->hash: ".$e->getMessage());
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
abstract protected function media_check_properties(MediaCheckPropertiesEvent $event): void;
|
||||
abstract protected function check_contents(string $tmpname): bool;
|
||||
abstract protected function create_thumb(string $hash, string $mime): bool;
|
||||
abstract protected function create_thumb(Image $image): bool;
|
||||
|
||||
protected function supported_mime(string $mime): bool
|
||||
{
|
||||
return MimeType::matches_array($mime, $this->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_supported_mimes(): array
|
||||
{
|
||||
$arr = [];
|
||||
foreach (get_subclasses_of("Shimmie2\DataHandlerExtension") as $handler) {
|
||||
foreach (get_subclasses_of(DataHandlerExtension::class) as $handler) {
|
||||
$handler = (new $handler());
|
||||
assert(is_a($handler, DataHandlerExtension::class));
|
||||
$arr = array_merge($arr, $handler->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
// Not sure how to handle this otherwise, don't want to set up a whole other event for this one class
|
||||
if (class_exists("Shimmie2\TranscodeImage")) {
|
||||
if (Extension::is_enabled(TranscodeImageInfo::KEY)) {
|
||||
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
|
||||
}
|
||||
|
||||
@@ -495,6 +453,9 @@ abstract class DataHandlerExtension extends Extension
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_all_supported_exts(): array
|
||||
{
|
||||
$arr = [];
|
||||
|
||||
34
core/imageboard/autocomplete_column.php
Normal file
34
core/imageboard/autocomplete_column.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use MicroCRUD\TextColumn;
|
||||
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
class AutoCompleteColumn extends TextColumn
|
||||
{
|
||||
public function read_input(array $inputs): \MicroHTML\HTMLElement
|
||||
{
|
||||
return INPUT([
|
||||
"type" => "text",
|
||||
"name" => "r_{$this->name}",
|
||||
"class" => "autocomplete_tags",
|
||||
"placeholder" => $this->title,
|
||||
"value" => @$inputs["r_{$this->name}"]
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_input(array $inputs): \MicroHTML\HTMLElement
|
||||
{
|
||||
return INPUT([
|
||||
"type" => "text",
|
||||
"name" => "c_{$this->name}",
|
||||
"class" => "autocomplete_tags",
|
||||
"placeholder" => $this->title,
|
||||
"value" => @$inputs["c_{$this->name}"]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ namespace Shimmie2;
|
||||
*/
|
||||
class ImageAdditionEvent extends Event
|
||||
{
|
||||
public User $user;
|
||||
public bool $merged = false;
|
||||
|
||||
/**
|
||||
* A new image is being added to the database - just the image,
|
||||
* metadata will come later with ImageInfoSetEvent (and if that
|
||||
@@ -24,10 +21,6 @@ class ImageAdditionEvent extends Event
|
||||
}
|
||||
}
|
||||
|
||||
class ImageAdditionException extends SCoreException
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* An image is being deleted.
|
||||
*/
|
||||
@@ -52,18 +45,25 @@ class ImageDeletionEvent extends Event
|
||||
*/
|
||||
class ImageReplaceEvent extends Event
|
||||
{
|
||||
public string $old_hash;
|
||||
public string $new_hash;
|
||||
|
||||
/**
|
||||
* Replaces an image.
|
||||
* Replaces an image file.
|
||||
*
|
||||
* Updates an existing ID in the database to use a new image
|
||||
* file, leaving the tags and such unchanged. Also removes
|
||||
* the old image file and thumbnail from the disk.
|
||||
*/
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public Image $image
|
||||
public Image $image,
|
||||
public string $tmp_filename,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->old_hash = $image->hash;
|
||||
$hash = md5_file($tmp_filename);
|
||||
assert($hash !== false, "Failed to hash file $tmp_filename");
|
||||
$this->new_hash = $hash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,8 @@ class ThumbnailGenerationEvent extends Event
|
||||
* Request a thumbnail be made for an image object
|
||||
*/
|
||||
public function __construct(
|
||||
public string $hash,
|
||||
public string $mime,
|
||||
public bool $force=false
|
||||
public Image $image,
|
||||
public bool $force = false
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->generated = false;
|
||||
|
||||
@@ -8,6 +8,13 @@ use GQLA\Type;
|
||||
use GQLA\Field;
|
||||
use GQLA\Query;
|
||||
|
||||
enum ImagePropType
|
||||
{
|
||||
case BOOL;
|
||||
case INT;
|
||||
case STRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Image
|
||||
*
|
||||
@@ -16,15 +23,18 @@ use GQLA\Query;
|
||||
* As of 2.2, this no longer necessarily represents an
|
||||
* image per se, but could be a video, sound file, or any
|
||||
* other supported upload type.
|
||||
*
|
||||
* @implements \ArrayAccess<string, mixed>
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
#[Type(name: "Post")]
|
||||
class Image
|
||||
class Image implements \ArrayAccess
|
||||
{
|
||||
public const IMAGE_DIR = "images";
|
||||
public const THUMBNAIL_DIR = "thumbs";
|
||||
|
||||
public ?int $id = null;
|
||||
private bool $in_db = false;
|
||||
|
||||
public int $id;
|
||||
#[Field]
|
||||
public int $height = 0;
|
||||
#[Field]
|
||||
@@ -44,9 +54,9 @@ class Image
|
||||
public int $owner_id;
|
||||
public string $owner_ip;
|
||||
#[Field]
|
||||
public ?string $posted = null;
|
||||
public string $posted;
|
||||
#[Field]
|
||||
public ?string $source;
|
||||
public ?string $source = null;
|
||||
#[Field]
|
||||
public bool $locked = false;
|
||||
public ?bool $lossless = null;
|
||||
@@ -55,18 +65,25 @@ class Image
|
||||
public ?bool $image = null;
|
||||
public ?bool $audio = null;
|
||||
public ?int $length = null;
|
||||
public ?string $tmp_file = null;
|
||||
|
||||
public static array $bool_props = ["locked", "lossless", "video", "audio", "image"];
|
||||
public static array $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
|
||||
/** @var array<string, ImagePropType> */
|
||||
public static array $prop_types = [];
|
||||
/** @var array<string, mixed> */
|
||||
private array $dynamic_props = [];
|
||||
|
||||
/**
|
||||
* One will very rarely construct an image directly, more common
|
||||
* would be to use Image::by_id, Image::by_hash, etc.
|
||||
*
|
||||
* @param array<string|int, mixed>|null $row
|
||||
*/
|
||||
public function __construct(?array $row=null)
|
||||
public function __construct(?array $row = null)
|
||||
{
|
||||
if (!is_null($row)) {
|
||||
foreach ($row as $name => $value) {
|
||||
// some databases return both key=>value and numeric indices,
|
||||
// we only want the key=>value ones
|
||||
if (is_numeric($name)) {
|
||||
continue;
|
||||
} elseif (property_exists($this, $name)) {
|
||||
@@ -106,6 +123,7 @@ class Image
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->in_db = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,11 +167,11 @@ class Image
|
||||
public static function by_id(int $post_id): ?Image
|
||||
{
|
||||
global $database;
|
||||
if ($post_id > 2**32) {
|
||||
if ($post_id > 2 ** 32) {
|
||||
// for some reason bots query huge numbers and pollute the DB error logs...
|
||||
return null;
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id"=>$post_id]);
|
||||
$row = $database->get_row("SELECT * FROM images WHERE images.id=:id", ["id" => $post_id]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
@@ -170,26 +188,29 @@ class Image
|
||||
{
|
||||
global $database;
|
||||
$hash = strtolower($hash);
|
||||
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash"=>$hash]);
|
||||
$row = $database->get_row("SELECT images.* FROM images WHERE hash=:hash", ["hash" => $hash]);
|
||||
return ($row ? new Image($row) : null);
|
||||
}
|
||||
|
||||
public static function by_id_or_hash(string $id): ?Image
|
||||
{
|
||||
return (is_numeric($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
|
||||
return (is_numberish($id) && strlen($id) != 32) ? Image::by_id((int)$id) : Image::by_hash($id);
|
||||
}
|
||||
|
||||
public static function by_random(array $tags=[], int $limit_range=0): ?Image
|
||||
/**
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public static function by_random(array $tags = [], int $limit_range = 0): ?Image
|
||||
{
|
||||
$max = Image::count_images($tags);
|
||||
$max = Search::count_images($tags);
|
||||
if ($max < 1) {
|
||||
return null;
|
||||
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
|
||||
} // From Issue #22 - opened by HungryFeline on May 30, 2011.
|
||||
if ($limit_range > 0 && $max > $limit_range) {
|
||||
$max = $limit_range;
|
||||
}
|
||||
$rand = mt_rand(0, $max-1);
|
||||
$set = Image::find_images($rand, 1, $tags);
|
||||
$rand = mt_rand(0, $max - 1);
|
||||
$set = Search::find_images($rand, 1, $tags);
|
||||
if (count($set) > 0) {
|
||||
return $set[0];
|
||||
} else {
|
||||
@@ -197,136 +218,6 @@ class Image
|
||||
}
|
||||
}
|
||||
|
||||
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
|
||||
{
|
||||
global $database, $user;
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
if ($limit !== null && $limit < 1) {
|
||||
$limit = 1;
|
||||
}
|
||||
|
||||
if (SPEED_HAX) {
|
||||
if (!$user->can(Permissions::BIG_SEARCH) and count($tags) > 3) {
|
||||
throw new PermissionDeniedException("Anonymous users may only search for up to 3 tags at a time");
|
||||
}
|
||||
}
|
||||
|
||||
$querylet = Image::build_search_querylet($tags, $limit, $start);
|
||||
return $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images
|
||||
*
|
||||
* @param string[] $tags
|
||||
* @return Image[]
|
||||
*/
|
||||
#[Query(name: "posts", type: "[Post!]!", args: ["tags" => "[string!]"])]
|
||||
public static function find_images(int $offset = 0, ?int $limit = null, array $tags=[]): array
|
||||
{
|
||||
$result = self::find_images_internal($offset, $limit, $tags);
|
||||
|
||||
$images = [];
|
||||
foreach ($result as $row) {
|
||||
$images[] = new Image($row);
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an array of images, returning a iterable object of Image
|
||||
*/
|
||||
public static function find_images_iterable(int $start = 0, ?int $limit = null, array $tags=[]): \Generator
|
||||
{
|
||||
$result = self::find_images_internal($start, $limit, $tags);
|
||||
foreach ($result as $row) {
|
||||
yield new Image($row);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Image-related utility functions
|
||||
*/
|
||||
|
||||
public static function count_total_images(): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$total = $cache->get("image-count");
|
||||
if (is_null($total)) {
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) FROM images");
|
||||
$cache->set("image-count", $total, 600);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
public static function count_tag(string $tag): int
|
||||
{
|
||||
global $database;
|
||||
return (int)$database->get_one(
|
||||
"SELECT count FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag"=>$tag]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of image results for a given search
|
||||
*
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public static function count_images(array $tags=[]): int
|
||||
{
|
||||
global $cache, $database;
|
||||
$tag_count = count($tags);
|
||||
|
||||
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])) {
|
||||
if (!str_starts_with($tags[0], "-")) {
|
||||
// one tag - we can look that up directly
|
||||
$total = self::count_tag($tags[0]);
|
||||
} else {
|
||||
// one negative tag - subtract from the total
|
||||
$total = self::count_total_images() - self::count_tag(substr($tags[0], 1));
|
||||
}
|
||||
} else {
|
||||
// complex query
|
||||
// implode(tags) can be too long for memcache...
|
||||
$cache_key = "image-count:" . md5(Tag::implode($tags));
|
||||
$total = $cache->get($cache_key);
|
||||
if (is_null($total)) {
|
||||
if (Extension::is_enabled(RatingsInfo::KEY)) {
|
||||
$tags[] = "rating:*";
|
||||
}
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_null($total)) {
|
||||
return 0;
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of pages for a given search
|
||||
*
|
||||
* @param String[] $tags
|
||||
*/
|
||||
public static function count_pages(array $tags=[]): int
|
||||
{
|
||||
global $config;
|
||||
return (int)ceil(Image::count_images($tags) / $config->get_int(IndexConfig::IMAGES));
|
||||
}
|
||||
|
||||
/*
|
||||
* Accessors & mutators
|
||||
*/
|
||||
@@ -337,9 +228,9 @@ class Image
|
||||
* Rather than simply $this_id + 1, one must take into account
|
||||
* deleted images and search queries
|
||||
*
|
||||
* @param String[] $tags
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function get_next(array $tags=[], bool $next=true): ?Image
|
||||
public function get_next(array $tags = [], bool $next = true): ?Image
|
||||
{
|
||||
global $database;
|
||||
|
||||
@@ -351,31 +242,18 @@ class Image
|
||||
$dir = "ASC";
|
||||
}
|
||||
|
||||
if (count($tags) === 0) {
|
||||
$row = $database->get_row('
|
||||
SELECT images.*
|
||||
FROM images
|
||||
WHERE images.id '.$gtlt.' '.$this->id.'
|
||||
ORDER BY images.id '.$dir.'
|
||||
LIMIT 1
|
||||
');
|
||||
} else {
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$tags[] = 'order:id_'. strtolower($dir);
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$querylet->append_sql(' LIMIT 1');
|
||||
$row = $database->get_row($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
return ($row ? new Image($row) : null);
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$tags[] = 'order:id_'. strtolower($dir);
|
||||
$images = Search::find_images(0, 1, $tags);
|
||||
return (count($images) > 0) ? $images[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reverse of get_next
|
||||
*
|
||||
* @param String[] $tags
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function get_prev(array $tags=[]): ?Image
|
||||
public function get_prev(array $tags = []): ?Image
|
||||
{
|
||||
return $this->get_next($tags, false);
|
||||
}
|
||||
@@ -386,7 +264,9 @@ class Image
|
||||
#[Field(name: "owner")]
|
||||
public function get_owner(): User
|
||||
{
|
||||
return User::by_id($this->owner_id);
|
||||
$user = User::by_id($this->owner_id);
|
||||
assert(!is_null($user));
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,92 +277,73 @@ class Image
|
||||
global $database;
|
||||
if ($owner->id != $this->owner_id) {
|
||||
$database->execute("
|
||||
UPDATE images
|
||||
SET owner_id=:owner_id
|
||||
WHERE id=:id
|
||||
", ["owner_id"=>$owner->id, "id"=>$this->id]);
|
||||
UPDATE images
|
||||
SET owner_id=:owner_id
|
||||
WHERE id=:id
|
||||
", ["owner_id" => $owner->id, "id" => $this->id]);
|
||||
log_info("core_image", "Owner for Post #{$this->id} set to {$owner->name}");
|
||||
}
|
||||
}
|
||||
|
||||
public function save_to_db()
|
||||
public function save_to_db(): void
|
||||
{
|
||||
global $database, $user;
|
||||
$cut_name = substr($this->filename, 0, 255);
|
||||
|
||||
if (is_null($this->posted) || $this->posted == "") {
|
||||
$this->posted = date('Y-m-d H:i:s', time());
|
||||
}
|
||||
$props_to_save = [
|
||||
"filename" => substr($this->filename, 0, 255),
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"source" => $this->source,
|
||||
"width" => $this->width,
|
||||
"height" => $this->height,
|
||||
"lossless" => $this->lossless,
|
||||
"video" => $this->video,
|
||||
"video_codec" => $this->video_codec,
|
||||
"image" => $this->image,
|
||||
"audio" => $this->audio,
|
||||
"length" => $this->length
|
||||
];
|
||||
if (!$this->in_db) {
|
||||
$props_to_save["owner_id"] = $user->id;
|
||||
$props_to_save["owner_ip"] = get_real_ip();
|
||||
$props_to_save["posted"] = date('Y-m-d H:i:s', time());
|
||||
|
||||
$props_sql = implode(", ", array_keys($props_to_save));
|
||||
$vals_sql = implode(", ", array_map(fn ($prop) => ":$prop", array_keys($props_to_save)));
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$database->execute(
|
||||
"INSERT INTO images(
|
||||
owner_id, owner_ip,
|
||||
filename, filesize,
|
||||
hash, mime, ext,
|
||||
width, height,
|
||||
posted, source
|
||||
)
|
||||
VALUES (
|
||||
:owner_id, :owner_ip,
|
||||
:filename, :filesize,
|
||||
:hash, :mime, :ext,
|
||||
0, 0,
|
||||
:posted, :source
|
||||
)",
|
||||
[
|
||||
"owner_id" => $user->id, "owner_ip" => get_real_ip(),
|
||||
"filename" => $cut_name, "filesize" => $this->filesize,
|
||||
"hash" => $this->hash, "mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"posted" => $this->posted, "source" => $this->source
|
||||
]
|
||||
"INSERT INTO images($props_sql) VALUES ($vals_sql)",
|
||||
$props_to_save,
|
||||
);
|
||||
$this->id = $database->get_last_insert_id('images_id_seq');
|
||||
$this->in_db = true;
|
||||
} else {
|
||||
$props_sql = implode(", ", array_map(fn ($prop) => "$prop = :$prop", array_keys($props_to_save)));
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"filename = :filename, filesize = :filesize, hash = :hash, ".
|
||||
"mime = :mime, ext = :ext, width = 0, height = 0, ".
|
||||
"posted = :posted, source = :source ".
|
||||
"WHERE id = :id",
|
||||
[
|
||||
"filename" => $cut_name,
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"posted" => $this->posted,
|
||||
"source" => $this->source,
|
||||
"id" => $this->id,
|
||||
]
|
||||
"UPDATE images SET $props_sql WHERE id = :id",
|
||||
array_merge(
|
||||
$props_to_save,
|
||||
["id" => $this->id]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"lossless = :lossless, ".
|
||||
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ".
|
||||
"height = :height, width = :width, ".
|
||||
"length = :length WHERE id = :id",
|
||||
[
|
||||
"id" => $this->id,
|
||||
"width" => $this->width ?? 0,
|
||||
"height" => $this->height ?? 0,
|
||||
"lossless" => $this->lossless,
|
||||
"video" => $this->video,
|
||||
"video_codec" => $this->video_codec,
|
||||
"image" => $this->image,
|
||||
"audio" => $this->audio,
|
||||
"length" => $this->length
|
||||
]
|
||||
);
|
||||
// For the future: automatically save dynamic props instead of
|
||||
// requiring each extension to do it manually.
|
||||
/*
|
||||
$props_sql = "UPDATE images SET ";
|
||||
$props_sql .= implode(", ", array_map(fn ($prop) => "$prop = :$prop", array_keys($this->dynamic_props)));
|
||||
$props_sql .= " WHERE id = :id";
|
||||
$database->execute($props_sql, array_merge($this->dynamic_props, ["id" => $this->id]));
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this image's tags as an array.
|
||||
*
|
||||
* @return String[]
|
||||
* @return string[]
|
||||
*/
|
||||
#[Field(name: "tags", type: "[string!]!")]
|
||||
public function get_tag_array(): array
|
||||
@@ -490,12 +351,12 @@ class Image
|
||||
global $database;
|
||||
if (!isset($this->tag_array)) {
|
||||
$this->tag_array = $database->get_col("
|
||||
SELECT tag
|
||||
FROM image_tags
|
||||
JOIN tags ON image_tags.tag_id = tags.id
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id"=>$this->id]);
|
||||
SELECT tag
|
||||
FROM image_tags
|
||||
JOIN tags ON image_tags.tag_id = tags.id
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id" => $this->id]);
|
||||
sort($this->tag_array);
|
||||
}
|
||||
return $this->tag_array;
|
||||
@@ -616,22 +477,18 @@ class Image
|
||||
* Get the image's mime type.
|
||||
*/
|
||||
#[Field(name: "mime")]
|
||||
public function get_mime(): ?string
|
||||
public function get_mime(): string
|
||||
{
|
||||
if ($this->mime===MimeType::WEBP&&$this->lossless) {
|
||||
if ($this->mime === MimeType::WEBP && $this->lossless) {
|
||||
return MimeType::WEBP_LOSSLESS;
|
||||
}
|
||||
$m = $this->mime;
|
||||
if (is_null($m)) {
|
||||
$m = MimeMap::get_for_extension($this->ext)[0];
|
||||
}
|
||||
return $m;
|
||||
return strtolower($this->mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image's mime type.
|
||||
*/
|
||||
public function set_mime($mime): void
|
||||
public function set_mime(string $mime): void
|
||||
{
|
||||
$this->mime = $mime;
|
||||
$ext = FileExtension::get_for_mime($this->get_mime());
|
||||
@@ -659,7 +516,7 @@ class Image
|
||||
$new_source = null;
|
||||
}
|
||||
if ($new_source != $old_source) {
|
||||
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source"=>$new_source, "id"=>$this->id]);
|
||||
$database->execute("UPDATE images SET source=:source WHERE id=:id", ["source" => $new_source, "id" => $this->id]);
|
||||
log_info("core_image", "Source for Post #{$this->id} set to: $new_source (was $old_source)");
|
||||
}
|
||||
}
|
||||
@@ -676,8 +533,9 @@ class Image
|
||||
{
|
||||
global $database;
|
||||
if ($locked !== $this->locked) {
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$locked, "id"=>$this->id]);
|
||||
log_info("core_image", "Setting Post #{$this->id} lock to: $locked");
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn" => $locked, "id" => $this->id]);
|
||||
$s = $locked ? "locked" : "unlocked";
|
||||
log_info("core_image", "Setting Post #{$this->id} to $s");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,39 +555,38 @@ class Image
|
||||
FROM image_tags
|
||||
WHERE image_id = :id
|
||||
)
|
||||
", ["id"=>$this->id]);
|
||||
", ["id" => $this->id]);
|
||||
$database->execute("
|
||||
DELETE
|
||||
FROM image_tags
|
||||
WHERE image_id=:id
|
||||
", ["id"=>$this->id]);
|
||||
DELETE
|
||||
FROM image_tags
|
||||
WHERE image_id=:id
|
||||
", ["id" => $this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tags for this image.
|
||||
*
|
||||
* @param string[] $unfiltered_tags
|
||||
*/
|
||||
public function set_tags(array $unfiltered_tags): void
|
||||
{
|
||||
global $cache, $database, $page;
|
||||
|
||||
$unfiltered_tags = array_unique($unfiltered_tags);
|
||||
$tags = array_unique($unfiltered_tags);
|
||||
|
||||
$tags = [];
|
||||
foreach ($unfiltered_tags as $tag) {
|
||||
foreach ($tags as $tag) {
|
||||
if (mb_strlen($tag, 'UTF-8') > 255) {
|
||||
$page->flash("Can't set a tag longer than 255 characters");
|
||||
continue;
|
||||
throw new TagSetException("Can't set a tag longer than 255 characters");
|
||||
}
|
||||
if (str_starts_with($tag, "-")) {
|
||||
$page->flash("Can't set a tag which starts with a minus");
|
||||
continue;
|
||||
throw new TagSetException("Can't set a tag which starts with a minus");
|
||||
}
|
||||
if (str_contains($tag, "*")) {
|
||||
throw new TagSetException("Can't set a tag which contains a wildcard (*)");
|
||||
}
|
||||
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
if (count($tags) <= 0) {
|
||||
throw new SCoreException('Tried to set zero tags');
|
||||
throw new TagSetException('Tried to set zero tags');
|
||||
}
|
||||
|
||||
if (strtolower(Tag::implode($tags)) != strtolower($this->get_tag_list())) {
|
||||
@@ -748,7 +605,7 @@ class Image
|
||||
FROM image_tags
|
||||
WHERE image_id = :id
|
||||
)
|
||||
", ["id"=>$this->id]);
|
||||
", ["id" => $this->id]);
|
||||
|
||||
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags));
|
||||
$cache->delete("image-{$this->id}-tags");
|
||||
@@ -762,18 +619,16 @@ class Image
|
||||
{
|
||||
global $database;
|
||||
$this->delete_tags_from_image();
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id" => $this->id]);
|
||||
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')');
|
||||
|
||||
unlink($this->get_image_filename());
|
||||
unlink($this->get_thumb_filename());
|
||||
$this->remove_image_only(quiet: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function removes an image (and thumbnail) from the DISK ONLY.
|
||||
* It DOES NOT remove anything from the database.
|
||||
*/
|
||||
public function remove_image_only(): void
|
||||
public function remove_image_only(bool $quiet = false): void
|
||||
{
|
||||
$img_del = @unlink($this->get_image_filename());
|
||||
$thumb_del = @unlink($this->get_thumb_filename());
|
||||
@@ -788,229 +643,10 @@ class Image
|
||||
}
|
||||
}
|
||||
|
||||
public function parse_link_template(string $tmpl, int $n=0): string
|
||||
public function parse_link_template(string $tmpl, int $n = 0): string
|
||||
{
|
||||
$plte = send_event(new ParseLinkTemplateEvent($tmpl, $this));
|
||||
$tmpl = $plte->link;
|
||||
return load_balance_url($tmpl, $this->hash, $n);
|
||||
}
|
||||
|
||||
private static function tag_or_wildcard_to_ids(string $tag): array
|
||||
{
|
||||
global $database;
|
||||
$sq = "SELECT id FROM tags WHERE LOWER(tag) LIKE LOWER(:tag)";
|
||||
if ($database->get_driver_id() === DatabaseDriverID::SQLITE) {
|
||||
$sq .= "ESCAPE '\\'";
|
||||
}
|
||||
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String[] $terms
|
||||
*/
|
||||
private static function build_search_querylet(
|
||||
array $terms,
|
||||
?int $limit=null,
|
||||
?int $offset=null
|
||||
): Querylet {
|
||||
global $config;
|
||||
|
||||
$tag_conditions = [];
|
||||
$img_conditions = [];
|
||||
$order = null;
|
||||
|
||||
/*
|
||||
* Turn a bunch of strings into a bunch of TagCondition
|
||||
* and ImgCondition objects
|
||||
*/
|
||||
$stpen = 0; // search term parse event number
|
||||
foreach (array_merge([null], $terms) as $term) {
|
||||
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
|
||||
$order ??= $stpe->order;
|
||||
$img_conditions = array_merge($img_conditions, $stpe->img_conditions);
|
||||
$tag_conditions = array_merge($tag_conditions, $stpe->tag_conditions);
|
||||
}
|
||||
|
||||
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
|
||||
/*
|
||||
* Turn a bunch of Querylet objects into a base query
|
||||
*
|
||||
* Must follow the format
|
||||
*
|
||||
* SELECT images.*
|
||||
* FROM (...) AS images
|
||||
* WHERE (...)
|
||||
*
|
||||
* ie, return a set of images.* columns, and end with a WHERE
|
||||
*/
|
||||
|
||||
// no tags, do a simple search
|
||||
if (count($tag_conditions) === 0) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
|
||||
// one tag sorted by ID - we can fetch this from the image_tags table,
|
||||
// and do the offset / limit there, which is 10x faster than fetching
|
||||
// all the image_tags and doing the offset / limit on the result.
|
||||
elseif (
|
||||
count($tag_conditions) === 1
|
||||
&& empty($img_conditions)
|
||||
&& ($order == "id DESC" || $order == "images.id DESC")
|
||||
&& !is_null($offset)
|
||||
&& !is_null($limit)
|
||||
) {
|
||||
$tc = $tag_conditions[0];
|
||||
$in = $tc->positive ? "IN" : "NOT IN";
|
||||
// IN (SELECT id FROM tags) is 100x slower than doing a separate
|
||||
// query and then a second query for IN(first_query_results)??
|
||||
$tag_array = self::tag_or_wildcard_to_ids($tc->tag);
|
||||
if (count($tag_array) == 0) {
|
||||
// if wildcard expanded to nothing, take a shortcut
|
||||
if ($tc->positive) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} else {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
}
|
||||
} else {
|
||||
$set = implode(', ', $tag_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images INNER JOIN (
|
||||
SELECT it.image_id
|
||||
FROM image_tags it
|
||||
WHERE it.tag_id $in ($set)
|
||||
ORDER BY it.image_id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
) a on a.image_id = images.id
|
||||
ORDER BY images.id DESC
|
||||
", ["limit"=>$limit, "offset"=>$offset]);
|
||||
// don't offset at the image level because
|
||||
// we already offset at the image_tags level
|
||||
$order = null;
|
||||
$limit = null;
|
||||
$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
// more than one tag, or more than zero other conditions, or a non-default sort order
|
||||
else {
|
||||
$positive_tag_id_array = [];
|
||||
$positive_wildcard_id_array = [];
|
||||
$negative_tag_id_array = [];
|
||||
$all_nonexistent_negatives = true;
|
||||
|
||||
foreach ($tag_conditions as $tq) {
|
||||
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
|
||||
$tag_count = count($tag_ids);
|
||||
|
||||
if ($tq->positive) {
|
||||
$all_nonexistent_negatives = false;
|
||||
if ($tag_count== 0) {
|
||||
# one of the positive tags had zero results, therefor there
|
||||
# can be no results; "where 1=0" should shortcut things
|
||||
return new Querylet("SELECT images.* FROM images WHERE 1=0");
|
||||
} elseif ($tag_count==1) {
|
||||
// All wildcard terms that qualify for a single tag can be treated the same as non-wildcards
|
||||
$positive_tag_id_array[] = $tag_ids[0];
|
||||
} else {
|
||||
// Terms that resolve to multiple tags act as an OR within themselves
|
||||
// and as an AND in relation to all other terms,
|
||||
$positive_wildcard_id_array[] = $tag_ids;
|
||||
}
|
||||
} else {
|
||||
if ($tag_count > 0) {
|
||||
$all_nonexistent_negatives = false;
|
||||
// Unlike positive criteria, negative criteria are all handled in an OR fashion,
|
||||
// so we can just compile them all into a single sub-query.
|
||||
$negative_tag_id_array = array_merge($negative_tag_id_array, $tag_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert($positive_tag_id_array || $positive_wildcard_id_array || $negative_tag_id_array || $all_nonexistent_negatives, @$_GET['q']);
|
||||
|
||||
if ($all_nonexistent_negatives) {
|
||||
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
|
||||
} elseif (!empty($positive_tag_id_array) || !empty($positive_wildcard_id_array)) {
|
||||
$inner_joins = [];
|
||||
if (!empty($positive_tag_id_array)) {
|
||||
foreach ($positive_tag_id_array as $tag) {
|
||||
$inner_joins[] = "= $tag";
|
||||
}
|
||||
}
|
||||
if (!empty($positive_wildcard_id_array)) {
|
||||
foreach ($positive_wildcard_id_array as $tags) {
|
||||
$positive_tag_id_list = join(', ', $tags);
|
||||
$inner_joins[] = "IN ($positive_tag_id_list)";
|
||||
}
|
||||
}
|
||||
|
||||
$first = array_shift($inner_joins);
|
||||
$sub_query = "SELECT it.image_id FROM image_tags it ";
|
||||
$i = 0;
|
||||
foreach ($inner_joins as $inner_join) {
|
||||
$i++;
|
||||
$sub_query .= " INNER JOIN image_tags it$i ON it$i.image_id = it.image_id AND it$i.tag_id $inner_join ";
|
||||
}
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$sub_query .= " LEFT JOIN image_tags negative ON negative.image_id = it.image_id AND negative.tag_id IN ($negative_tag_id_list) ";
|
||||
}
|
||||
$sub_query .= "WHERE it.tag_id $first ";
|
||||
if (!empty($negative_tag_id_array)) {
|
||||
$sub_query .= " AND negative.image_id IS NULL";
|
||||
}
|
||||
$sub_query .= " GROUP BY it.image_id ";
|
||||
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
INNER JOIN ($sub_query) a on a.image_id = images.id
|
||||
");
|
||||
} elseif (!empty($negative_tag_id_array)) {
|
||||
$negative_tag_id_list = join(', ', $negative_tag_id_array);
|
||||
$query = new Querylet("
|
||||
SELECT images.*
|
||||
FROM images
|
||||
LEFT JOIN image_tags negative ON negative.image_id = images.id AND negative.tag_id in ($negative_tag_id_list)
|
||||
WHERE negative.image_id IS NULL
|
||||
");
|
||||
} else {
|
||||
throw new SCoreException("No criteria specified");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Merge all the image metadata searches into one generic querylet
|
||||
* and append to the base querylet with "AND blah"
|
||||
*/
|
||||
if (!empty($img_conditions)) {
|
||||
$n = 0;
|
||||
$img_sql = "";
|
||||
$img_vars = [];
|
||||
foreach ($img_conditions as $iq) {
|
||||
if ($n++ > 0) {
|
||||
$img_sql .= " AND";
|
||||
}
|
||||
if (!$iq->positive) {
|
||||
$img_sql .= " NOT";
|
||||
}
|
||||
$img_sql .= " (" . $iq->qlet->sql . ")";
|
||||
$img_vars = array_merge($img_vars, $iq->qlet->variables);
|
||||
}
|
||||
$query->append_sql(" AND ");
|
||||
$query->append(new Querylet($img_sql, $img_vars));
|
||||
}
|
||||
|
||||
if (!is_null($order)) {
|
||||
$query->append(new Querylet(" ORDER BY ".$order));
|
||||
}
|
||||
if (!is_null($limit)) {
|
||||
$query->append(new Querylet(" LIMIT :limit ", ["limit" => $limit]));
|
||||
$query->append(new Querylet(" OFFSET :offset ", ["offset" => $offset]));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@ namespace Shimmie2;
|
||||
* Add a directory full of images
|
||||
*
|
||||
* @param string $base
|
||||
* @return array
|
||||
* @param string[] $extra_tags
|
||||
* @return UploadResult[]
|
||||
*/
|
||||
function add_dir(string $base): array
|
||||
function add_dir(string $base, array $extra_tags = []): array
|
||||
{
|
||||
global $database;
|
||||
$results = [];
|
||||
|
||||
foreach (list_files($base) as $full_path) {
|
||||
$short_path = str_replace($base, "", $full_path);
|
||||
$filename = basename($full_path);
|
||||
|
||||
$tags = path_to_tags($short_path);
|
||||
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
||||
$tags = array_merge(path_to_tags($short_path), $extra_tags);
|
||||
try {
|
||||
$more_results = $database->with_savepoint(function () use ($full_path, $filename, $tags) {
|
||||
$dae = send_event(new DataUploadEvent($full_path, basename($full_path), 0, [
|
||||
@@ -38,26 +39,13 @@ function add_dir(string $base): array
|
||||
});
|
||||
$results = array_merge($results, $more_results);
|
||||
} catch (UploadException $ex) {
|
||||
$result .= "failed: ".$ex->getMessage();
|
||||
$results[] = new UploadError($filename, $ex->getMessage());
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DataUploadEvent for a file.
|
||||
*/
|
||||
function add_image(string $tmpname, string $filename, string $tags, ?string $source=null): DataUploadEvent
|
||||
{
|
||||
return send_event(new DataUploadEvent($tmpname, [
|
||||
'filename' => pathinfo($filename, PATHINFO_BASENAME),
|
||||
'tags' => Tag::explode($tags),
|
||||
'source' => $source,
|
||||
]));
|
||||
}
|
||||
|
||||
function get_file_ext(string $filename): ?string
|
||||
{
|
||||
return pathinfo($filename)['extension'] ?? null;
|
||||
@@ -71,7 +59,7 @@ function get_file_ext(string $filename): ?string
|
||||
* @param int $orig_width
|
||||
* @param int $orig_height
|
||||
* @param bool $use_dpi_scaling Enables the High-DPI scaling.
|
||||
* @return array
|
||||
* @return array{0: int, 1: int}
|
||||
*/
|
||||
function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_scaling = false): array
|
||||
{
|
||||
@@ -119,53 +107,60 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int, 2: float}
|
||||
*/
|
||||
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height): array
|
||||
{
|
||||
$xscale = ($max_width/ $original_width);
|
||||
$yscale = ($max_height/ $original_height);
|
||||
$xscale = ($max_width / $original_width);
|
||||
$yscale = ($max_height / $original_height);
|
||||
|
||||
$scale = ($yscale < $xscale) ? $yscale : $xscale ;
|
||||
|
||||
return [(int)($original_width*$scale), (int)($original_height*$scale), $scale];
|
||||
return [(int)($original_width * $scale), (int)($original_height * $scale), $scale];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the thumbnails height and width settings and applies the High-DPI scaling setting before returning the dimensions.
|
||||
*
|
||||
* @return array [width, height]
|
||||
* @return array{0: int, 1: int}
|
||||
*/
|
||||
function get_thumbnail_max_size_scaled(): array
|
||||
{
|
||||
global $config;
|
||||
|
||||
$scaling = $config->get_int(ImageConfig::THUMB_SCALING);
|
||||
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
|
||||
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
|
||||
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling / 100);
|
||||
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling / 100);
|
||||
return [$max_width, $max_height];
|
||||
}
|
||||
|
||||
|
||||
function create_image_thumb(string $hash, string $mime, string $engine = null)
|
||||
function create_image_thumb(Image $image, string $engine = null): void
|
||||
{
|
||||
global $config;
|
||||
|
||||
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
|
||||
$tsize = get_thumbnail_max_size_scaled();
|
||||
create_scaled_image(
|
||||
$inname,
|
||||
$outname,
|
||||
$tsize,
|
||||
$mime,
|
||||
$image->get_image_filename(),
|
||||
$image->get_thumb_filename(),
|
||||
get_thumbnail_max_size_scaled(),
|
||||
$image->get_mime(),
|
||||
$engine,
|
||||
$config->get_string(ImageConfig::THUMB_FIT)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function create_scaled_image(string $inname, string $outname, array $tsize, string $mime, ?string $engine = null, ?string $resize_type = null)
|
||||
{
|
||||
/**
|
||||
* @param array{0: int, 1: int} $tsize
|
||||
*/
|
||||
function create_scaled_image(
|
||||
string $inname,
|
||||
string $outname,
|
||||
array $tsize,
|
||||
string $mime,
|
||||
?string $engine = null,
|
||||
?string $resize_type = null
|
||||
): void {
|
||||
global $config;
|
||||
if (empty($engine)) {
|
||||
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
|
||||
@@ -207,7 +202,7 @@ function redirect_to_next_image(Image $image, ?string $search = null): void
|
||||
$target_image = $image->get_next($search_terms);
|
||||
|
||||
if ($target_image === null) {
|
||||
$redirect_target = referer_or(make_link("post/list"), ['post/view']);
|
||||
$redirect_target = referer_or(search_link(), ['post/view']);
|
||||
} else {
|
||||
$redirect_target = make_link("post/view/{$target_image->id}", null, $query);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,13 @@ use GQLA\Query;
|
||||
*/
|
||||
class Querylet
|
||||
{
|
||||
/**
|
||||
* @param string $sql
|
||||
* @param array<string, mixed> $variables
|
||||
*/
|
||||
public function __construct(
|
||||
public string $sql,
|
||||
public array $variables=[],
|
||||
public array $variables = [],
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -34,16 +38,6 @@ class Querylet
|
||||
$this->sql .= $querylet->sql;
|
||||
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||
}
|
||||
|
||||
public function append_sql(string $sql): void
|
||||
{
|
||||
$this->sql .= $sql;
|
||||
}
|
||||
|
||||
public function add_variable($var): void
|
||||
{
|
||||
$this->variables[] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +47,7 @@ class TagCondition
|
||||
{
|
||||
public function __construct(
|
||||
public string $tag,
|
||||
public bool $positive,
|
||||
public bool $positive = true,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -66,7 +60,7 @@ class ImgCondition
|
||||
{
|
||||
public function __construct(
|
||||
public Querylet $qlet,
|
||||
public bool $positive,
|
||||
public bool $positive = true,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,10 @@ class TagUsage
|
||||
* @return TagUsage[]
|
||||
*/
|
||||
#[Query(name: "tags", type: '[TagUsage!]!')]
|
||||
public static function tags(string $search, int $limit=10): array
|
||||
public static function tags(string $search, int $limit = 10): array
|
||||
{
|
||||
global $cache, $database;
|
||||
|
||||
if (!$search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$search = strtolower($search);
|
||||
if (
|
||||
$search == '' ||
|
||||
@@ -48,16 +44,16 @@ class TagUsage
|
||||
$limitSQL = "";
|
||||
$search = str_replace('_', '\_', $search);
|
||||
$search = str_replace('%', '\%', $search);
|
||||
$SQLarr = ["search"=>"$search%"]; #, "cat_search"=>"%:$search%"];
|
||||
$SQLarr = ["search" => "$search%"]; #, "cat_search"=>"%:$search%"];
|
||||
if ($limit !== 0) {
|
||||
$limitSQL = "LIMIT :limit";
|
||||
$SQLarr['limit'] = $limit;
|
||||
$cache_key .= "-" . $limit;
|
||||
}
|
||||
|
||||
$res = $cache->get($cache_key);
|
||||
if (is_null($res)) {
|
||||
$res = $database->get_pairs(
|
||||
$res = cache_get_or_set(
|
||||
$cache_key,
|
||||
fn () => $database->get_pairs(
|
||||
"
|
||||
SELECT tag, count
|
||||
FROM tags
|
||||
@@ -68,9 +64,9 @@ class TagUsage
|
||||
$limitSQL
|
||||
",
|
||||
$SQLarr
|
||||
);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
),
|
||||
600
|
||||
);
|
||||
|
||||
$counts = [];
|
||||
foreach ($res as $k => $v) {
|
||||
@@ -90,7 +86,8 @@ class TagUsage
|
||||
*/
|
||||
class Tag
|
||||
{
|
||||
private static $tag_id_cache = [];
|
||||
/** @var array<string, int> */
|
||||
private static array $tag_id_cache = [];
|
||||
|
||||
public static function get_or_create_id(string $tag): int
|
||||
{
|
||||
@@ -104,17 +101,17 @@ class Tag
|
||||
|
||||
$id = $database->get_one(
|
||||
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag"=>$tag]
|
||||
["tag" => $tag]
|
||||
);
|
||||
if (empty($id)) {
|
||||
// a new tag
|
||||
$database->execute(
|
||||
"INSERT INTO tags(tag) VALUES (:tag)",
|
||||
["tag"=>$tag]
|
||||
["tag" => $tag]
|
||||
);
|
||||
$id = $database->get_one(
|
||||
"SELECT id FROM tags WHERE LOWER(tag) = LOWER(:tag)",
|
||||
["tag"=>$tag]
|
||||
["tag" => $tag]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,18 +119,19 @@ class Tag
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** @param string[] $tags */
|
||||
public static function implode(array $tags): string
|
||||
{
|
||||
sort($tags, SORT_FLAG_CASE|SORT_STRING);
|
||||
sort($tags, SORT_FLAG_CASE | SORT_STRING);
|
||||
return implode(' ', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human-supplied string into a valid tag array.
|
||||
*
|
||||
* #return string[]
|
||||
* @return string[]
|
||||
*/
|
||||
public static function explode(string $tags, bool $tagme=true): array
|
||||
public static function explode(string $tags, bool $tagme = true): array
|
||||
{
|
||||
global $database;
|
||||
|
||||
@@ -151,7 +149,7 @@ class Tag
|
||||
$new = [];
|
||||
$i = 0;
|
||||
$tag_count = count($tag_array);
|
||||
while ($i<$tag_count) {
|
||||
while ($i < $tag_count) {
|
||||
$tag = $tag_array[$i];
|
||||
$negative = '';
|
||||
if (!empty($tag) && ($tag[0] == '-')) {
|
||||
@@ -165,7 +163,7 @@ class Tag
|
||||
FROM aliases
|
||||
WHERE LOWER(oldtag)=LOWER(:tag)
|
||||
",
|
||||
["tag"=>$tag]
|
||||
["tag" => $tag]
|
||||
);
|
||||
if (empty($newtags)) {
|
||||
//tag has no alias, use old tag
|
||||
@@ -199,9 +197,13 @@ class Tag
|
||||
public static function sanitize(string $tag): string
|
||||
{
|
||||
$tag = preg_replace("/\s/", "", $tag); # whitespace
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace('/\x20[\x0e\x0f]/', '', $tag); # unicode RTL
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
|
||||
assert($tag !== null);
|
||||
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
|
||||
assert($tag !== null);
|
||||
$tag = trim($tag, ", \t\n\r\0\x0B");
|
||||
|
||||
if ($tag == ".") {
|
||||
@@ -211,9 +213,13 @@ class Tag
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags1
|
||||
* @param string[] $tags2
|
||||
*/
|
||||
public static function compare(array $tags1, array $tags2): bool
|
||||
{
|
||||
if (count($tags1)!==count($tags2)) {
|
||||
if (count($tags1) !== count($tags2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -225,6 +231,11 @@ class Tag
|
||||
return $tags1 == $tags2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $source
|
||||
* @param string[] $remove
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_diff_tags(array $source, array $remove): array
|
||||
{
|
||||
$before = array_map('strtolower', $source);
|
||||
@@ -238,6 +249,10 @@ class Tag
|
||||
return $after;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @return string[]
|
||||
*/
|
||||
public static function sanitize_array(array $tags): array
|
||||
{
|
||||
global $page;
|
||||
@@ -269,53 +284,4 @@ class Tag
|
||||
// $term = str_replace("?", "_", $term);
|
||||
return $term;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind of like urlencode, but using a custom scheme so that
|
||||
* tags always fit neatly between slashes in a URL. Use this
|
||||
* when you want to put an arbitrary tag into a URL.
|
||||
*/
|
||||
public static function caret(string $input): string
|
||||
{
|
||||
$to_caret = [
|
||||
"^" => "^",
|
||||
"/" => "s",
|
||||
"\\" => "b",
|
||||
"?" => "q",
|
||||
"&" => "a",
|
||||
"." => "d",
|
||||
];
|
||||
|
||||
foreach ($to_caret as $from => $to) {
|
||||
$input = str_replace($from, '^' . $to, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this when you want to get a tag out of a URL
|
||||
*/
|
||||
public static function decaret(string $str): string
|
||||
{
|
||||
$from_caret = [
|
||||
"^" => "^",
|
||||
"s" => "/",
|
||||
"b" => "\\",
|
||||
"q" => "?",
|
||||
"a" => "&",
|
||||
"d" => ".",
|
||||
];
|
||||
|
||||
$out = "";
|
||||
$length = strlen($str);
|
||||
for ($i=0; $i<$length; $i++) {
|
||||
if ($str[$i] == "^") {
|
||||
$i++;
|
||||
$out .= $from_caret[$str[$i]] ?? '';
|
||||
} else {
|
||||
$out .= $str[$i];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ require_once "core/urls.php";
|
||||
* and other such things that aren't ready yet
|
||||
*/
|
||||
|
||||
function install()
|
||||
function install(): void
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
@@ -48,11 +48,16 @@ function install()
|
||||
if ($dsn) {
|
||||
do_install($dsn);
|
||||
} else {
|
||||
ask_questions();
|
||||
if (PHP_SAPI == 'cli') {
|
||||
print("INSTALL_DSN needs to be set for CLI installation\n");
|
||||
exit(1);
|
||||
} else {
|
||||
ask_questions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_dsn()
|
||||
function get_dsn(): ?string
|
||||
{
|
||||
if (getenv("INSTALL_DSN")) {
|
||||
$dsn = getenv("INSTALL_DSN");
|
||||
@@ -68,7 +73,7 @@ function get_dsn()
|
||||
return $dsn;
|
||||
}
|
||||
|
||||
function do_install($dsn)
|
||||
function do_install(string $dsn): void
|
||||
{
|
||||
try {
|
||||
create_dirs();
|
||||
@@ -79,7 +84,7 @@ function do_install($dsn)
|
||||
}
|
||||
}
|
||||
|
||||
function ask_questions()
|
||||
function ask_questions(): void
|
||||
{
|
||||
$warnings = [];
|
||||
$errors = [];
|
||||
@@ -116,9 +121,9 @@ function ask_questions()
|
||||
";
|
||||
}
|
||||
|
||||
$db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '<option value="'. DatabaseDriverID::SQLITE->value .'">SQLite</option>' : "";
|
||||
$db_m = in_array(DatabaseDriverID::MYSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::MYSQL->value .'">MySQL</option>' : "";
|
||||
$db_p = in_array(DatabaseDriverID::PGSQL->value, $drivers) ? '<option value="'. DatabaseDriverID::PGSQL->value .'">PostgreSQL</option>' : "";
|
||||
$db_s = in_array(DatabaseDriverID::SQLITE->value, $drivers) ? '<option value="'. DatabaseDriverID::SQLITE->value .'">SQLite</option>' : "";
|
||||
|
||||
$warn_msg = $warnings ? "<h3>Warnings</h3>".implode("\n<p>", $warnings) : "";
|
||||
$err_msg = $errors ? "<h3>Errors</h3>".implode("\n<p>", $errors) : "";
|
||||
@@ -136,9 +141,9 @@ function ask_questions()
|
||||
<tr>
|
||||
<th>Type:</th>
|
||||
<td><select name="database_type" id="database_type" onchange="update_qs();">
|
||||
$db_m
|
||||
$db_s
|
||||
$db_m
|
||||
$db_p
|
||||
$db_s
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr class="dbconf mysql pgsql">
|
||||
@@ -165,13 +170,9 @@ function ask_questions()
|
||||
return document.querySelectorAll(n);
|
||||
}
|
||||
function update_qs() {
|
||||
Array.prototype.forEach.call(q('.dbconf'), function(el, i){
|
||||
el.style.display = 'none';
|
||||
});
|
||||
q('.dbconf').forEach(el => el.style.display = 'none');
|
||||
let seldb = q("#database_type")[0].value || "none";
|
||||
Array.prototype.forEach.call(q('.'+seldb), function(el, i){
|
||||
el.style.display = null;
|
||||
});
|
||||
q('.'+seldb).forEach(el => el.style.display = null);
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
@@ -182,8 +183,9 @@ function ask_questions()
|
||||
The username provided must have access to create tables within the database.
|
||||
</p>
|
||||
<p class="dbconf sqlite">
|
||||
For SQLite the database name will be a filename on disk, relative to
|
||||
where shimmie was installed.
|
||||
SQLite with default settings is fine for tens of users with thousands
|
||||
of images. For thousands of users or millions of images, postgres is
|
||||
recommended.
|
||||
</p>
|
||||
<p class="dbconf none">
|
||||
Drivers can generally be downloaded with your OS package manager;
|
||||
@@ -194,7 +196,7 @@ EOD
|
||||
}
|
||||
|
||||
|
||||
function create_dirs()
|
||||
function create_dirs(): void
|
||||
{
|
||||
$data_exists = file_exists("data") || mkdir("data");
|
||||
$data_writable = $data_exists && (is_writable("data") || chmod("data", 0755));
|
||||
@@ -211,7 +213,7 @@ function create_dirs()
|
||||
}
|
||||
}
|
||||
|
||||
function create_tables(Database $db)
|
||||
function create_tables(Database $db): void
|
||||
{
|
||||
try {
|
||||
if ($db->count_tables() > 0) {
|
||||
@@ -296,6 +298,8 @@ function create_tables(Database $db)
|
||||
if ($db->is_transaction_open()) {
|
||||
$db->commit();
|
||||
}
|
||||
// Ensure that we end this code in a transaction (for testing)
|
||||
$db->begin_transaction();
|
||||
} catch (\PDOException $e) {
|
||||
throw new InstallerException(
|
||||
"PDO Error:",
|
||||
@@ -308,7 +312,7 @@ function create_tables(Database $db)
|
||||
}
|
||||
}
|
||||
|
||||
function write_config($dsn)
|
||||
function write_config(string $dsn): void
|
||||
{
|
||||
$file_content = "<" . "?php\ndefine('DATABASE_DSN', '$dsn');\n";
|
||||
|
||||
@@ -317,11 +321,16 @@ function write_config($dsn)
|
||||
}
|
||||
|
||||
if (file_put_contents("data/config/shimmie.conf.php", $file_content, LOCK_EX)) {
|
||||
header("Location: index.php?flash=Installation%20complete");
|
||||
die_nicely(
|
||||
"Installation Successful",
|
||||
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
|
||||
);
|
||||
if (PHP_SAPI == 'cli') {
|
||||
print("Installation Successful\n");
|
||||
exit(0);
|
||||
} else {
|
||||
header("Location: index.php?flash=Installation%20complete");
|
||||
die_nicely(
|
||||
"Installation Successful",
|
||||
"<p>If you aren't redirected, <a href=\"index.php\">click here to Continue</a>."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$h_file_content = htmlentities($file_content);
|
||||
throw new InstallerException(
|
||||
|
||||
@@ -16,12 +16,12 @@ define("SCORE_LOG_DEBUG", 10);
|
||||
define("SCORE_LOG_NOTSET", 0);
|
||||
|
||||
const LOGGING_LEVEL_NAMES = [
|
||||
SCORE_LOG_NOTSET=>"Not Set",
|
||||
SCORE_LOG_DEBUG=>"Debug",
|
||||
SCORE_LOG_INFO=>"Info",
|
||||
SCORE_LOG_WARNING=>"Warning",
|
||||
SCORE_LOG_ERROR=>"Error",
|
||||
SCORE_LOG_CRITICAL=>"Critical",
|
||||
SCORE_LOG_NOTSET => "Not Set",
|
||||
SCORE_LOG_DEBUG => "Debug",
|
||||
SCORE_LOG_INFO => "Info",
|
||||
SCORE_LOG_WARNING => "Warning",
|
||||
SCORE_LOG_ERROR => "Error",
|
||||
SCORE_LOG_CRITICAL => "Critical",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ const LOGGING_LEVEL_NAMES = [
|
||||
* When taking action, a log event should be stored by the server
|
||||
* Quite often, both of these happen at once, hence log_*() having $flash
|
||||
*/
|
||||
function log_msg(string $section, int $priority, string $message, ?string $flash=null)
|
||||
function log_msg(string $section, int $priority, string $message, ?string $flash = null): void
|
||||
{
|
||||
global $page;
|
||||
send_event(new LogEvent($section, $priority, $message));
|
||||
@@ -47,23 +47,23 @@ function log_msg(string $section, int $priority, string $message, ?string $flash
|
||||
}
|
||||
|
||||
// More shorthand ways of logging
|
||||
function log_debug(string $section, string $message, ?string $flash=null)
|
||||
function log_debug(string $section, string $message, ?string $flash = null): void
|
||||
{
|
||||
log_msg($section, SCORE_LOG_DEBUG, $message, $flash);
|
||||
}
|
||||
function log_info(string $section, string $message, ?string $flash=null)
|
||||
function log_info(string $section, string $message, ?string $flash = null): void
|
||||
{
|
||||
log_msg($section, SCORE_LOG_INFO, $message, $flash);
|
||||
}
|
||||
function log_warning(string $section, string $message, ?string $flash=null)
|
||||
function log_warning(string $section, string $message, ?string $flash = null): void
|
||||
{
|
||||
log_msg($section, SCORE_LOG_WARNING, $message, $flash);
|
||||
}
|
||||
function log_error(string $section, string $message, ?string $flash=null)
|
||||
function log_error(string $section, string $message, ?string $flash = null): void
|
||||
{
|
||||
log_msg($section, SCORE_LOG_ERROR, $message, $flash);
|
||||
}
|
||||
function log_critical(string $section, string $message, ?string $flash=null)
|
||||
function log_critical(string $section, string $message, ?string $flash = null): void
|
||||
{
|
||||
log_msg($section, SCORE_LOG_CRITICAL, $message, $flash);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Shimmie2;
|
||||
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\emptyHTML;
|
||||
use function MicroHTML\{emptyHTML};
|
||||
use function MicroHTML\A;
|
||||
use function MicroHTML\FORM;
|
||||
use function MicroHTML\INPUT;
|
||||
@@ -15,12 +15,8 @@ use function MicroHTML\OPTION;
|
||||
use function MicroHTML\PRE;
|
||||
use function MicroHTML\P;
|
||||
use function MicroHTML\SELECT;
|
||||
use function MicroHTML\TABLE;
|
||||
use function MicroHTML\THEAD;
|
||||
use function MicroHTML\TFOOT;
|
||||
use function MicroHTML\TR;
|
||||
use function MicroHTML\TH;
|
||||
use function MicroHTML\TD;
|
||||
use function MicroHTML\SPAN;
|
||||
use function MicroHTML\{TABLE,THEAD,TFOOT,TR,TH,TD};
|
||||
|
||||
function SHM_FORM(string $target, bool $multipart = false, string $form_id = "", string $onsubmit = "", string $name = ""): HTMLElement
|
||||
{
|
||||
@@ -50,21 +46,30 @@ function SHM_FORM(string $target, bool $multipart = false, string $form_id = "",
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
|
||||
/**
|
||||
* @param array<string|HTMLElement|null> $children
|
||||
*/
|
||||
function SHM_SIMPLE_FORM(string $target, ...$children): HTMLElement
|
||||
{
|
||||
$form = SHM_FORM($target);
|
||||
$form->appendChild(emptyHTML(...$children));
|
||||
return $form;
|
||||
}
|
||||
|
||||
function SHM_SUBMIT(string $text, array $args=[]): HTMLElement
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
function SHM_SUBMIT(string $text, array $args = []): HTMLElement
|
||||
{
|
||||
$args["type"] = "submit";
|
||||
$args["value"] = $text;
|
||||
return INPUT($args);
|
||||
}
|
||||
|
||||
function SHM_A(string $href, string|HTMLElement $text, string $id="", string $class="", array $args=[]): HTMLElement
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
function SHM_A(string $href, string|HTMLElement $text, string $id = "", string $class = "", array $args = []): HTMLElement
|
||||
{
|
||||
$args["href"] = make_link($href);
|
||||
|
||||
@@ -81,24 +86,24 @@ function SHM_A(string $href, string|HTMLElement $text, string $id="", string $cl
|
||||
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
|
||||
{
|
||||
return DIV(
|
||||
["class"=>"command_example"],
|
||||
["class" => "command_example"],
|
||||
PRE($ex),
|
||||
P($desc)
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, HTMLElement $body, HTMLElement|string $foot): HTMLElement
|
||||
{
|
||||
if (is_string($foot)) {
|
||||
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
|
||||
$foot = TFOOT(TR(TD(["colspan" => "2"], INPUT(["type" => "submit", "value" => $foot]))));
|
||||
}
|
||||
return SHM_SIMPLE_FORM(
|
||||
$target,
|
||||
P(
|
||||
INPUT(["type"=>'hidden', "name"=>'id', "value"=>$duser->id]),
|
||||
INPUT(["type" => 'hidden', "name" => 'id', "value" => $duser->id]),
|
||||
TABLE(
|
||||
["class"=>"form"],
|
||||
THEAD(TR(TH(["colspan"=>"2"], $title))),
|
||||
["class" => "form"],
|
||||
THEAD(TR(TH(["colspan" => "2"], $title))),
|
||||
$body,
|
||||
$foot
|
||||
)
|
||||
@@ -110,14 +115,14 @@ function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
|
||||
* Generates a <select> element and sets up the given options.
|
||||
*
|
||||
* @param string $name The name attribute of <select>.
|
||||
* @param array $options An array of pairs of parameters for <option> tags. First one is value, second one is text. Example: ('optionA', 'Choose Option A').
|
||||
* @param array $selected_options The values of options that should be pre-selected.
|
||||
* @param array<string|int, string> $options An array of pairs of parameters for <option> tags. First one is value, second one is text. Example: ('optionA', 'Choose Option A').
|
||||
* @param array<string> $selected_options The values of options that should be pre-selected.
|
||||
* @param bool $required Wether the <select> element is required.
|
||||
* @param bool $multiple Wether the <select> element is multiple-choice.
|
||||
* @param bool $empty_option Whether the first option should be an empty one.
|
||||
* @param array $attrs Additional attributes dict for <select>. Example: ["id"=>"some_id", "class"=>"some_class"].
|
||||
* @param array<string, mixed> $attrs Additional attributes dict for <select>. Example: ["id"=>"some_id", "class"=>"some_class"].
|
||||
*/
|
||||
function SHM_SELECT(string $name, array $options, array $selected_options=[], bool $required=false, bool $multiple=false, bool $empty_option=false, array $attrs=[]): HTMLElement
|
||||
function SHM_SELECT(string $name, array $options, array $selected_options = [], bool $required = false, bool $multiple = false, bool $empty_option = false, array $attrs = []): HTMLElement
|
||||
{
|
||||
if ($required) {
|
||||
$attrs["required"] = "";
|
||||
@@ -143,10 +148,10 @@ function SHM_SELECT(string $name, array $options, array $selected_options=[], bo
|
||||
return SELECT($attrs, ...$_options);
|
||||
}
|
||||
|
||||
function SHM_OPTION(string $value, string $text, bool $selected=false): HTMLElement
|
||||
function SHM_OPTION(string $value, string $text, bool $selected = false): HTMLElement
|
||||
{
|
||||
if ($selected) {
|
||||
return OPTION(["value"=>$value, "selected"=>""], $text);
|
||||
return OPTION(["value" => $value, "selected" => ""], $text);
|
||||
}
|
||||
|
||||
return OPTION(["value" => $value], $text);
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace Shimmie2;
|
||||
|
||||
/**
|
||||
* Return the unique elements of an array, case insensitively
|
||||
*
|
||||
* @param array<string> $array
|
||||
* @return list<string>
|
||||
*/
|
||||
function array_iunique(array $array): array
|
||||
{
|
||||
@@ -54,42 +57,16 @@ function ip_in_range(string $IP, string $CIDR): bool
|
||||
|
||||
/**
|
||||
* Delete an entire file heirachy
|
||||
*
|
||||
* from a patch by Christian Walde; only intended for use in the
|
||||
* "extension manager" extension, but it seems to fit better here
|
||||
*/
|
||||
function deltree(string $f): void
|
||||
function deltree(string $dir): void
|
||||
{
|
||||
//Because Windows (I know, bad excuse)
|
||||
if (PHP_OS === 'WINNT') {
|
||||
$real = realpath($f);
|
||||
$path = realpath('./').'\\'.str_replace('/', '\\', $f);
|
||||
if ($path != $real) {
|
||||
rmdir($path);
|
||||
} else {
|
||||
foreach (glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
} else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
} else {
|
||||
if (is_link($f)) {
|
||||
unlink($f);
|
||||
} elseif (is_dir($f)) {
|
||||
foreach (glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
} else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
$di = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::KEY_AS_PATHNAME);
|
||||
$ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST);
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($ri as $filename => $file) {
|
||||
$file->isDir() ? rmdir($filename) : unlink($filename);
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,9 +79,13 @@ function full_copy(string $source, string $target): void
|
||||
if (is_dir($source)) {
|
||||
@mkdir($target);
|
||||
|
||||
$d = dir($source);
|
||||
$d = dir_ex($source);
|
||||
|
||||
while (false !== ($entry = $d->read())) {
|
||||
while (true) {
|
||||
$entry = $d->read();
|
||||
if ($entry === false) {
|
||||
break;
|
||||
}
|
||||
if ($entry == '.' || $entry == '..') {
|
||||
continue;
|
||||
}
|
||||
@@ -124,8 +105,10 @@ function full_copy(string $source, string $target): void
|
||||
|
||||
/**
|
||||
* Return a list of all the regular files in a directory and subdirectories
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function list_files(string $base, string $_sub_dir=""): array
|
||||
function list_files(string $base, string $_sub_dir = ""): array
|
||||
{
|
||||
assert(is_dir($base));
|
||||
|
||||
@@ -185,6 +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);
|
||||
}
|
||||
echo fread($fp, $buffer);
|
||||
flush_output();
|
||||
@@ -204,13 +188,13 @@ function stream_file(string $file, int $start, int $end): void
|
||||
# http://www.php.net/manual/en/function.http-parse-headers.php#112917
|
||||
if (!function_exists('http_parse_headers')) {
|
||||
/**
|
||||
* #return string[]
|
||||
* @return array<string, string|string[]>
|
||||
*/
|
||||
function http_parse_headers(string $raw_headers): array
|
||||
{
|
||||
$headers = []; // $headers = [];
|
||||
$headers = [];
|
||||
|
||||
foreach (explode("\n", $raw_headers) as $i => $h) {
|
||||
foreach (explode("\n", $raw_headers) as $h) {
|
||||
$h = explode(':', $h, 2);
|
||||
|
||||
if (isset($h[1])) {
|
||||
@@ -232,6 +216,8 @@ if (!function_exists('http_parse_headers')) {
|
||||
/**
|
||||
* HTTP Headers can sometimes be lowercase which will cause issues.
|
||||
* In cases like these, we need to make sure to check for them if the camelcase version does not exist.
|
||||
*
|
||||
* @param array<string, mixed> $headers
|
||||
*/
|
||||
function find_header(array $headers, string $name): ?string
|
||||
{
|
||||
@@ -268,6 +254,8 @@ function get_subclasses_of(string $parent): array
|
||||
|
||||
/**
|
||||
* Like glob, with support for matching very long patterns with braces.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
function zglob(string $pattern): array
|
||||
{
|
||||
@@ -289,52 +277,6 @@ function zglob(string $pattern): array
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the path to the shimmie install directory.
|
||||
*
|
||||
* eg if shimmie is visible at https://foo.com/gallery, this
|
||||
* function should return /gallery
|
||||
*
|
||||
* PHP really, really sucks.
|
||||
*/
|
||||
function get_base_href(): string
|
||||
{
|
||||
if (defined("BASE_HREF") && !empty(BASE_HREF)) {
|
||||
return BASE_HREF;
|
||||
}
|
||||
$possible_vars = ['SCRIPT_NAME', 'PHP_SELF', 'PATH_INFO', 'ORIG_PATH_INFO'];
|
||||
$ok_var = null;
|
||||
foreach ($possible_vars as $var) {
|
||||
if (isset($_SERVER[$var]) && substr($_SERVER[$var], -4) === '.php') {
|
||||
$ok_var = $_SERVER[$var];
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(!empty($ok_var));
|
||||
$dir = dirname($ok_var);
|
||||
$dir = str_replace("\\", "/", $dir);
|
||||
$dir = str_replace("//", "/", $dir);
|
||||
$dir = rtrim($dir, "/");
|
||||
return $dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The opposite of the standard library's parse_url
|
||||
*/
|
||||
function unparse_url(array $parsed_url): string
|
||||
{
|
||||
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
|
||||
$host = $parsed_url['host'] ?? '';
|
||||
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
|
||||
$user = $parsed_url['user'] ?? '';
|
||||
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
|
||||
$pass = ($user || $pass) ? "$pass@" : '';
|
||||
$path = $parsed_url['path'] ?? '';
|
||||
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
|
||||
$fragment = !empty($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
|
||||
return "$scheme$user$pass$host$port$path$query$fragment";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Input / Output Sanitising *
|
||||
@@ -389,7 +331,7 @@ function url_escape(?string $input): string
|
||||
/**
|
||||
* Turn all manner of HTML / INI / JS / DB booleans into a PHP one
|
||||
*/
|
||||
function bool_escape($input): bool
|
||||
function bool_escape(mixed $input): bool
|
||||
{
|
||||
/*
|
||||
Sometimes, I don't like PHP -- this, is one of those times...
|
||||
@@ -432,7 +374,7 @@ function no_escape(string $input): string
|
||||
* Given a 1-indexed numeric-ish thing, return a zero-indexed
|
||||
* number between 0 and $max
|
||||
*/
|
||||
function page_number(string $input, ?int $max=null): int
|
||||
function page_number(string $input, ?int $max = null): int
|
||||
{
|
||||
if (!is_numeric($input)) {
|
||||
$pageNumber = 0;
|
||||
@@ -443,10 +385,23 @@ function page_number(string $input, ?int $max=null): int
|
||||
} else {
|
||||
$pageNumber = $input - 1;
|
||||
}
|
||||
return $pageNumber;
|
||||
return (int)$pageNumber;
|
||||
}
|
||||
|
||||
function clamp(int $val, ?int $min=null, ?int $max=null): int
|
||||
function is_numberish(string $s): bool
|
||||
{
|
||||
return is_numeric($s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Because apparently phpstan thinks that if $i is an int, type(-$i) == int|float
|
||||
*/
|
||||
function negative_int(int $i): int
|
||||
{
|
||||
return -$i;
|
||||
}
|
||||
|
||||
function clamp(int $val, ?int $min = null, ?int $max = null): int
|
||||
{
|
||||
if (!is_null($min) && $val < $min) {
|
||||
$val = $min;
|
||||
@@ -522,17 +477,17 @@ function to_shorthand_int(int|float $int): string
|
||||
{
|
||||
assert($int >= 0);
|
||||
|
||||
if ($int >= pow(1024, 4)) {
|
||||
return sprintf("%.1fTB", $int / pow(1024, 4));
|
||||
} elseif ($int >= pow(1024, 3)) {
|
||||
return sprintf("%.1fGB", $int / pow(1024, 3));
|
||||
} elseif ($int >= pow(1024, 2)) {
|
||||
return sprintf("%.1fMB", $int / pow(1024, 2));
|
||||
} elseif ($int >= 1024) {
|
||||
return sprintf("%.1fKB", $int / 1024);
|
||||
} else {
|
||||
return (string)$int;
|
||||
}
|
||||
return match (true) {
|
||||
$int >= pow(1024, 4) * 10 => sprintf("%.0fTB", $int / pow(1024, 4)),
|
||||
$int >= pow(1024, 4) => sprintf("%.1fTB", $int / pow(1024, 4)),
|
||||
$int >= pow(1024, 3) * 10 => sprintf("%.0fGB", $int / pow(1024, 3)),
|
||||
$int >= pow(1024, 3) => sprintf("%.1fGB", $int / pow(1024, 3)),
|
||||
$int >= pow(1024, 2) * 10 => sprintf("%.0fMB", $int / pow(1024, 2)),
|
||||
$int >= pow(1024, 2) => sprintf("%.1fMB", $int / pow(1024, 2)),
|
||||
$int >= pow(1024, 1) * 10 => sprintf("%.0fKB", $int / pow(1024, 1)),
|
||||
$int >= pow(1024, 1) => sprintf("%.1fKB", $int / pow(1024, 1)),
|
||||
default => (string)$int,
|
||||
};
|
||||
}
|
||||
abstract class TIME_UNITS
|
||||
{
|
||||
@@ -543,12 +498,12 @@ abstract class TIME_UNITS
|
||||
public const DAYS = "d";
|
||||
public const YEARS = "y";
|
||||
public const CONVERSION = [
|
||||
self::MILLISECONDS=>1000,
|
||||
self::SECONDS=>60,
|
||||
self::MINUTES=>60,
|
||||
self::HOURS=>24,
|
||||
self::DAYS=>365,
|
||||
self::YEARS=>PHP_INT_MAX
|
||||
self::MILLISECONDS => 1000,
|
||||
self::SECONDS => 60,
|
||||
self::MINUTES => 60,
|
||||
self::HOURS => 24,
|
||||
self::DAYS => 365,
|
||||
self::YEARS => PHP_INT_MAX
|
||||
];
|
||||
}
|
||||
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
|
||||
@@ -559,17 +514,17 @@ function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS)
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit => $conversion) {
|
||||
$count = $remainder % $conversion;
|
||||
$remainder = floor($remainder / $conversion);
|
||||
|
||||
if ($found||$unit==$min_unit) {
|
||||
if ($found || $unit == $min_unit) {
|
||||
$found = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($count==0&&$remainder<1) {
|
||||
if ($count == 0 && $remainder < 1) {
|
||||
break;
|
||||
}
|
||||
$output = "$count".$unit." ".$output;
|
||||
@@ -590,7 +545,7 @@ function parse_to_milliseconds(string $input): int
|
||||
$output += $length;
|
||||
}
|
||||
} else {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit => $conversion) {
|
||||
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
|
||||
$length = $match[1];
|
||||
if (is_numeric($length)) {
|
||||
@@ -607,7 +562,7 @@ function parse_to_milliseconds(string $input): int
|
||||
/**
|
||||
* Turn a date into a time, a date, an "X minutes ago...", etc
|
||||
*/
|
||||
function autodate(string $date, bool $html=true): string
|
||||
function autodate(string $date, bool $html = true): string
|
||||
{
|
||||
$cpu = date('c', \Safe\strtotime($date));
|
||||
$hum = date('F j, Y; H:i', \Safe\strtotime($date));
|
||||
@@ -643,6 +598,10 @@ function isValidDate(string $date): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $inputs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function validate_input(array $inputs): array
|
||||
{
|
||||
$outputs = [];
|
||||
@@ -675,6 +634,7 @@ function validate_input(array $inputs): array
|
||||
}
|
||||
$outputs[$key] = $id;
|
||||
} elseif (in_array('user_name', $flags)) {
|
||||
// @phpstan-ignore-next-line - phpstan thinks $value can never be empty?
|
||||
if (strlen($value) < 1) {
|
||||
throw new InvalidInput("Username must be at least 1 character");
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
|
||||
@@ -685,8 +645,7 @@ function validate_input(array $inputs): array
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
} elseif (in_array('user_class', $flags)) {
|
||||
global $_shm_user_classes;
|
||||
if (!array_key_exists($value, $_shm_user_classes)) {
|
||||
if (!array_key_exists($value, UserClass::$known_classes)) {
|
||||
throw new InvalidInput("Invalid user class: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
@@ -762,6 +721,12 @@ function join_path(string ...$paths): string
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator.
|
||||
*
|
||||
* @template T
|
||||
* @template U
|
||||
* @param callable(U):T $callback
|
||||
* @param \iterator<U> $iter
|
||||
* @return \Generator<T>
|
||||
*/
|
||||
function iterator_map(callable $callback, \iterator $iter): \Generator
|
||||
{
|
||||
@@ -772,20 +737,26 @@ function iterator_map(callable $callback, \iterator $iter): \Generator
|
||||
|
||||
/**
|
||||
* Perform callback on each item returned by an iterator and combine the result into an array.
|
||||
*
|
||||
* @template T
|
||||
* @template U
|
||||
* @param callable(U):T $callback
|
||||
* @param \iterator<U> $iter
|
||||
* @return array<T>
|
||||
*/
|
||||
function iterator_map_to_array(callable $callback, \iterator $iter): array
|
||||
{
|
||||
return iterator_to_array(iterator_map($callback, $iter));
|
||||
}
|
||||
|
||||
function stringer($s): string
|
||||
function stringer(mixed $s): string
|
||||
{
|
||||
if (is_array($s)) {
|
||||
if (isset($s[0])) {
|
||||
return "[" . implode(", ", array_map("Shimmie2\stringer", $s)) . "]";
|
||||
} else {
|
||||
$pairs = [];
|
||||
foreach ($s as $k=>$v) {
|
||||
foreach ($s as $k => $v) {
|
||||
$pairs[] = "\"$k\"=>" . stringer($v);
|
||||
}
|
||||
return "[" . implode(", ", $pairs) . "]";
|
||||
@@ -808,3 +779,24 @@ function stringer($s): string
|
||||
}
|
||||
return "<Unstringable>";
|
||||
}
|
||||
|
||||
/**
|
||||
* If a value is in the cache, return it; otherwise, call the callback
|
||||
* to generate it and store it in the cache.
|
||||
*
|
||||
* @template T
|
||||
* @param string $key
|
||||
* @param callable():T $callback
|
||||
* @param int|null $ttl
|
||||
* @return T
|
||||
*/
|
||||
function cache_get_or_set(string $key, callable $callback, ?int $ttl = null): mixed
|
||||
{
|
||||
global $cache;
|
||||
$value = $cache->get($key);
|
||||
if ($value === null) {
|
||||
$value = $callback();
|
||||
$cache->set($key, $value, $ttl);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ require_once "core/urls.php";
|
||||
* be included right at the very start of index.php and tests/bootstrap.php
|
||||
*/
|
||||
|
||||
function die_nicely($title, $body, $code=0)
|
||||
function die_nicely(string $title, string $body, int $code = 0): void
|
||||
{
|
||||
$data_href = get_base_href();
|
||||
print("<!DOCTYPE html>
|
||||
|
||||
@@ -23,7 +23,10 @@ function _load_event_listeners(): void
|
||||
|
||||
$_tracer->begin("Load Event Listeners");
|
||||
|
||||
$cache_path = data_path("cache/shm_event_listeners.php");
|
||||
$ver = preg_replace("/[^a-zA-Z0-9\.]/", "_", VERSION);
|
||||
$key = md5(Extension::get_enabled_extensions_as_string());
|
||||
|
||||
$cache_path = data_path("cache/event_listeners/el.$ver.$key.php");
|
||||
if (SPEED_HAX && file_exists($cache_path)) {
|
||||
require_once($cache_path);
|
||||
} else {
|
||||
@@ -49,7 +52,7 @@ function _set_event_listeners(): void
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = [];
|
||||
|
||||
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
|
||||
foreach (get_subclasses_of(Extension::class) as $class) {
|
||||
/** @var Extension $extension */
|
||||
$extension = new $class();
|
||||
|
||||
@@ -76,11 +79,16 @@ function _namespaced_class_name(string $class): string
|
||||
return str_replace("Shimmie2\\", "", $class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the event listeners to a file for faster loading.
|
||||
*
|
||||
* @param array<string, array<int, Extension>> $event_listeners
|
||||
*/
|
||||
function _dump_event_listeners(array $event_listeners, string $path): void
|
||||
{
|
||||
$p = "<"."?php\nnamespace Shimmie2;\n";
|
||||
|
||||
foreach (get_subclasses_of("Shimmie2\Extension") as $class) {
|
||||
foreach (get_subclasses_of(Extension::class) as $class) {
|
||||
$scn = _namespaced_class_name($class);
|
||||
$p .= "\$$scn = new $scn(); ";
|
||||
}
|
||||
@@ -104,7 +112,7 @@ global $_shm_event_count;
|
||||
$_shm_event_count = 0;
|
||||
$_shm_timeout = null;
|
||||
|
||||
function shm_set_timeout(?int $timeout=null): void
|
||||
function shm_set_timeout(?int $timeout = null): void
|
||||
{
|
||||
global $_shm_timeout;
|
||||
if ($timeout) {
|
||||
@@ -159,7 +167,7 @@ function send_event(Event $event): Event
|
||||
if ($tracer_enabled) {
|
||||
$_tracer->end();
|
||||
}
|
||||
if ($event->stop_processing===true) {
|
||||
if ($event->stop_processing === true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
34
core/stdlib_ex.php
Normal file
34
core/stdlib_ex.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param T|false $x
|
||||
* @return T
|
||||
*/
|
||||
function false_throws(mixed $x, ?callable $errorgen = null): mixed
|
||||
{
|
||||
if ($x === false) {
|
||||
$msg = "Unexpected false";
|
||||
if ($errorgen) {
|
||||
$msg = $errorgen();
|
||||
}
|
||||
throw new \Exception($msg);
|
||||
}
|
||||
return $x;
|
||||
}
|
||||
|
||||
# https://github.com/thecodingmachine/safe/pull/428
|
||||
function inet_pton_ex(string $ip_address): string
|
||||
{
|
||||
return false_throws(inet_pton($ip_address));
|
||||
}
|
||||
|
||||
function dir_ex(string $directory): \Directory
|
||||
{
|
||||
return false_throws(dir($directory));
|
||||
}
|
||||
|
||||
function filter_var_ex(mixed $variable, int $filter = FILTER_DEFAULT, mixed $options = null): mixed
|
||||
{
|
||||
return false_throws(filter_var($variable, $filter, $options));
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Shimmie2;
|
||||
* Shimmie will set the values to their defaults
|
||||
*
|
||||
* All of these can be over-ridden by placing a 'define' in
|
||||
* data/config/shimmie.conf.php
|
||||
* data/config/shimmie.conf.php.
|
||||
*
|
||||
* Do NOT change them in this file. These are the defaults only!
|
||||
*
|
||||
@@ -17,13 +17,13 @@ namespace Shimmie2;
|
||||
* define("SPEED_HAX", true);
|
||||
*/
|
||||
|
||||
function _d(string $name, $value): void
|
||||
function _d(string $name, mixed $value): void
|
||||
{
|
||||
if (!defined($name)) {
|
||||
define($name, $value);
|
||||
}
|
||||
}
|
||||
$_g = file_exists(".git") ? '+' : '';
|
||||
|
||||
_d("DATABASE_DSN", null); // string PDO database connection details
|
||||
_d("DATABASE_TIMEOUT", 10000); // int Time to wait for each statement to complete
|
||||
_d("CACHE_DSN", null); // string cache connection details
|
||||
@@ -37,4 +37,4 @@ _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("REVERSE_PROXY_X_HEADERS", false); // boolean get request IPs from "X-Real-IP" and protocol from "X-Forwarded-Proto" HTTP headers
|
||||
_d("TRUSTED_PROXIES", []); // array trust "X-Real-IP" / "X-Forwarded-For" / "X-Forwarded-Proto" headers from these IP ranges
|
||||
|
||||
282
core/testcase.php
Normal file
282
core/testcase.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
if (class_exists("\\PHPUnit\\Framework\\TestCase")) {
|
||||
abstract class ShimmiePHPUnitTestCase extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
protected static string $anon_name = "anonymous";
|
||||
protected static string $admin_name = "demo";
|
||||
protected static string $user_name = "test";
|
||||
protected string $wipe_time = "test";
|
||||
|
||||
/**
|
||||
* Start a DB transaction for each test class
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
global $_tracer, $database;
|
||||
$_tracer->begin(get_called_class());
|
||||
$database->begin_transaction();
|
||||
parent::setUpBeforeClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a savepoint for each test
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
global $database, $_tracer;
|
||||
$_tracer->begin($this->name());
|
||||
$_tracer->begin("setUp");
|
||||
$class = str_replace("Test", "", get_class($this));
|
||||
try {
|
||||
if (!ExtensionInfo::get_for_extension_class($class)->is_supported()) {
|
||||
$this->markTestSkipped("$class not supported with this database");
|
||||
}
|
||||
} catch (ExtensionNotFound $e) {
|
||||
// ignore - this is a core test rather than an extension test
|
||||
}
|
||||
|
||||
// Set up a clean environment for each test
|
||||
$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));
|
||||
}
|
||||
|
||||
$_tracer->end(); # setUp
|
||||
$_tracer->begin("test");
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
global $_tracer, $database;
|
||||
$database->execute("ROLLBACK TO test_start");
|
||||
$_tracer->end(); # test
|
||||
$_tracer->end(); # $this->getName()
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
global $_tracer, $database;
|
||||
$database->rollback();
|
||||
$_tracer->end(); # get_called_class()
|
||||
$_tracer->clear();
|
||||
$_tracer->flush("data/test-trace.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
* @return array<string, string|mixed[]>
|
||||
*/
|
||||
private static function check_args(array $args): array
|
||||
{
|
||||
if (!$args) {
|
||||
return [];
|
||||
}
|
||||
foreach ($args as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$args[$k] = $v;
|
||||
} else {
|
||||
$args[$k] = (string)$v;
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $get_args
|
||||
* @param array<string, mixed> $post_args
|
||||
*/
|
||||
protected static function request(
|
||||
string $method,
|
||||
string $page_name,
|
||||
array $get_args = [],
|
||||
array $post_args = []
|
||||
): Page {
|
||||
// use a fresh page
|
||||
global $page;
|
||||
$get_args = self::check_args($get_args);
|
||||
$post_args = self::check_args($post_args);
|
||||
|
||||
if (str_contains($page_name, "?")) {
|
||||
throw new \RuntimeException("Query string included in page name");
|
||||
}
|
||||
$_SERVER['REQUEST_METHOD'] = $method;
|
||||
$_SERVER['REQUEST_URI'] = make_link($page_name, http_build_query($get_args));
|
||||
$_GET = $get_args;
|
||||
$_POST = $post_args;
|
||||
$page = new Page();
|
||||
send_event(new PageRequestEvent($method, $page_name, $get_args, $post_args));
|
||||
if ($page->mode == PageMode::REDIRECT) {
|
||||
$page->code = 302;
|
||||
}
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
protected static function get_page(string $page_name, array $args = []): Page
|
||||
{
|
||||
return self::request("GET", $page_name, $args, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $args
|
||||
*/
|
||||
protected static function post_page(string $page_name, array $args = []): Page
|
||||
{
|
||||
return self::request("POST", $page_name, [], $args);
|
||||
}
|
||||
|
||||
// page things
|
||||
protected function assert_title(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringContainsString($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_title_matches(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringMatchesFormat($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_no_title(string $title): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringNotContainsString($title, $page->title);
|
||||
}
|
||||
|
||||
protected function assert_response(int $code): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertEquals($code, $page->code);
|
||||
}
|
||||
|
||||
protected function page_to_text(string $section = null): 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";
|
||||
}
|
||||
}
|
||||
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)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the page contains the given text somewhere in the blocks
|
||||
*/
|
||||
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
|
||||
{
|
||||
$this->assertStringNotContainsString($text, $this->page_to_text($section));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the page contains the given text somewhere in the binary data
|
||||
*/
|
||||
protected function assert_content(string $content): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringContainsString($content, $page->data);
|
||||
}
|
||||
|
||||
protected function assert_no_content(string $content): void
|
||||
{
|
||||
global $page;
|
||||
$this->assertStringNotContainsString($content, $page->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param int[] $results
|
||||
*/
|
||||
protected function assert_search_results(array $tags, array $results): void
|
||||
{
|
||||
$images = Search::find_images(0, null, $tags);
|
||||
$ids = [];
|
||||
foreach ($images as $image) {
|
||||
$ids[] = $image->id;
|
||||
}
|
||||
$this->assertEquals($results, $ids);
|
||||
}
|
||||
|
||||
protected function assertException(string $type, callable $function): \Exception|null
|
||||
{
|
||||
$exception = null;
|
||||
try {
|
||||
call_user_func($function);
|
||||
} catch (\Exception $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
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
|
||||
protected static function log_in_as_admin(): void
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
}
|
||||
|
||||
protected static function log_in_as_user(): void
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$user_name)));
|
||||
}
|
||||
|
||||
protected static function log_out(): void
|
||||
{
|
||||
global $config;
|
||||
send_event(new UserLoginEvent(User::by_id($config->get_int("anon_id", 0))));
|
||||
}
|
||||
|
||||
// post things
|
||||
protected function post_image(string $filename, string $tags): int
|
||||
{
|
||||
$dae = send_event(new DataUploadEvent($filename, basename($filename), 0, [
|
||||
"filename" => $filename,
|
||||
"tags" => $tags,
|
||||
]));
|
||||
if (count($dae->images) == 0) {
|
||||
throw new \Exception("Upload failed :(");
|
||||
}
|
||||
return $dae->images[0]->id;
|
||||
}
|
||||
|
||||
protected function delete_image(int $image_id): void
|
||||
{
|
||||
$img = Image::by_id($image_id);
|
||||
if ($img) {
|
||||
send_event(new ImageDeletionEvent($img, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
abstract class ShimmiePHPUnitTestCase
|
||||
{
|
||||
}
|
||||
}
|
||||
63
core/tests/BasePageTest.php
Normal file
63
core/tests/BasePageTest.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/basepage.php";
|
||||
|
||||
class BasePageTest extends TestCase
|
||||
{
|
||||
public function test_page(): void
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::PAGE);
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_file(): void
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::FILE);
|
||||
$page->set_file("tests/pbx_screenshot.jpg");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_data(): void
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_data("hello world");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_redirect(): void
|
||||
{
|
||||
$page = new BasePage();
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect("/new/page");
|
||||
ob_start();
|
||||
$page->display();
|
||||
ob_end_clean();
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
|
||||
public function test_subNav(): void
|
||||
{
|
||||
// the default theme doesn't send this, so let's have
|
||||
// a random test manually
|
||||
send_event(new PageSubNavBuildingEvent("system"));
|
||||
$this->assertTrue(true); // doesn't crash
|
||||
}
|
||||
}
|
||||
21
core/tests/BlockTest.php
Normal file
21
core/tests/BlockTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/block.php";
|
||||
|
||||
class BlockTest extends TestCase
|
||||
{
|
||||
public function test_basic(): void
|
||||
{
|
||||
$b = new Block("head", "body");
|
||||
$this->assertEquals(
|
||||
"<section id='headmain'><h3 data-toggle-sel='#headmain' class=''>head</h3><div class='blockbody'>body</div></section>\n",
|
||||
$b->get_html()
|
||||
);
|
||||
}
|
||||
}
|
||||
22
core/tests/InitTest.php
Normal file
22
core/tests/InitTest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class InitTest extends TestCase
|
||||
{
|
||||
public function testInitExt(): void
|
||||
{
|
||||
send_event(new InitExtEvent());
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testDatabaseUpgrade(): void
|
||||
{
|
||||
send_event(new DatabaseUpgradeEvent());
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
258
core/tests/PolyfillsTest.php
Normal file
258
core/tests/PolyfillsTest.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/polyfills.php";
|
||||
|
||||
class PolyfillsTest extends TestCase
|
||||
{
|
||||
public function test_html_escape(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"Foo & <main>",
|
||||
html_escape("Foo & <main>")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"Foo & <main>",
|
||||
html_unescape("Foo & <main>")
|
||||
);
|
||||
|
||||
$x = "Foo & <waffles>";
|
||||
$this->assertEquals(html_escape(html_unescape($x)), $x);
|
||||
}
|
||||
|
||||
public function test_int_escape(): void
|
||||
{
|
||||
$this->assertEquals(0, int_escape(""));
|
||||
$this->assertEquals(1, int_escape("1"));
|
||||
$this->assertEquals(-1, int_escape("-1"));
|
||||
$this->assertEquals(-1, int_escape("-1.5"));
|
||||
$this->assertEquals(0, int_escape(null));
|
||||
}
|
||||
|
||||
public function test_url_escape(): void
|
||||
{
|
||||
$this->assertEquals("%5E%5Co%2F%5E", url_escape("^\o/^"));
|
||||
$this->assertEquals("", url_escape(null));
|
||||
}
|
||||
|
||||
public function test_bool_escape(): void
|
||||
{
|
||||
$this->assertTrue(bool_escape(true));
|
||||
$this->assertFalse(bool_escape(false));
|
||||
|
||||
$this->assertTrue(bool_escape("true"));
|
||||
$this->assertFalse(bool_escape("false"));
|
||||
|
||||
$this->assertTrue(bool_escape("t"));
|
||||
$this->assertFalse(bool_escape("f"));
|
||||
|
||||
$this->assertTrue(bool_escape("T"));
|
||||
$this->assertFalse(bool_escape("F"));
|
||||
|
||||
$this->assertTrue(bool_escape("yes"));
|
||||
$this->assertFalse(bool_escape("no"));
|
||||
|
||||
$this->assertTrue(bool_escape("Yes"));
|
||||
$this->assertFalse(bool_escape("No"));
|
||||
|
||||
$this->assertTrue(bool_escape("on"));
|
||||
$this->assertFalse(bool_escape("off"));
|
||||
|
||||
$this->assertTrue(bool_escape(1));
|
||||
$this->assertFalse(bool_escape(0));
|
||||
|
||||
$this->assertTrue(bool_escape("1"));
|
||||
$this->assertFalse(bool_escape("0"));
|
||||
}
|
||||
|
||||
public function test_clamp(): void
|
||||
{
|
||||
$this->assertEquals(5, clamp(0, 5, 10)); // too small
|
||||
$this->assertEquals(5, clamp(5, 5, 10)); // lower limit
|
||||
$this->assertEquals(7, clamp(7, 5, 10)); // ok
|
||||
$this->assertEquals(10, clamp(10, 5, 10)); // upper limit
|
||||
$this->assertEquals(10, clamp(15, 5, 10)); // too large
|
||||
$this->assertEquals(0, clamp(0, null, 10)); // no lower limit
|
||||
$this->assertEquals(10, clamp(10, 0, null)); // no upper limit
|
||||
$this->assertEquals(42, clamp(42, null, null)); // no limit
|
||||
}
|
||||
|
||||
public function test_truncate(): void
|
||||
{
|
||||
$this->assertEquals("test words", truncate("test words", 10), "No truncation if string is short enough");
|
||||
$this->assertEquals("test...", truncate("test words", 9), "Truncate when string is too long");
|
||||
$this->assertEquals("test...", truncate("test words", 7), "Truncate to the same breakpoint");
|
||||
$this->assertEquals("te...", truncate("test words", 5), "Breakpoints past the limit don't matter");
|
||||
$this->assertEquals("o...", truncate("oneVeryLongWord", 4), "Hard-break if there are no breakpoints");
|
||||
}
|
||||
|
||||
public function test_to_shorthand_int(): void
|
||||
{
|
||||
// 0-9 should have 1 decimal place, 10+ should have none
|
||||
$this->assertEquals("1.1GB", to_shorthand_int(1231231231));
|
||||
$this->assertEquals("10KB", to_shorthand_int(10240));
|
||||
$this->assertEquals("9.2KB", to_shorthand_int(9440));
|
||||
$this->assertEquals("2", to_shorthand_int(2));
|
||||
}
|
||||
|
||||
public function test_parse_shorthand_int(): void
|
||||
{
|
||||
$this->assertEquals(-1, parse_shorthand_int("foo"));
|
||||
$this->assertEquals(33554432, parse_shorthand_int("32M"));
|
||||
$this->assertEquals(44441, parse_shorthand_int("43.4KB"));
|
||||
$this->assertEquals(1231231231, parse_shorthand_int("1231231231"));
|
||||
}
|
||||
|
||||
public function test_format_milliseconds(): void
|
||||
{
|
||||
$this->assertEquals("", format_milliseconds(5));
|
||||
$this->assertEquals("5s", format_milliseconds(5000));
|
||||
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
|
||||
}
|
||||
|
||||
public function test_parse_to_milliseconds(): void
|
||||
{
|
||||
$this->assertEquals(10, parse_to_milliseconds("10"));
|
||||
$this->assertEquals(5000, parse_to_milliseconds("5s"));
|
||||
$this->assertEquals(50000000000, parse_to_milliseconds("1y 213d 16h 53m 20s"));
|
||||
}
|
||||
|
||||
public function test_autodate(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<time datetime='2012-06-23T16:14:22+00:00'>June 23, 2012; 16:14</time>",
|
||||
autodate("2012-06-23 16:14:22")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_validate_input(): void
|
||||
{
|
||||
$_POST = [
|
||||
"foo" => " bar ",
|
||||
"to_null" => " ",
|
||||
"num" => "42",
|
||||
];
|
||||
$this->assertEquals(
|
||||
["foo" => "bar"],
|
||||
validate_input(["foo" => "string,trim,lower"])
|
||||
);
|
||||
//$this->assertEquals(
|
||||
// ["to_null"=>null],
|
||||
// validate_input(["to_null"=>"string,trim,nullify"])
|
||||
//);
|
||||
$this->assertEquals(
|
||||
["num" => 42],
|
||||
validate_input(["num" => "int"])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_sanitize_path(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
sanitize_path("one")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one/two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one//two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one\\\\\\two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
sanitize_path("one///two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR,
|
||||
sanitize_path("\\/one/\\/\\/two\\/")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_join_path(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"one",
|
||||
join_path("one")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two",
|
||||
join_path("one", "two")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
|
||||
join_path("one", "two", "three")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three",
|
||||
join_path("one/two", "three")
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
DIRECTORY_SEPARATOR."one".DIRECTORY_SEPARATOR."two".DIRECTORY_SEPARATOR."three".DIRECTORY_SEPARATOR,
|
||||
join_path("\\/////\\\\one/\///"."\\//two\/\\//\\//", "//\/\\\/three/\\/\/")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_stringer(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'["foo"=>"bar", "baz"=>[1, 2, 3], "qux"=>["a"=>"b"]]',
|
||||
stringer(["foo" => "bar", "baz" => [1,2,3], "qux" => ["a" => "b"]])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_ip_in_range(): void
|
||||
{
|
||||
$this->assertTrue(ip_in_range("1.2.3.4", "1.2.0.0/16"));
|
||||
$this->assertFalse(ip_in_range("4.3.2.1", "1.2.0.0/16"));
|
||||
|
||||
// A single IP should be interpreted as a /32
|
||||
$this->assertTrue(ip_in_range("1.2.3.4", "1.2.3.4"));
|
||||
}
|
||||
|
||||
public function test_deltree(): void
|
||||
{
|
||||
$tmp = sys_get_temp_dir();
|
||||
$dir = "$tmp/test_deltree";
|
||||
mkdir($dir);
|
||||
file_put_contents("$dir/foo", "bar");
|
||||
mkdir("$dir/baz");
|
||||
file_put_contents("$dir/baz/.qux", "quux");
|
||||
$this->assertTrue(file_exists($dir));
|
||||
$this->assertTrue(file_exists("$dir/foo"));
|
||||
$this->assertTrue(file_exists("$dir/baz"));
|
||||
$this->assertTrue(file_exists("$dir/baz/.qux"));
|
||||
deltree($dir);
|
||||
$this->assertFalse(file_exists($dir));
|
||||
}
|
||||
}
|
||||
547
core/tests/SearchTest.php
Normal file
547
core/tests/SearchTest.php
Normal file
@@ -0,0 +1,547 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Depends;
|
||||
use PHPUnit\Framework\Constraint\IsEqual;
|
||||
|
||||
require_once "core/imageboard/search.php";
|
||||
|
||||
class SearchTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testWeirdTags(): void
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "question? colon:thing exclamation!");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "question. colon_thing exclamation%");
|
||||
|
||||
$this->assert_search_results(["question?"], [$image_id_1]);
|
||||
$this->assert_search_results(["question."], [$image_id_2]);
|
||||
$this->assert_search_results(["colon:thing"], [$image_id_1]);
|
||||
$this->assert_search_results(["colon_thing"], [$image_id_2]);
|
||||
$this->assert_search_results(["exclamation!"], [$image_id_1]);
|
||||
$this->assert_search_results(["exclamation%"], [$image_id_2]);
|
||||
}
|
||||
|
||||
public function testOrder(): void
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$i1 = $this->post_image("tests/pbx_screenshot.jpg", "question? colon:thing exclamation!");
|
||||
$i2 = $this->post_image("tests/bedroom_workshop.jpg", "question. colon_thing exclamation%");
|
||||
$i3 = $this->post_image("tests/favicon.png", "another");
|
||||
|
||||
$is1 = Search::find_images(0, null, ["order=random_4123"]);
|
||||
$ids1 = array_map(fn ($image) => $image->id, $is1);
|
||||
$this->assertEquals(3, count($ids1));
|
||||
|
||||
$is2 = Search::find_images(0, null, ["order=random_4123"]);
|
||||
$ids2 = array_map(fn ($image) => $image->id, $is2);
|
||||
$this->assertEquals(3, count($ids1));
|
||||
|
||||
$is3 = Search::find_images(0, null, ["order=random_6543"]);
|
||||
$ids3 = array_map(fn ($image) => $image->id, $is3);
|
||||
$this->assertEquals(3, count($ids3));
|
||||
|
||||
$this->assertEquals($ids1, $ids2);
|
||||
$this->assertNotEquals($ids1, $ids3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function testUpload(): array
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "thing computer screenshot pbx phone");
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "thing computer computing bedroom workshop");
|
||||
$this->log_out();
|
||||
|
||||
# make sure both uploads were ok
|
||||
$this->assertTrue($image_id_1 > 0);
|
||||
$this->assertTrue($image_id_2 > 0);
|
||||
|
||||
return [$image_id_1, $image_id_2];
|
||||
}
|
||||
|
||||
|
||||
/** ******************************************************
|
||||
* Test turning a string into an abstract query
|
||||
*
|
||||
* @param string $tags
|
||||
* @param TagCondition[] $expected_tag_conditions
|
||||
* @param ImgCondition[] $expected_img_conditions
|
||||
* @param string $expected_order
|
||||
*/
|
||||
private function assert_TTC(
|
||||
string $tags,
|
||||
array $expected_tag_conditions,
|
||||
array $expected_img_conditions,
|
||||
string $expected_order,
|
||||
): void {
|
||||
$class = new \ReflectionClass(Search::class);
|
||||
$terms_to_conditions = $class->getMethod("terms_to_conditions");
|
||||
$terms_to_conditions->setAccessible(true); // Use this if you are running PHP older than 8.1.0
|
||||
|
||||
$obj = new Search();
|
||||
[$tag_conditions, $img_conditions, $order] = $terms_to_conditions->invokeArgs($obj, [Tag::explode($tags, false)]);
|
||||
|
||||
static::assertThat(
|
||||
[
|
||||
"tags" => $expected_tag_conditions,
|
||||
"imgs" => $expected_img_conditions,
|
||||
"order" => $expected_order,
|
||||
],
|
||||
new IsEqual([
|
||||
"tags" => $tag_conditions,
|
||||
"imgs" => $img_conditions,
|
||||
"order" => $order,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Empty(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Hash(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"hash=1234567890",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
new ImgCondition(new Querylet("images.hash = :hash", ["hash" => "1234567890"])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Ratio(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"ratio=42:12345",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
new ImgCondition(new Querylet("width / :width1 = height / :height1", ['width1' => 42,
|
||||
'height1' => 12345])),
|
||||
],
|
||||
"images.id DESC"
|
||||
);
|
||||
}
|
||||
|
||||
public function testTTC_Order(): void
|
||||
{
|
||||
$this->assert_TTC(
|
||||
"order=score",
|
||||
[
|
||||
],
|
||||
[
|
||||
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
|
||||
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
|
||||
"private_owner_id" => 1,
|
||||
"true" => true])),
|
||||
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
|
||||
],
|
||||
"images.numeric_score DESC"
|
||||
);
|
||||
}
|
||||
|
||||
/** ******************************************************
|
||||
* Test turning an abstract query into SQL + fetching the results
|
||||
*
|
||||
* @param string[] $tcs
|
||||
* @param string[] $ics
|
||||
* @param string $order
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param int[] $res
|
||||
* @param string[] $path
|
||||
*/
|
||||
private function assert_BSQ(
|
||||
array $tcs = [],
|
||||
array $ics = [],
|
||||
string $order = "id DESC",
|
||||
int $limit = 9999,
|
||||
int $start = 0,
|
||||
array $res = [],
|
||||
array $path = null,
|
||||
): void {
|
||||
global $database;
|
||||
|
||||
$tcs = array_map(
|
||||
fn ($tag) => ($tag[0] == "-") ?
|
||||
new TagCondition(substr($tag, 1), false) :
|
||||
new TagCondition($tag),
|
||||
$tcs
|
||||
);
|
||||
|
||||
$ics = array_map(
|
||||
fn ($ic) => send_event(new SearchTermParseEvent(0, $ic, []))->img_conditions,
|
||||
$ics
|
||||
);
|
||||
$ics = array_merge(...$ics);
|
||||
|
||||
Search::$_search_path = [];
|
||||
|
||||
$class = new \ReflectionClass(Search::class);
|
||||
$build_search_querylet = $class->getMethod("build_search_querylet");
|
||||
$build_search_querylet->setAccessible(true); // Use this if you are running PHP older than 8.1.0
|
||||
|
||||
$obj = new Search();
|
||||
$querylet = $build_search_querylet->invokeArgs($obj, [$tcs, $ics, $order, $limit, $start]);
|
||||
|
||||
$results = $database->get_all($querylet->sql, $querylet->variables);
|
||||
|
||||
static::assertThat(
|
||||
[
|
||||
"res" => array_map(fn ($row) => $row['id'], $results),
|
||||
"path" => Search::$_search_path,
|
||||
],
|
||||
new IsEqual([
|
||||
"res" => $res,
|
||||
"path" => $path ?? Search::$_search_path,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* No-tag search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_NoTags(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: [],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Fast-path search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["maumaumau"],
|
||||
res: [],
|
||||
path: ["fast", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["pbx"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildNoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["asdfasdf*"],
|
||||
res: [],
|
||||
path: ["fast", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the first image matches both the wildcard and the tag.
|
||||
* This checks for a bug where searching for "a* b" would return
|
||||
* an image tagged "a1 a2" because the number of matched tags
|
||||
* was equal to the number of searched tags.
|
||||
*
|
||||
* https://github.com/shish/shimmie2/issues/547
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildOneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["screen*"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the fast path doesn't return duplicate results
|
||||
* when a wildcard matches one image multiple times.
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_FastPath_WildManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// two images match comp* - one matches it once, one matches it twice
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["fast"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* General search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
# multiple tags, one of which doesn't exist
|
||||
# (test the "one tag doesn't exist = no hits" path)
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "not_a_tag"],
|
||||
res: [],
|
||||
path: ["general", "invalid_tag"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the first image matches both the wildcard and the tag.
|
||||
* This checks for a bug where searching for "a* b" would return
|
||||
* an image tagged "a1 a2" because the number of matched tags
|
||||
* was equal to the number of searched tags.
|
||||
*
|
||||
* https://github.com/shish/shimmie2/issues/547
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_WildOneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*", "screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "thing"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_WildManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["comp*", "-asdf"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractValidFromResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "-pbx"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractNotValidFromResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer", "-not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// negative tag alone, should remove the image with that tag
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-pbx"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["general", "only_negative_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractNotValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// negative that doesn't exist, should return all results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "all_nonexistent_negatives"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_GeneralPath_SubtractMultipleNotValidFromDefault(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// multiple negative tags that don't exist, should return all results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["-not_a_tag", "-also_not_a_tag"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "all_nonexistent_negatives"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Meta Search *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_NoResults(): void
|
||||
{
|
||||
$this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
ics: ["hash=1234567890"],
|
||||
res: [],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["ratio=42:12345"],
|
||||
res: [],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_OneResult(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
$this->assert_BSQ(
|
||||
ics: ["hash=feb01bab5698a11dd87416724c7a89e3"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["id={$image_ids[1]}"],
|
||||
res: [$image_ids[1]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["filename=screenshot"],
|
||||
res: [$image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_ImgCond_ManyResults(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
|
||||
$this->assert_BSQ(
|
||||
ics: ["size=640x480"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["tags=5"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
$this->assert_BSQ(
|
||||
ics: ["ext=jpg"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["no_tags"],
|
||||
);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * *
|
||||
* Mixed *
|
||||
* * * * * * * * * * */
|
||||
#[Depends('testUpload')]
|
||||
public function testBSQ_TagCondWithImgCond(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
// multiple tags, many results
|
||||
$this->assert_BSQ(
|
||||
tcs: ["computer"],
|
||||
ics: ["size=640x480"],
|
||||
res: [$image_ids[1], $image_ids[0]],
|
||||
path: ["general", "some_positives"],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_images
|
||||
*/
|
||||
#[Depends('testUpload')]
|
||||
public function test_get_images(): void
|
||||
{
|
||||
$image_ids = $this->testUpload();
|
||||
|
||||
$res = Search::get_images($image_ids);
|
||||
$this->assertGreaterThan($res[0]->id, $res[1]->id);
|
||||
|
||||
$res = Search::get_images(array_reverse($image_ids));
|
||||
$this->assertLessThan($res[0]->id, $res[1]->id);
|
||||
}
|
||||
}
|
||||
27
core/tests/StdLibExTest.php
Normal file
27
core/tests/StdLibExTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class StdLibExTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testJsonEncodeOk(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'{"a":1,"b":2,"c":3,"d":4,"e":5}',
|
||||
\Safe\json_encode(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5])
|
||||
);
|
||||
}
|
||||
|
||||
public function testJsonEncodeError(): void
|
||||
{
|
||||
$e = $this->assertException(\Exception::class, function () {
|
||||
\Safe\json_encode("\xB1\x31");
|
||||
});
|
||||
$this->assertEquals(
|
||||
"Malformed UTF-8 characters, possibly incorrectly encoded",
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
21
core/tests/TagTest.php
Normal file
21
core/tests/TagTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/imageboard/tag.php";
|
||||
|
||||
class TagTest extends TestCase
|
||||
{
|
||||
public function test_compare(): void
|
||||
{
|
||||
$this->assertFalse(Tag::compare(["foo"], ["bar"]));
|
||||
$this->assertFalse(Tag::compare(["foo"], ["foo", "bar"]));
|
||||
$this->assertTrue(Tag::compare([], []));
|
||||
$this->assertTrue(Tag::compare(["foo"], ["FoO"]));
|
||||
$this->assertTrue(Tag::compare(["foo", "bar"], ["bar", "FoO"]));
|
||||
}
|
||||
}
|
||||
273
core/tests/UrlsTest.php
Normal file
273
core/tests/UrlsTest.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Depends;
|
||||
|
||||
require_once "core/urls.php";
|
||||
|
||||
class UrlsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* An integration test for
|
||||
* - search_link()
|
||||
* - make_link()
|
||||
* - _get_query()
|
||||
* - get_search_terms()
|
||||
*/
|
||||
#[Depends("test_search_link")]
|
||||
public function test_get_search_terms_from_search_link(): void
|
||||
{
|
||||
/**
|
||||
* @param array<string> $vars
|
||||
* @return array<string>
|
||||
*/
|
||||
$gst = function (array $terms): array {
|
||||
$pre = new PageRequestEvent("GET", _get_query(search_link($terms)), [], []);
|
||||
$pre->page_matches("post/list/{search}/{page}");
|
||||
return Tag::explode($pre->get_arg('search'));
|
||||
};
|
||||
|
||||
global $config;
|
||||
foreach ([true, false] as $nice_urls) {
|
||||
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
|
||||
|
||||
$this->assertEquals(
|
||||
["bar", "foo"],
|
||||
$gst(["foo", "bar"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
["AC/DC"],
|
||||
$gst(["AC/DC"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
["cat*", "rating=?"],
|
||||
$gst(["rating=?", "cat*"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_make_link(): void
|
||||
{
|
||||
global $config;
|
||||
foreach ([true, false] as $nice_urls) {
|
||||
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
|
||||
|
||||
// basic
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo" : "/test/index.php?q=foo",
|
||||
make_link("foo")
|
||||
);
|
||||
|
||||
// remove leading slash from path
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo" : "/test/index.php?q=foo",
|
||||
make_link("/foo")
|
||||
);
|
||||
|
||||
// query
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo?a=1&b=2" : "/test/index.php?q=foo&a=1&b=2",
|
||||
make_link("foo", "a=1&b=2")
|
||||
);
|
||||
|
||||
// hash
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo#cake" : "/test/index.php?q=foo#cake",
|
||||
make_link("foo", null, "cake")
|
||||
);
|
||||
|
||||
// query + hash
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/foo?a=1&b=2#cake" : "/test/index.php?q=foo&a=1&b=2#cake",
|
||||
make_link("foo", "a=1&b=2", "cake")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_make_link")]
|
||||
public function test_search_link(): void
|
||||
{
|
||||
global $config;
|
||||
foreach ([true, false] as $nice_urls) {
|
||||
$config->set_bool(SetupConfig::NICE_URLS, $nice_urls);
|
||||
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/bar%20foo/1" : "/test/index.php?q=post/list/bar%20foo/1",
|
||||
search_link(["foo", "bar"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/AC%2FDC/1" : "/test/index.php?q=post/list/AC%2FDC/1",
|
||||
search_link(["AC/DC"])
|
||||
);
|
||||
$this->assertEquals(
|
||||
$nice_urls ? "/test/post/list/cat%2A%20rating%3D%3F/1" : "/test/index.php?q=post/list/cat%2A%20rating%3D%3F/1",
|
||||
search_link(["rating=?", "cat*"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_get_query(): void
|
||||
{
|
||||
// just validating an assumption that this test relies upon
|
||||
$this->assertEquals(get_base_href(), "/test");
|
||||
|
||||
$this->assertEquals(
|
||||
"tasty/cake",
|
||||
_get_query("/test/tasty/cake"),
|
||||
'http://$SERVER/$INSTALL_DIR/$PATH should return $PATH'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"tasty/cake",
|
||||
_get_query("/test/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"),
|
||||
'URL encoded paths should be left alone'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"tasty/cake%20pie",
|
||||
_get_query("/test/tasty/cake%20pie"),
|
||||
'URL encoded queries should be left alone'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"",
|
||||
_get_query("/test/"),
|
||||
'If just viewing install directory, should return /'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"",
|
||||
_get_query("/test/index.php"),
|
||||
'If just viewing index.php, should return /'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"post/list/tasty%2Fcake/1",
|
||||
_get_query("/test/post/list/tasty%2Fcake/1"),
|
||||
'URL encoded niceurls should be left alone, even encoded slashes'
|
||||
);
|
||||
$this->assertEquals(
|
||||
"post/list/tasty%2Fcake/1",
|
||||
_get_query("/test/index.php?q=post/list/tasty%2Fcake/1"),
|
||||
'URL encoded uglyurls should be left alone, even encoded slashes'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_is_https_enabled(): void
|
||||
{
|
||||
$this->assertFalse(is_https_enabled(), "HTTPS should be disabled by default");
|
||||
|
||||
$_SERVER['HTTPS'] = "on";
|
||||
$this->assertTrue(is_https_enabled(), "HTTPS should be enabled when set to 'on'");
|
||||
unset($_SERVER['HTTPS']);
|
||||
}
|
||||
|
||||
public function test_get_base_href(): void
|
||||
{
|
||||
// PHP_SELF should point to "the currently executing script
|
||||
// relative to the document root"
|
||||
$this->assertEquals("", get_base_href(["PHP_SELF" => "/index.php"]));
|
||||
$this->assertEquals("/mydir", get_base_href(["PHP_SELF" => "/mydir/index.php"]));
|
||||
|
||||
// SCRIPT_FILENAME should point to "the absolute pathname of
|
||||
// the currently executing script" and DOCUMENT_ROOT should
|
||||
// point to "the document root directory under which the
|
||||
// current script is executing"
|
||||
$this->assertEquals("", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html",
|
||||
]), "root directory");
|
||||
$this->assertEquals("/mydir", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/mydir/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html",
|
||||
]), "subdirectory");
|
||||
$this->assertEquals("", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html/",
|
||||
]), "trailing slash in DOCUMENT_ROOT root should be ignored");
|
||||
$this->assertEquals("/mydir", get_base_href([
|
||||
"PHP_SELF" => "<invalid>",
|
||||
"SCRIPT_FILENAME" => "/var/www/html/mydir/index.php",
|
||||
"DOCUMENT_ROOT" => "/var/www/html/",
|
||||
]), "trailing slash in DOCUMENT_ROOT subdir should be ignored");
|
||||
}
|
||||
|
||||
#[Depends("test_is_https_enabled")]
|
||||
#[Depends("test_get_base_href")]
|
||||
public function test_make_http(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"http://cli-command/test/foo",
|
||||
make_http("foo"),
|
||||
"relative to shimmie root"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"http://cli-command/foo",
|
||||
make_http("/foo"),
|
||||
"relative to web server"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"https://foo.com",
|
||||
make_http("https://foo.com"),
|
||||
"absolute URL should be left alone"
|
||||
);
|
||||
}
|
||||
|
||||
public function test_modify_url(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"/foo/bar?a=3&b=2",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a" => "3"])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"https://blah.com/foo/bar?b=2",
|
||||
modify_url("https://blah.com/foo/bar?a=1&b=2", ["a" => null])
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"/foo/bar",
|
||||
modify_url("/foo/bar?a=1&b=2", ["a" => null, "b" => null])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_referer_or(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_REFERER']);
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"cake",
|
||||
referer_or("foo")
|
||||
);
|
||||
|
||||
$_SERVER['HTTP_REFERER'] = "cake";
|
||||
$this->assertEquals(
|
||||
"foo",
|
||||
referer_or("foo", ["cake"])
|
||||
);
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_bool(SetupConfig::NICE_URLS, true);
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
196
core/tests/UtilTest.php
Normal file
196
core/tests/UtilTest.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once "core/util.php";
|
||||
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public function test_get_theme(): void
|
||||
{
|
||||
$this->assertEquals("default", get_theme());
|
||||
}
|
||||
|
||||
public function test_get_memory_limit(): void
|
||||
{
|
||||
get_memory_limit();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_check_gd_version(): void
|
||||
{
|
||||
check_gd_version();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_check_im_version(): void
|
||||
{
|
||||
check_im_version();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_human_filesize(): void
|
||||
{
|
||||
$this->assertEquals("123.00B", human_filesize(123));
|
||||
$this->assertEquals("123B", human_filesize(123, 0));
|
||||
$this->assertEquals("120.56KB", human_filesize(123456));
|
||||
}
|
||||
|
||||
public function test_generate_key(): void
|
||||
{
|
||||
$this->assertEquals(20, strlen(generate_key()));
|
||||
}
|
||||
|
||||
public function test_warehouse_path(): void
|
||||
{
|
||||
$hash = "7ac19c10d6859415";
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", $hash),
|
||||
warehouse_path("base", $hash, false, 0)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", $hash),
|
||||
warehouse_path("base", $hash, false, 1)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", $hash),
|
||||
warehouse_path("base", $hash, false, 2)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", $hash),
|
||||
warehouse_path("base", $hash, false, 3)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", $hash),
|
||||
warehouse_path("base", $hash, false, 4)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", $hash),
|
||||
warehouse_path("base", $hash, false, 5)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", $hash),
|
||||
warehouse_path("base", $hash, false, 6)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", $hash),
|
||||
warehouse_path("base", $hash, false, 7)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 8)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 9)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
join_path(DATA_DIR, "base", "7a", "c1", "9c", "10", "d6", "85", "94", "15", $hash),
|
||||
warehouse_path("base", $hash, false, 10)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_load_balance_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)
|
||||
);
|
||||
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_path_to_tags(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
[],
|
||||
path_to_tags("nope.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
path_to_tags("\\")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
path_to_tags("/")
|
||||
);
|
||||
$this->assertEquals(
|
||||
[],
|
||||
path_to_tags("C:\\")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["test", "tag"],
|
||||
path_to_tags("123 - test tag.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["foo", "bar"],
|
||||
path_to_tags("/foo/bar/baz.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["cake", "pie", "foo", "bar"],
|
||||
path_to_tags("/foo/bar/123 - cake pie.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["bacon", "lemon"],
|
||||
path_to_tags("\\bacon\\lemon\\baz.jpg")
|
||||
);
|
||||
$this->assertEquals(
|
||||
["category:tag"],
|
||||
path_to_tags("/category:/tag/baz.jpg")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_contact_link(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
"mailto:asdf@example.com",
|
||||
contact_link("asdf@example.com")
|
||||
);
|
||||
$this->assertEquals(
|
||||
"http://example.com",
|
||||
contact_link("http://example.com")
|
||||
);
|
||||
$this->assertEquals(
|
||||
"https://foo.com/bar",
|
||||
contact_link("foo.com/bar")
|
||||
);
|
||||
$this->assertEquals(
|
||||
"john",
|
||||
contact_link("john")
|
||||
);
|
||||
}
|
||||
|
||||
public function test_get_user(): void
|
||||
{
|
||||
// TODO: HTTP_AUTHORIZATION
|
||||
// TODO: cookie user + session
|
||||
// fallback to anonymous
|
||||
$this->assertEquals(
|
||||
"Anonymous",
|
||||
_get_user()->name
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
use PhpParser\Node\Expr\Cast\Double;
|
||||
|
||||
class Link
|
||||
{
|
||||
public ?string $page;
|
||||
public ?string $query;
|
||||
|
||||
public function __construct(?string $page=null, ?string $query=null)
|
||||
public function __construct(?string $page = null, ?string $query = null)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->query = $query;
|
||||
@@ -43,9 +41,10 @@ function search_link(array $terms = [], int $page = 1): string
|
||||
* Figure out the correct way to link to a page, taking into account
|
||||
* things like the nice URLs setting.
|
||||
*
|
||||
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
|
||||
* eg make_link("foo/bar") becomes either "/v2/foo/bar" (niceurls) or
|
||||
* "/v2/index.php?q=foo/bar" (uglyurls)
|
||||
*/
|
||||
function make_link(?string $page=null, ?string $query=null, ?string $fragment=null): string
|
||||
function make_link(?string $page = null, ?string $query = null, ?string $fragment = null): string
|
||||
{
|
||||
global $config;
|
||||
|
||||
@@ -176,14 +175,22 @@ function unparse_url(array $parsed_url): string
|
||||
|
||||
/**
|
||||
* Take the current URL and modify some parameters
|
||||
*
|
||||
* @param array<string, mixed> $changes
|
||||
*/
|
||||
function modify_current_url(array $changes): string
|
||||
{
|
||||
return modify_url($_SERVER['REQUEST_URI'], $changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a URL and modify some parameters
|
||||
*
|
||||
* @param array<string, mixed> $changes
|
||||
*/
|
||||
function modify_url(string $url, array $changes): string
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
$parts = parse_url($url);
|
||||
|
||||
$params = [];
|
||||
@@ -224,8 +231,10 @@ function make_http(string $link): string
|
||||
/**
|
||||
* If HTTP_REFERER is set, and not blacklisted, then return it
|
||||
* Else return a default $dest
|
||||
*
|
||||
* @param string[]|null $blacklist
|
||||
*/
|
||||
function referer_or(string $dest, ?array $blacklist=null): string
|
||||
function referer_or(string $dest, ?array $blacklist = null): string
|
||||
{
|
||||
if (empty($_SERVER['HTTP_REFERER'])) {
|
||||
return $dest;
|
||||
|
||||
@@ -11,12 +11,6 @@ use MicroHTML\HTMLElement;
|
||||
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
function _new_user(array $row): User
|
||||
{
|
||||
return new User($row);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class User
|
||||
*
|
||||
@@ -54,16 +48,14 @@ class User
|
||||
*/
|
||||
public function __construct(array $row)
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->id = int_escape((string)$row['id']);
|
||||
$this->name = $row['name'];
|
||||
$this->email = $row['email'];
|
||||
$this->join_date = $row['joindate'];
|
||||
$this->passhash = $row['pass'];
|
||||
|
||||
if (array_key_exists($row["class"], $_shm_user_classes)) {
|
||||
$this->class = $_shm_user_classes[$row["class"]];
|
||||
if (array_key_exists($row["class"], UserClass::$known_classes)) {
|
||||
$this->class = UserClass::$known_classes[$row["class"]];
|
||||
} else {
|
||||
throw new ServerError("User '{$this->name}' has invalid class '{$row["class"]}'");
|
||||
}
|
||||
@@ -98,7 +90,7 @@ class User
|
||||
} else {
|
||||
$query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess";
|
||||
}
|
||||
$row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]);
|
||||
$row = $database->get_row($query, ["name" => $name, "ip" => get_session_ip($config), "sess" => $session]);
|
||||
$cache->set("user-session:$name-$session", $row, 600);
|
||||
}
|
||||
return is_null($row) ? null : new User($row);
|
||||
@@ -113,7 +105,7 @@ class User
|
||||
return new User($cached);
|
||||
}
|
||||
}
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
|
||||
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id" => $id]);
|
||||
if ($id === 1) {
|
||||
$cache->set('user-id:'.$id, $row, 600);
|
||||
}
|
||||
@@ -124,7 +116,7 @@ class 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]);
|
||||
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name" => $name]);
|
||||
return is_null($row) ? null : new User($row);
|
||||
}
|
||||
|
||||
@@ -188,7 +180,7 @@ class User
|
||||
public function set_class(string $class): void
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
|
||||
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class" => $class, "id" => $this->id]);
|
||||
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
|
||||
}
|
||||
|
||||
@@ -200,7 +192,7 @@ class User
|
||||
}
|
||||
$old_name = $this->name;
|
||||
$this->name = $name;
|
||||
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
|
||||
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name" => $this->name, "id" => $this->id]);
|
||||
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
|
||||
}
|
||||
|
||||
@@ -208,19 +200,15 @@ class User
|
||||
{
|
||||
global $database;
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
if (is_string($hash)) {
|
||||
$this->passhash = $hash;
|
||||
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
} else {
|
||||
throw new SCoreException("Failed to hash password");
|
||||
}
|
||||
$this->passhash = $hash;
|
||||
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash" => $this->passhash, "id" => $this->id]);
|
||||
log_info("core-user", 'Set password for '.$this->name);
|
||||
}
|
||||
|
||||
public function set_email(string $address): void
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
|
||||
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email" => $address, "id" => $this->id]);
|
||||
log_info("core-user", 'Set email for '.$this->name);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,6 @@ namespace Shimmie2;
|
||||
|
||||
use GQLA\Type;
|
||||
use GQLA\Field;
|
||||
use GQLA\Query;
|
||||
|
||||
/**
|
||||
* @global UserClass[] $_shm_user_classes
|
||||
*/
|
||||
global $_shm_user_classes;
|
||||
$_shm_user_classes = [];
|
||||
|
||||
/**
|
||||
* Class UserClass
|
||||
@@ -20,30 +13,39 @@ $_shm_user_classes = [];
|
||||
#[Type(name: "UserClass")]
|
||||
class UserClass
|
||||
{
|
||||
/** @var array<string, UserClass> */
|
||||
public static array $known_classes = [];
|
||||
|
||||
#[Field]
|
||||
public ?string $name = null;
|
||||
public ?UserClass $parent = null;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
public array $abilities = [];
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $abilities
|
||||
*/
|
||||
public function __construct(string $name, string $parent = null, array $abilities = [])
|
||||
{
|
||||
global $_shm_user_classes;
|
||||
|
||||
$this->name = $name;
|
||||
$this->abilities = $abilities;
|
||||
|
||||
if (!is_null($parent)) {
|
||||
$this->parent = $_shm_user_classes[$parent];
|
||||
$this->parent = static::$known_classes[$parent];
|
||||
}
|
||||
|
||||
$_shm_user_classes[$name] = $this;
|
||||
static::$known_classes[$name] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
#[Field(type: "[Permission!]!")]
|
||||
public function permissions(): array
|
||||
{
|
||||
$perms = [];
|
||||
foreach ((new \ReflectionClass('\Shimmie2\Permissions'))->getConstants() as $k => $v) {
|
||||
foreach ((new \ReflectionClass(Permissions::class))->getConstants() as $k => $v) {
|
||||
if ($this->can($v)) {
|
||||
$perms[] = $v;
|
||||
}
|
||||
@@ -61,10 +63,9 @@ class UserClass
|
||||
} elseif (!is_null($this->parent)) {
|
||||
return $this->parent->can($ability);
|
||||
} else {
|
||||
global $_shm_user_classes;
|
||||
$min_dist = 9999;
|
||||
$min_ability = null;
|
||||
foreach ($_shm_user_classes['base']->abilities as $a => $cando) {
|
||||
foreach (UserClass::$known_classes['base']->abilities as $a => $cando) {
|
||||
$v = levenshtein($ability, $a);
|
||||
if ($v < $min_dist) {
|
||||
$min_dist = $v;
|
||||
@@ -93,6 +94,7 @@ unset($_all_false);
|
||||
|
||||
// Ghost users can't do anything
|
||||
new UserClass("ghost", "base", [
|
||||
Permissions::READ_PM => true,
|
||||
]);
|
||||
|
||||
// Anonymous users can't do anything by default, but
|
||||
|
||||
152
core/util.php
152
core/util.php
@@ -54,8 +54,8 @@ function contact_link(?string $contact = null): ?string
|
||||
function is_https_enabled(): bool
|
||||
{
|
||||
// check forwarded protocol
|
||||
if (REVERSE_PROXY_X_HEADERS && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
|
||||
$_SERVER['HTTPS']='on';
|
||||
if (is_trusted_proxy() && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
}
|
||||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
@@ -80,10 +80,10 @@ function get_memory_limit(): int
|
||||
global $config;
|
||||
|
||||
// thumbnail generation requires lots of memory
|
||||
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
|
||||
$default_limit = 8 * 1024 * 1024; // 8 MB of memory is PHP's default.
|
||||
$shimmie_limit = $config->get_int(MediaConfig::MEM_LIMIT);
|
||||
|
||||
if ($shimmie_limit < 3*1024*1024) {
|
||||
if ($shimmie_limit < 3 * 1024 * 1024) {
|
||||
// we aren't going to fit, override
|
||||
$shimmie_limit = $default_limit;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ function check_gd_version(): int
|
||||
*/
|
||||
function check_im_version(): int
|
||||
{
|
||||
$convert_check = exec("convert");
|
||||
$convert_check = exec("convert --version");
|
||||
|
||||
return (empty($convert_check) ? 0 : 1);
|
||||
}
|
||||
@@ -181,8 +181,7 @@ function is_bot(): bool
|
||||
/**
|
||||
* Get real IP if behind a reverse proxy
|
||||
*/
|
||||
|
||||
function get_real_ip()
|
||||
function get_real_ip(): string
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
@@ -257,9 +256,9 @@ function only_strings(array $map): array
|
||||
* @param int $splits The number of octet pairs to split the hash into. Caps out at strlen($hash)/2.
|
||||
* @return string
|
||||
*/
|
||||
function warehouse_path(string $base, string $hash, bool $create=true, int $splits = WH_SPLITS): string
|
||||
function warehouse_path(string $base, string $hash, bool $create = true, int $splits = WH_SPLITS): string
|
||||
{
|
||||
$dirs =[DATA_DIR, $base];
|
||||
$dirs = [DATA_DIR, $base];
|
||||
$splits = min($splits, strlen($hash) / 2);
|
||||
for ($i = 0; $i < $splits; $i++) {
|
||||
$dirs[] = substr($hash, $i * 2, 2);
|
||||
@@ -280,13 +279,13 @@ function warehouse_path(string $base, string $hash, bool $create=true, int $spli
|
||||
function data_path(string $filename, bool $create = true): string
|
||||
{
|
||||
$filename = join_path("data", $filename);
|
||||
if ($create&&!file_exists(dirname($filename))) {
|
||||
if ($create && !file_exists(dirname($filename))) {
|
||||
mkdir(dirname($filename), 0755, true);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
function load_balance_url(string $tmpl, string $hash, int $n=0): string
|
||||
function load_balance_url(string $tmpl, string $hash, int $n = 0): string
|
||||
{
|
||||
static $flexihashes = [];
|
||||
$matches = [];
|
||||
@@ -324,7 +323,14 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
|
||||
return $tmpl;
|
||||
}
|
||||
|
||||
function fetch_url(string $url, string $mfile): ?array
|
||||
class FetchException extends \Exception
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]>
|
||||
*/
|
||||
function fetch_url(string $url, string $mfile): array
|
||||
{
|
||||
global $config;
|
||||
|
||||
@@ -342,7 +348,10 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
return null;
|
||||
throw new FetchException("cURL failed: ".curl_error($ch));
|
||||
}
|
||||
if ($response === true) { // we use CURLOPT_RETURNTRANSFER, so this should never happen
|
||||
throw new FetchException("cURL failed successfully??");
|
||||
}
|
||||
|
||||
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
@@ -353,11 +362,7 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
curl_close($ch);
|
||||
fwrite($fp, $body);
|
||||
fclose($fp);
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
|
||||
} elseif ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
|
||||
$s_url = escapeshellarg($url);
|
||||
$s_mfile = escapeshellarg($mfile);
|
||||
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
|
||||
@@ -369,7 +374,7 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
$fp_in = @fopen($url, "r");
|
||||
$fp_out = fopen($mfile, "w");
|
||||
if (!$fp_in || !$fp_out) {
|
||||
return null;
|
||||
throw new FetchException("fopen failed");
|
||||
}
|
||||
$length = 0;
|
||||
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) {
|
||||
@@ -381,14 +386,22 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
fclose($fp_out);
|
||||
|
||||
$headers = http_parse_headers(implode("\n", $http_response_header));
|
||||
|
||||
return $headers;
|
||||
} else {
|
||||
throw new FetchException("No transload engine configured");
|
||||
}
|
||||
|
||||
return null;
|
||||
if (filesize($mfile) == 0) {
|
||||
@unlink($mfile);
|
||||
throw new FetchException("No data found in $url -- perhaps the site has hotlink protection?");
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
function path_to_tags(string $path): string
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function path_to_tags(string $path): array
|
||||
{
|
||||
$matches = [];
|
||||
$tags = [];
|
||||
@@ -409,7 +422,7 @@ function path_to_tags(string $path): string
|
||||
$category_to_inherit = "";
|
||||
foreach (explode(" ", $dir) as $tag) {
|
||||
$tag = trim($tag);
|
||||
if ($tag=="") {
|
||||
if ($tag == "") {
|
||||
continue;
|
||||
}
|
||||
if (substr_compare($tag, ":", -1) === 0) {
|
||||
@@ -417,7 +430,7 @@ function path_to_tags(string $path): string
|
||||
// which is for inheriting to tags on the subfolder
|
||||
$category_to_inherit = $tag;
|
||||
} else {
|
||||
if ($category!="" && !str_contains($tag, ":")) {
|
||||
if ($category != "" && !str_contains($tag, ":")) {
|
||||
// This indicates that category inheritance is active,
|
||||
// and we've encountered a tag that does not specify a category.
|
||||
// So we attach the inherited category to the tag.
|
||||
@@ -432,9 +445,12 @@ function path_to_tags(string $path): string
|
||||
$category = $category_to_inherit;
|
||||
}
|
||||
|
||||
return implode(" ", $tags);
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function get_dir_contents(string $dir): array
|
||||
{
|
||||
assert(!empty($dir));
|
||||
@@ -450,20 +466,10 @@ function get_dir_contents(string $dir): array
|
||||
|
||||
function remove_empty_dirs(string $dir): bool
|
||||
{
|
||||
assert(!empty($dir));
|
||||
|
||||
$result = true;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$items = array_diff(
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
$items = get_dir_contents($dir);
|
||||
;
|
||||
foreach ($items as $item) {
|
||||
$path = join_path($dir, $item);
|
||||
if (is_dir($path)) {
|
||||
@@ -472,31 +478,21 @@ function remove_empty_dirs(string $dir): bool
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
if ($result===true) {
|
||||
if ($result === true) {
|
||||
$result = rmdir($dir);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function get_files_recursively(string $dir): array
|
||||
{
|
||||
assert(!empty($dir));
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$things = array_diff(
|
||||
scandir(
|
||||
$dir
|
||||
),
|
||||
['..', '.']
|
||||
);
|
||||
$things = get_dir_contents($dir);
|
||||
|
||||
$output = [];
|
||||
|
||||
|
||||
foreach ($things as $thing) {
|
||||
$path = join_path($dir, $thing);
|
||||
if (is_file($path)) {
|
||||
@@ -511,6 +507,8 @@ function get_files_recursively(string $dir): array
|
||||
|
||||
/**
|
||||
* Returns amount of files & total size of dir.
|
||||
*
|
||||
* @return array{"path": string, "total_files": int, "total_mb": string}
|
||||
*/
|
||||
function scan_dir(string $path): array
|
||||
{
|
||||
@@ -570,6 +568,11 @@ function get_debug_info(): string
|
||||
return $debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects some debug information (execution time, memory usage, queries, etc)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function get_debug_info_arr(): array
|
||||
{
|
||||
global $cache, $config, $_shm_event_count, $database, $_shm_load_start;
|
||||
@@ -583,7 +586,7 @@ function get_debug_info_arr(): array
|
||||
return [
|
||||
"time" => round(ftime() - $_shm_load_start, 2),
|
||||
"dbtime" => round($database->dbtime, 2),
|
||||
"mem_mb" => round(((memory_get_peak_usage(true)+512)/1024)/1024, 2),
|
||||
"mem_mb" => round(((memory_get_peak_usage(true) + 512) / 1024) / 1024, 2),
|
||||
"files" => count(get_included_files()),
|
||||
"query_count" => $database->query_count,
|
||||
// "query_log" => $database->queries,
|
||||
@@ -599,6 +602,9 @@ function get_debug_info_arr(): array
|
||||
* Request initialisation stuff *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* @param string[] $files
|
||||
*/
|
||||
function require_all(array $files): void
|
||||
{
|
||||
foreach ($files as $filename) {
|
||||
@@ -606,7 +612,7 @@ function require_all(array $files): void
|
||||
}
|
||||
}
|
||||
|
||||
function _load_core_files()
|
||||
function _load_core_files(): void
|
||||
{
|
||||
global $_tracer;
|
||||
$_tracer->begin("Load Core Files");
|
||||
@@ -659,20 +665,8 @@ function _set_up_shimmie_environment(): void
|
||||
// The trace system has a certain amount of memory consumption every time it is used,
|
||||
// so to prevent running out of memory during complex operations code that uses it should
|
||||
// check if tracer output is enabled before making use of it.
|
||||
$tracer_enabled = constant('TRACE_FILE')!==null;
|
||||
}
|
||||
|
||||
|
||||
function _get_themelet_files(string $_theme): array
|
||||
{
|
||||
$base_themelets = [];
|
||||
$base_themelets[] = 'themes/'.$_theme.'/page.class.php';
|
||||
$base_themelets[] = 'themes/'.$_theme.'/themelet.class.php';
|
||||
|
||||
$ext_themelets = zglob("ext/{".Extension::get_enabled_extensions_as_string()."}/theme.php");
|
||||
$custom_themelets = zglob('themes/'.$_theme.'/{'.Extension::get_enabled_extensions_as_string().'}.theme.php');
|
||||
|
||||
return array_merge($base_themelets, $ext_themelets, $custom_themelets);
|
||||
// @phpstan-ignore-next-line - TRACE_FILE is defined in config
|
||||
$tracer_enabled = !is_null('TRACE_FILE');
|
||||
}
|
||||
|
||||
|
||||
@@ -684,8 +678,6 @@ function _fatal_error(\Exception $e): void
|
||||
$version = VERSION;
|
||||
$message = $e->getMessage();
|
||||
$phpver = phpversion();
|
||||
$query = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->query : null;
|
||||
$code = is_subclass_of($e, "Shimmie2\SCoreException") ? $e->http_code : 500;
|
||||
|
||||
//$hash = exec("git rev-parse HEAD");
|
||||
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
|
||||
@@ -697,14 +689,17 @@ function _fatal_error(\Exception $e): void
|
||||
foreach ($t as $n => $f) {
|
||||
$c = $f['class'] ?? '';
|
||||
$t = $f['type'] ?? '';
|
||||
$i = $f['file'] ?? 'unknown file';
|
||||
$l = $f['line'] ?? -1;
|
||||
$a = implode(", ", array_map("Shimmie2\stringer", $f['args'] ?? []));
|
||||
print("$n: {$f['file']}({$f['line']}): {$c}{$t}{$f['function']}({$a})\n");
|
||||
print("$n: {$i}({$l}): {$c}{$t}{$f['function']}({$a})\n");
|
||||
}
|
||||
|
||||
print("Message: $message\n");
|
||||
|
||||
if ($query) {
|
||||
print("Query: {$query}\n");
|
||||
if (is_a($e, DatabaseException::class)) {
|
||||
print("Query: {$e->query}\n");
|
||||
print("Args: ".var_export($e->args, true)."\n");
|
||||
}
|
||||
|
||||
print("Version: $version (on $phpver)\n");
|
||||
@@ -752,7 +747,7 @@ function _get_user(): User
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
if (is_null($my_user) && $page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
$my_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
|
||||
}
|
||||
if (is_null($my_user)) {
|
||||
@@ -763,11 +758,6 @@ function _get_user(): User
|
||||
return $my_user;
|
||||
}
|
||||
|
||||
function _get_query(): string
|
||||
{
|
||||
return (@$_POST["q"] ?: @$_GET["q"]) ?: "/";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
@@ -812,7 +802,7 @@ function make_form(string $target, bool $multipart = false, string $form_id = ""
|
||||
}
|
||||
|
||||
const BYTE_DENOMINATIONS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function human_filesize(int $bytes, $decimals = 2): string
|
||||
function human_filesize(int $bytes, int $decimals = 2): string
|
||||
{
|
||||
$factor = floor((strlen(strval($bytes)) - 1) / 3);
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
|
||||
|
||||
Reference in New Issue
Block a user