Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
6631e2cb6f | |||
|
7040b1b8e5 |
33
.github/workflows/test_and_publish.yml
vendored
33
.github/workflows/test_and_publish.yml
vendored
@ -7,22 +7,13 @@ on:
|
||||
- cron: '0 2 * * 0' # Weekly on Sundays at 02:00
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check format
|
||||
uses: zalexki/php-cs-fixer-action@master
|
||||
|
||||
test:
|
||||
name: PHP ${{ matrix.php }} / DB ${{ matrix.database }}
|
||||
strategy:
|
||||
max-parallel: 3
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['7.4', '8.0']
|
||||
php: ['7.3']
|
||||
database: ['pgsql', 'mysql', 'sqlite']
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
@ -30,13 +21,6 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Up Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
vendor
|
||||
key: vendor-${{ matrix.php }}-${{ hashFiles('composer.lock') }}
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
@ -72,8 +56,11 @@ jobs:
|
||||
run: composer validate
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer update && composer install --prefer-dist --no-progress
|
||||
run: composer install --prefer-dist --no-progress --no-suggest
|
||||
|
||||
- name: Install shimmie
|
||||
run: php index.php
|
||||
|
||||
- name: Run test suite
|
||||
run: |
|
||||
if [[ "${{ matrix.database }}" == "pgsql" ]]; then
|
||||
@ -86,18 +73,16 @@ jobs:
|
||||
export TEST_DSN="sqlite:data/shimmie.sqlite"
|
||||
fi
|
||||
vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||
|
||||
|
||||
- name: Upload coverage
|
||||
if: matrix.php == '7.4'
|
||||
run: |
|
||||
wget https://scrutinizer-ci.com/ocular.phar
|
||||
php ocular.phar code-coverage:upload --format=php-clover data/coverage.clover
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
needs: [format, test]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
needs: test
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
|
14
Dockerfile
14
Dockerfile
@ -1,7 +1,7 @@
|
||||
# "Build" shimmie (composer install - done in its own stage so that we don't
|
||||
# need to include all the composer fluff in the final image)
|
||||
FROM debian:testing-slim AS app
|
||||
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
|
||||
FROM debian:stable-slim AS app
|
||||
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
|
||||
COPY composer.json composer.lock /app/
|
||||
WORKDIR /app
|
||||
RUN composer install --no-dev
|
||||
@ -10,8 +10,8 @@ COPY . /app/
|
||||
# Tests in their own image. Really we should inherit from app and then
|
||||
# `composer install` phpunit on top of that; but for some reason
|
||||
# `composer install --no-dev && composer install` doesn't install dev
|
||||
FROM debian:testing-slim AS tests
|
||||
RUN apt update && apt install -y composer php7.4-gd php7.4-dom php7.4-sqlite3 php-xdebug imagemagick
|
||||
FROM debian:stable-slim AS tests
|
||||
RUN apt update && apt install -y composer php7.3-gd php7.3-dom php7.3-sqlite3 php-xdebug imagemagick
|
||||
COPY composer.json composer.lock /app/
|
||||
WORKDIR /app
|
||||
RUN composer install
|
||||
@ -25,7 +25,7 @@ RUN [ $RUN_TESTS = false ] || (\
|
||||
echo '=== Cleaning ===' && rm -rf data)
|
||||
|
||||
# Build su-exec so that our final image can be nicer
|
||||
FROM debian:testing-slim AS suexec
|
||||
FROM debian:stable-slim AS suexec
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc-dev curl
|
||||
RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
|
||||
gcc -Wall /usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
|
||||
@ -33,13 +33,13 @@ RUN curl -k -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa
|
||||
chmod 0755 /usr/local/bin/su-exec;
|
||||
|
||||
# Actually run shimmie
|
||||
FROM debian:testing-slim
|
||||
FROM debian:stable-slim
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
|
||||
ENV UID=1000 \
|
||||
GID=1000
|
||||
RUN apt update && apt install -y curl \
|
||||
php7.4-cli php7.4-gd php7.4-pgsql php7.4-mysql php7.4-sqlite3 php7.4-zip php7.4-dom php7.4-mbstring \
|
||||
php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring \
|
||||
imagemagick zip unzip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=app /app /app
|
||||
|
@ -5,17 +5,7 @@
|
||||
"license" : "GPL-2.0-or-later",
|
||||
"minimum-stability" : "dev",
|
||||
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.3.0"
|
||||
}
|
||||
},
|
||||
|
||||
"repositories" : [
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://asset-packagist.org"
|
||||
},
|
||||
{
|
||||
"type" : "package",
|
||||
"package" : {
|
||||
@ -31,30 +21,30 @@
|
||||
],
|
||||
|
||||
"require" : {
|
||||
"php" : "^7.4 | ^8.0",
|
||||
"php" : "^7.3",
|
||||
"ext-pdo": "*",
|
||||
"ext-json": "*",
|
||||
"ext-fileinfo": "*",
|
||||
|
||||
"flexihash/flexihash" : "^2.0",
|
||||
"ifixit/php-akismet" : "^1.0",
|
||||
"google/recaptcha" : "^1.1",
|
||||
"dapphp/securimage" : "^3.6",
|
||||
"shish/eventtracer-php" : "^2.0",
|
||||
"shish/ffsphp" : "^1.0",
|
||||
"shish/microcrud" : "^2.0",
|
||||
"shish/microhtml" : "^2.0",
|
||||
"enshrined/svg-sanitize" : "^0.14",
|
||||
"flexihash/flexihash" : "^2.0.0",
|
||||
"ifixit/php-akismet" : "1.*",
|
||||
"google/recaptcha" : "~1.1",
|
||||
"dapphp/securimage" : "3.6.*",
|
||||
"shish/eventtracer-php" : "^2.0.0",
|
||||
"shish/ffsphp" : "^1.0.0",
|
||||
"shish/microcrud" : "^2.0.0",
|
||||
"shish/microhtml" : "^2.0.0",
|
||||
"enshrined/svg-sanitize" : "0.13.*",
|
||||
|
||||
"bower-asset/jquery" : "^1.12",
|
||||
"bower-asset/jquery-timeago" : "^1.5",
|
||||
"bower-asset/js-cookie" : "^2.1"
|
||||
"bower-asset/jquery" : "1.12.*",
|
||||
"bower-asset/jquery-timeago" : "1.5.*",
|
||||
"bower-asset/mediaelement" : "2.21.*",
|
||||
"bower-asset/js-cookie" : "2.1.*"
|
||||
},
|
||||
|
||||
"require-dev" : {
|
||||
"phpunit/phpunit" : "^9.0",
|
||||
"friendsofphp/php-cs-fixer" : "*"
|
||||
},
|
||||
},
|
||||
|
||||
"suggest": {
|
||||
"ext-memcache": "memcache caching",
|
||||
"ext-memcached": "memcached caching",
|
||||
@ -68,5 +58,10 @@
|
||||
"ext-zlib": "anti-spam",
|
||||
"ext-xml": "some extensions",
|
||||
"ext-gd": "GD-based thumbnailing"
|
||||
}
|
||||
},"replace": {
|
||||
"bower-asset/jquery": ">=1.11.0",
|
||||
"bower-asset/inputmask": ">=3.2.0",
|
||||
"bower-asset/punycode": ">=1.3.0",
|
||||
"bower-asset/yii2-pjax": ">=2.0.0"
|
||||
}
|
||||
}
|
||||
|
4221
composer.lock
generated
4221
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,8 +20,10 @@ abstract class PageMode
|
||||
*/
|
||||
class BasePage
|
||||
{
|
||||
public string $mode = PageMode::PAGE;
|
||||
private string $mime;
|
||||
/** @var string */
|
||||
public $mode = PageMode::PAGE;
|
||||
/** @var string */
|
||||
private $type = "text/html; charset=utf-8";
|
||||
|
||||
/**
|
||||
* Set what this page should do; "page", "data", or "redirect".
|
||||
@ -34,14 +36,13 @@ class BasePage
|
||||
/**
|
||||
* Set the page's MIME type.
|
||||
*/
|
||||
public function set_mime(string $mime): void
|
||||
public function set_type(string $type): void
|
||||
{
|
||||
$this->mime = $mime;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->mime = MimeType::add_parameters(MimeType::HTML, MimeType::CHARSET_UTF8);
|
||||
if (@$_GET["flash"]) {
|
||||
$this->flash[] = $_GET['flash'];
|
||||
unset($_GET["flash"]);
|
||||
@ -50,11 +51,19 @@ class BasePage
|
||||
|
||||
// ==============================================
|
||||
|
||||
public string $data = ""; // public only for unit test
|
||||
private ?string $file = null;
|
||||
private bool $file_delete = false;
|
||||
private ?string $filename = null;
|
||||
private ?string $disposition = null;
|
||||
/** @var string; public only for unit test */
|
||||
public $data = "";
|
||||
|
||||
/** @var string */
|
||||
private $file = null;
|
||||
|
||||
/** @var bool */
|
||||
private $file_delete = false;
|
||||
|
||||
/** @var string */
|
||||
private $filename = null;
|
||||
|
||||
private $disposition = null;
|
||||
|
||||
/**
|
||||
* Set the raw data to be sent.
|
||||
@ -81,7 +90,8 @@ class BasePage
|
||||
|
||||
// ==============================================
|
||||
|
||||
public string $redirect = "";
|
||||
/** @var string */
|
||||
public $redirect = "";
|
||||
|
||||
/**
|
||||
* Set the URL to redirect to (remember to use make_link() if linking
|
||||
@ -94,25 +104,32 @@ class BasePage
|
||||
|
||||
// ==============================================
|
||||
|
||||
public int $code = 200;
|
||||
public string $title = "";
|
||||
public string $heading = "";
|
||||
public string $subheading = "";
|
||||
/** @var int */
|
||||
public $code = 200;
|
||||
|
||||
/** @var string */
|
||||
public $title = "";
|
||||
|
||||
/** @var string */
|
||||
public $heading = "";
|
||||
|
||||
/** @var string */
|
||||
public $subheading = "";
|
||||
|
||||
/** @var string[] */
|
||||
public array $html_headers = [];
|
||||
public $html_headers = [];
|
||||
|
||||
/** @var string[] */
|
||||
public array $http_headers = [];
|
||||
public $http_headers = [];
|
||||
|
||||
/** @var string[][] */
|
||||
public array $cookies = [];
|
||||
public $cookies = [];
|
||||
|
||||
/** @var Block[] */
|
||||
public array $blocks = [];
|
||||
public $blocks = [];
|
||||
|
||||
/** @var string[] */
|
||||
public array $flash = [];
|
||||
public $flash = [];
|
||||
|
||||
/**
|
||||
* Set the HTTP status code
|
||||
@ -226,7 +243,7 @@ class BasePage
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
header("HTTP/1.0 {$this->code} Shimmie");
|
||||
header("Content-type: " . $this->mime);
|
||||
header("Content-type: " . $this->type);
|
||||
header("X-Powered-By: Shimmie-" . VERSION);
|
||||
|
||||
foreach ($this->http_headers as $head) {
|
||||
@ -280,7 +297,7 @@ class BasePage
|
||||
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
|
||||
if (str_contains($range, ',')) {
|
||||
if (strpos($range, ',') !== false) {
|
||||
header('HTTP/1.1 416 Requested Range Not Satisfiable');
|
||||
header("Content-Range: bytes $start-$end/$size");
|
||||
break;
|
||||
@ -317,7 +334,7 @@ class BasePage
|
||||
break;
|
||||
case PageMode::REDIRECT:
|
||||
if ($this->flash) {
|
||||
$this->redirect .= str_contains($this->redirect, "?") ? "&" : "?";
|
||||
$this->redirect .= (strpos($this->redirect, "?") === false) ? "?" : "&";
|
||||
$this->redirect .= "flash=" . url_escape(implode("\n", $this->flash));
|
||||
}
|
||||
header('Location: ' . $this->redirect);
|
||||
@ -387,7 +404,6 @@ class BasePage
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(
|
||||
[
|
||||
"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",
|
||||
@ -410,7 +426,7 @@ class BasePage
|
||||
$this->add_html_header("<script defer src='$data_href/$js_cache_file' type='text/javascript'></script>", 44);
|
||||
}
|
||||
|
||||
protected function get_nav_links(): array
|
||||
protected function get_nav_links()
|
||||
{
|
||||
$pnbe = send_event(new PageNavBuildingEvent());
|
||||
|
||||
@ -542,7 +558,7 @@ EOD;
|
||||
$contact = empty($contact_link) ? "" : "<br><a href='$contact_link'>Contact</a>";
|
||||
|
||||
return "
|
||||
Media © their respective owners,
|
||||
Images © their respective owners,
|
||||
<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>
|
||||
@ -556,7 +572,7 @@ EOD;
|
||||
|
||||
class PageNavBuildingEvent extends Event
|
||||
{
|
||||
public array $links = [];
|
||||
public $links = [];
|
||||
|
||||
public function add_nav_link(string $name, Link $link, string $desc, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
@ -566,9 +582,9 @@ class PageNavBuildingEvent extends Event
|
||||
|
||||
class PageSubNavBuildingEvent extends Event
|
||||
{
|
||||
public string $parent;
|
||||
public $parent;
|
||||
|
||||
public array $links = [];
|
||||
public $links = [];
|
||||
|
||||
public function __construct(string $parent)
|
||||
{
|
||||
@ -584,11 +600,11 @@ class PageSubNavBuildingEvent extends Event
|
||||
|
||||
class NavLink
|
||||
{
|
||||
public string $name;
|
||||
public Link $link;
|
||||
public string $description;
|
||||
public int $order;
|
||||
public bool $active = false;
|
||||
public $name;
|
||||
public $link;
|
||||
public $description;
|
||||
public $order;
|
||||
public $active = false;
|
||||
|
||||
public function __construct(String $name, Link $link, String $description, ?bool $active = null, int $order = 50)
|
||||
{
|
||||
@ -645,7 +661,7 @@ class NavLink
|
||||
}
|
||||
}
|
||||
|
||||
function sort_nav_links(NavLink $a, NavLink $b): int
|
||||
function sort_nav_links(NavLink $a, NavLink $b)
|
||||
{
|
||||
return $a->order - $b->order;
|
||||
}
|
||||
|
@ -53,9 +53,8 @@ class BaseThemelet
|
||||
$h_tip = html_escape($image->get_tooltip());
|
||||
$h_tags = html_escape(strtolower($image->get_tag_list()));
|
||||
|
||||
// TODO: Set up a function for fetching what kind of files are currently thumbnailable
|
||||
$mimeArr = array_flip([MimeType::MP3]); //List of thumbless filetypes
|
||||
if (!isset($mimeArr[$image->get_mime()])) {
|
||||
$extArr = array_flip([EXTENSION_FLASH, EXTENSION_SVG, EXTENSION_MP3]); //List of thumbless filetypes
|
||||
if (!isset($extArr[$image->ext])) {
|
||||
$tsize = get_thumbnail_size($image->width, $image->height);
|
||||
} else {
|
||||
//Use max thumbnail size if using thumbless filetype
|
||||
@ -72,7 +71,7 @@ class BaseThemelet
|
||||
}
|
||||
}
|
||||
|
||||
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-height='$image->height' data-width='$image->width' data-mime='{$image->get_mime()}' data-post-id='$i_id'>".
|
||||
return "<a href='$h_view_link' class='thumb shm-thumb shm-thumb-link {$custom_classes}' data-tags='$h_tags' data-post-id='$i_id'>".
|
||||
"<img id='thumb_$i_id' title='$h_tip' alt='$h_tip' height='{$tsize[1]}' width='{$tsize[0]}' src='$h_thumb_link'>".
|
||||
"</a>\n";
|
||||
}
|
||||
|
@ -9,37 +9,49 @@ class Block
|
||||
{
|
||||
/**
|
||||
* The block's title.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public ?string $header;
|
||||
public $header;
|
||||
|
||||
/**
|
||||
* The content of the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public ?string $body;
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* Where the block should be placed. The default theme supports
|
||||
* "main" and "left", other themes can add their own areas.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $section;
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* How far down the section the block should appear, higher
|
||||
* numbers appear lower. The scale is 0-100 by convention,
|
||||
* though any number will work.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $position;
|
||||
public $position;
|
||||
|
||||
/**
|
||||
* A unique ID for the block.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $id;
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Should this block count as content for the sake of
|
||||
* the 404 handler
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public bool $is_content = true;
|
||||
public $is_content = true;
|
||||
|
||||
public function __construct(string $header=null, string $body=null, string $section="main", int $position=50, string $id=null)
|
||||
{
|
||||
@ -51,9 +63,7 @@ class Block
|
||||
if (is_null($id)) {
|
||||
$id = (empty($header) ? md5($body ?? '') : $header) . $section;
|
||||
}
|
||||
$str_id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
|
||||
assert(is_string($str_id));
|
||||
$this->id = $str_id;
|
||||
$this->id = preg_replace('/[^\w-]/', '', str_replace(' ', '_', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,8 +2,8 @@
|
||||
interface CacheEngine
|
||||
{
|
||||
public function get(string $key);
|
||||
public function set(string $key, $val, int $time=0): void;
|
||||
public function delete(string $key): void;
|
||||
public function set(string $key, $val, int $time=0);
|
||||
public function delete(string $key);
|
||||
}
|
||||
|
||||
class NoCache implements CacheEngine
|
||||
@ -12,17 +12,18 @@ class NoCache implements CacheEngine
|
||||
{
|
||||
return false;
|
||||
}
|
||||
public function set(string $key, $val, int $time=0): void
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
}
|
||||
public function delete(string $key): void
|
||||
public function delete(string $key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class MemcachedCache implements CacheEngine
|
||||
{
|
||||
public ?Memcached $memcache=null;
|
||||
/** @var ?Memcached */
|
||||
public $memcache=null;
|
||||
|
||||
public function __construct(string $args)
|
||||
{
|
||||
@ -51,7 +52,7 @@ class MemcachedCache implements CacheEngine
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0): void
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
$key = urlencode($key);
|
||||
|
||||
@ -62,7 +63,7 @@ class MemcachedCache implements CacheEngine
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key): void
|
||||
public function delete(string $key)
|
||||
{
|
||||
$key = urlencode($key);
|
||||
|
||||
@ -86,12 +87,12 @@ class APCCache implements CacheEngine
|
||||
return apc_fetch($key);
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0): void
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
apc_store($key, $val, $time);
|
||||
}
|
||||
|
||||
public function delete(string $key): void
|
||||
public function delete(string $key)
|
||||
{
|
||||
apc_delete($key);
|
||||
}
|
||||
@ -99,7 +100,7 @@ class APCCache implements CacheEngine
|
||||
|
||||
class RedisCache implements CacheEngine
|
||||
{
|
||||
private Redis $redis;
|
||||
private $redis=null;
|
||||
|
||||
public function __construct(string $args)
|
||||
{
|
||||
@ -115,7 +116,7 @@ class RedisCache implements CacheEngine
|
||||
return $this->redis->get($key);
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0): void
|
||||
public function set(string $key, $val, int $time=0)
|
||||
{
|
||||
if ($time > 0) {
|
||||
$this->redis->setEx($key, $time, $val);
|
||||
@ -124,7 +125,7 @@ class RedisCache implements CacheEngine
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key): void
|
||||
public function delete(string $key)
|
||||
{
|
||||
$this->redis->del($key);
|
||||
}
|
||||
@ -133,9 +134,9 @@ class RedisCache implements CacheEngine
|
||||
class Cache
|
||||
{
|
||||
public $engine;
|
||||
public int $hits=0;
|
||||
public int $misses=0;
|
||||
public int $time=0;
|
||||
public $hits=0;
|
||||
public $misses=0;
|
||||
public $time=0;
|
||||
|
||||
public function __construct(?string $dsn)
|
||||
{
|
||||
|
@ -1,65 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
// Provides mechanisms for cleanly executing command-line applications
|
||||
// Was created to try to centralize a solution for whatever caused this:
|
||||
// quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
|
||||
class CommandBuilder
|
||||
{
|
||||
private string $executable;
|
||||
private array $args = [];
|
||||
public array $output;
|
||||
|
||||
public function __construct(String $executable)
|
||||
{
|
||||
if (empty($executable)) {
|
||||
throw new InvalidArgumentException("executable cannot be empty");
|
||||
}
|
||||
|
||||
$this->executable = $executable;
|
||||
}
|
||||
|
||||
public function add_flag(string $value): void
|
||||
{
|
||||
$this->args[] = $value;
|
||||
}
|
||||
|
||||
public function add_escaped_arg(string $value): void
|
||||
{
|
||||
$this->args[] = escapeshellarg($value);
|
||||
}
|
||||
|
||||
public function generate(): string
|
||||
{
|
||||
$command = escapeshellarg($this->executable);
|
||||
if (!empty($this->args)) {
|
||||
$command .= " ";
|
||||
$command .= join(" ", $this->args);
|
||||
}
|
||||
|
||||
return escapeshellcmd($command)." 2>&1";
|
||||
}
|
||||
|
||||
public function combineOutput(string $empty_output = ""): string
|
||||
{
|
||||
if (empty($this->output)) {
|
||||
return $empty_output;
|
||||
} else {
|
||||
return implode("\r\n", $this->output);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(bool $fail_on_non_zero_return = false): int
|
||||
{
|
||||
$cmd = $this->generate();
|
||||
exec($cmd, $this->output, $ret);
|
||||
|
||||
$output = $this->combineOutput("nothing");
|
||||
|
||||
log_debug('command_builder', "Command `$cmd` returned $ret and outputted $output");
|
||||
|
||||
if ($fail_on_non_zero_return&&(int)$ret!==(int)0) {
|
||||
throw new SCoreException("Command `$cmd` failed, returning $ret and outputting $output");
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
@ -130,7 +130,7 @@ interface Config
|
||||
*/
|
||||
abstract class BaseConfig implements Config
|
||||
{
|
||||
public array $values = [];
|
||||
public $values = [];
|
||||
|
||||
public function set_int(string $name, ?int $value): void
|
||||
{
|
||||
@ -256,10 +256,12 @@ abstract class BaseConfig implements Config
|
||||
*/
|
||||
class DatabaseConfig extends BaseConfig
|
||||
{
|
||||
private Database $database;
|
||||
private string $table_name;
|
||||
private ?string $sub_column;
|
||||
private ?string $sub_value;
|
||||
/** @var Database */
|
||||
private $database = null;
|
||||
|
||||
private $table_name;
|
||||
private $sub_column;
|
||||
private $sub_value;
|
||||
|
||||
public function __construct(
|
||||
Database $database,
|
||||
@ -321,10 +323,10 @@ class DatabaseConfig extends BaseConfig
|
||||
$params[] = ":sub_value";
|
||||
}
|
||||
|
||||
$this->database->execute($query, $args);
|
||||
$this->database->Execute($query, $args);
|
||||
|
||||
$args["value"] =$this->values[$name];
|
||||
$this->database->execute(
|
||||
$this->database->Execute(
|
||||
"INSERT INTO {$this->table_name} (".join(",", $cols).") VALUES (".join(",", $params).")",
|
||||
$args
|
||||
);
|
||||
@ -332,6 +334,5 @@ class DatabaseConfig extends BaseConfig
|
||||
// rather than deleting and having some other request(s) do a thundering
|
||||
// herd of race-conditioned updates, just save the updated version once here
|
||||
$cache->set("config", $this->values);
|
||||
$this->database->notify("config");
|
||||
}
|
||||
}
|
||||
|
@ -13,23 +13,38 @@ abstract class DatabaseDriver
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
private string $dsn;
|
||||
/** @var string */
|
||||
private $dsn;
|
||||
|
||||
/**
|
||||
* The PDO database connection object, for anyone who wants direct access.
|
||||
* @var null|PDO
|
||||
*/
|
||||
private ?PDO $db = null;
|
||||
public float $dbtime = 0.0;
|
||||
private $db = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
public $dbtime = 0.0;
|
||||
|
||||
/**
|
||||
* Meta info about the database engine.
|
||||
* @var DBEngine|null
|
||||
*/
|
||||
private ?DBEngine $engine = null;
|
||||
private $engine = null;
|
||||
|
||||
/**
|
||||
* A boolean flag to track if we already have an active transaction.
|
||||
* (ie: True if beginTransaction() already called)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $transaction = false;
|
||||
|
||||
/**
|
||||
* How many queries this DB object has run
|
||||
*/
|
||||
public int $query_count = 0;
|
||||
public $query_count = 0;
|
||||
|
||||
public function __construct(string $dsn)
|
||||
{
|
||||
@ -38,9 +53,13 @@ class Database
|
||||
|
||||
private function connect_db(): void
|
||||
{
|
||||
$this->db = new PDO($this->dsn);
|
||||
$this->db = new PDO($this->dsn, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$this->connect_engine();
|
||||
$this->engine->init($this->db);
|
||||
|
||||
$this->begin_transaction();
|
||||
}
|
||||
|
||||
@ -68,19 +87,21 @@ class Database
|
||||
|
||||
public function begin_transaction(): void
|
||||
{
|
||||
if ($this->is_transaction_open() === false) {
|
||||
if ($this->transaction === false) {
|
||||
$this->db->beginTransaction();
|
||||
$this->transaction = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function is_transaction_open(): bool
|
||||
{
|
||||
return !is_null($this->db) && $this->db->inTransaction();
|
||||
return !is_null($this->db) && $this->transaction === true;
|
||||
}
|
||||
|
||||
public function commit(): bool
|
||||
{
|
||||
if ($this->is_transaction_open()) {
|
||||
$this->transaction = false;
|
||||
return $this->db->commit();
|
||||
} else {
|
||||
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
|
||||
@ -90,6 +111,7 @@ class Database
|
||||
public function rollback(): bool
|
||||
{
|
||||
if ($this->is_transaction_open()) {
|
||||
$this->transaction = false;
|
||||
return $this->db->rollback();
|
||||
} else {
|
||||
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
|
||||
@ -104,6 +126,19 @@ class Database
|
||||
return $this->engine->scoreql_to_sql($input);
|
||||
}
|
||||
|
||||
public function scoresql_value_prepare($input)
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
$this->connect_engine();
|
||||
}
|
||||
if ($input===true) {
|
||||
return $this->engine->BOOL_Y;
|
||||
} elseif ($input===false) {
|
||||
return $this->engine->BOOL_N;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public function get_driver_name(): string
|
||||
{
|
||||
if (is_null($this->engine)) {
|
||||
@ -134,11 +169,6 @@ class Database
|
||||
$this->engine->set_timeout($this->db, $time);
|
||||
}
|
||||
|
||||
public function notify(string $channel, ?string $data=null): void
|
||||
{
|
||||
$this->engine->notify($this->db, $channel, $data);
|
||||
}
|
||||
|
||||
public function execute(string $query, array $args = []): PDOStatement
|
||||
{
|
||||
try {
|
||||
@ -223,20 +253,6 @@ class Database
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first column => the second column as an iterable object.
|
||||
*/
|
||||
public function get_pairs_iterable(string $query, array $args = []): Generator
|
||||
{
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$this->count_time("get_pairs_iterable", $_start, $query, $args);
|
||||
foreach ($stmt as $row) {
|
||||
yield $row[0] => $row[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value, or null.
|
||||
*/
|
||||
@ -320,29 +336,4 @@ class Database
|
||||
{
|
||||
return $this->db;
|
||||
}
|
||||
|
||||
public function standardise_boolean(string $table, string $column, bool $include_postgres=false): void
|
||||
{
|
||||
$d = $this->get_driver_name();
|
||||
if ($d == DatabaseDriver::MYSQL) {
|
||||
# In mysql, ENUM('Y', 'N') is secretly INTEGER where Y=1 and N=2.
|
||||
# BOOLEAN is secretly TINYINT where true=1 and false=0.
|
||||
# So we can cast directly from ENUM to BOOLEAN which gives us a
|
||||
# column of values 'true' and 'invalid but who cares lol', which
|
||||
# we can then UPDATE to be 'true' and 'false'.
|
||||
$this->execute("ALTER TABLE $table MODIFY COLUMN $column BOOLEAN;");
|
||||
$this->execute("UPDATE $table SET $column=0 WHERE $column=2;");
|
||||
}
|
||||
if ($d == DatabaseDriver::SQLITE) {
|
||||
# SQLite doesn't care about column types at all, everything is
|
||||
# text, so we can in-place replace a char with a bool
|
||||
$this->execute("UPDATE $table SET $column = ($column IN ('Y', 1))");
|
||||
}
|
||||
if ($d == DatabaseDriver::PGSQL && $include_postgres) {
|
||||
$this->execute("ALTER TABLE $table ADD COLUMN ${column}_b BOOLEAN DEFAULT FALSE NOT NULL");
|
||||
$this->execute("UPDATE $table SET ${column}_b = ($column = 'Y')");
|
||||
$this->execute("ALTER TABLE $table DROP COLUMN $column");
|
||||
$this->execute("ALTER TABLE $table RENAME COLUMN ${column}_b TO $column");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,26 @@ abstract class SCORE
|
||||
{
|
||||
const AIPK = "SCORE_AIPK";
|
||||
const INET = "SCORE_INET";
|
||||
const BOOL_Y = "SCORE_BOOL_Y";
|
||||
const BOOL_N = "SCORE_BOOL_N";
|
||||
const BOOL = "SCORE_BOOL";
|
||||
}
|
||||
|
||||
abstract class DBEngine
|
||||
{
|
||||
public ?string $name = null;
|
||||
/** @var null|string */
|
||||
public $name = null;
|
||||
|
||||
public $BOOL_Y = null;
|
||||
public $BOOL_N = null;
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string
|
||||
public function scoreql_to_sql(string $scoreql): string
|
||||
{
|
||||
return $data;
|
||||
return $scoreql;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string
|
||||
@ -26,13 +33,15 @@ abstract class DBEngine
|
||||
abstract public function set_timeout(PDO $db, int $time);
|
||||
|
||||
abstract public function get_version(PDO $db): string;
|
||||
|
||||
abstract public function notify(PDO $db, string $channel, ?string $data=null): void;
|
||||
}
|
||||
|
||||
class MySQL extends DBEngine
|
||||
{
|
||||
public ?string $name = DatabaseDriver::MYSQL;
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::MYSQL;
|
||||
|
||||
public $BOOL_Y = 'Y';
|
||||
public $BOOL_N = 'N';
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
@ -43,6 +52,9 @@ class MySQL extends DBEngine
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY auto_increment", $data);
|
||||
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
|
||||
$data = str_replace(SCORE::BOOL, "ENUM('Y', 'N')", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -59,10 +71,6 @@ 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 get_version(PDO $db): string
|
||||
{
|
||||
return $db->query('select version()')->fetch()[0];
|
||||
@ -71,7 +79,11 @@ class MySQL extends DBEngine
|
||||
|
||||
class PostgreSQL extends DBEngine
|
||||
{
|
||||
public ?string $name = DatabaseDriver::PGSQL;
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::PGSQL;
|
||||
|
||||
public $BOOL_Y = "true";
|
||||
public $BOOL_N = "false";
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
@ -89,6 +101,9 @@ class PostgreSQL extends DBEngine
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY", $data);
|
||||
$data = str_replace(SCORE::INET, "INET", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "true", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "false", $data);
|
||||
$data = str_replace(SCORE::BOOL, "BOOL", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -103,15 +118,6 @@ class PostgreSQL extends DBEngine
|
||||
$db->exec("SET statement_timeout TO ".$time.";");
|
||||
}
|
||||
|
||||
public function notify(PDO $db, string $channel, ?string $data=null): void
|
||||
{
|
||||
if ($data) {
|
||||
$db->exec("NOTIFY $channel, '$data';");
|
||||
} else {
|
||||
$db->exec("NOTIFY $channel;");
|
||||
}
|
||||
}
|
||||
|
||||
public function get_version(PDO $db): string
|
||||
{
|
||||
return $db->query('select version()')->fetch()[0];
|
||||
@ -119,19 +125,19 @@ class PostgreSQL extends DBEngine
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date): int
|
||||
function _unix_timestamp($date)
|
||||
{
|
||||
return strtotime($date);
|
||||
}
|
||||
function _now(): string
|
||||
function _now()
|
||||
{
|
||||
return date("Y-m-d H:i:s");
|
||||
}
|
||||
function _floor($a): float
|
||||
function _floor($a)
|
||||
{
|
||||
return floor($a);
|
||||
}
|
||||
function _log($a, $b=null): float
|
||||
function _log($a, $b=null)
|
||||
{
|
||||
if (is_null($b)) {
|
||||
return log($a);
|
||||
@ -139,34 +145,39 @@ function _log($a, $b=null): float
|
||||
return log($a, $b);
|
||||
}
|
||||
}
|
||||
function _isnull($a): bool
|
||||
function _isnull($a)
|
||||
{
|
||||
return is_null($a);
|
||||
}
|
||||
function _md5($a): string
|
||||
function _md5($a)
|
||||
{
|
||||
return md5($a);
|
||||
}
|
||||
function _concat($a, $b): string
|
||||
function _concat($a, $b)
|
||||
{
|
||||
return $a . $b;
|
||||
}
|
||||
function _lower($a): string
|
||||
function _lower($a)
|
||||
{
|
||||
return strtolower($a);
|
||||
}
|
||||
function _rand(): int
|
||||
function _rand()
|
||||
{
|
||||
return rand();
|
||||
}
|
||||
function _ln($n): float
|
||||
function _ln($n)
|
||||
{
|
||||
return log($n);
|
||||
}
|
||||
|
||||
class SQLite extends DBEngine
|
||||
{
|
||||
public ?string $name = DatabaseDriver::SQLITE;
|
||||
/** @var string */
|
||||
public $name = DatabaseDriver::SQLITE;
|
||||
|
||||
public $BOOL_Y = 'Y';
|
||||
public $BOOL_N = 'N';
|
||||
|
||||
|
||||
public function init(PDO $db)
|
||||
{
|
||||
@ -188,6 +199,9 @@ class SQLite extends DBEngine
|
||||
{
|
||||
$data = str_replace(SCORE::AIPK, "INTEGER PRIMARY KEY", $data);
|
||||
$data = str_replace(SCORE::INET, "VARCHAR(45)", $data);
|
||||
$data = str_replace(SCORE::BOOL_Y, "'$this->BOOL_Y'", $data);
|
||||
$data = str_replace(SCORE::BOOL_N, "'$this->BOOL_N'", $data);
|
||||
$data = str_replace(SCORE::BOOL, "CHAR(1)", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -215,10 +229,6 @@ 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 get_version(PDO $db): string
|
||||
{
|
||||
return $db->query('select sqlite_version()')->fetch()[0];
|
||||
|
@ -6,13 +6,13 @@
|
||||
*/
|
||||
abstract class Event
|
||||
{
|
||||
public bool $stop_processing = false;
|
||||
public $stop_processing = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
public function __toString()
|
||||
{
|
||||
return var_export($this, true);
|
||||
}
|
||||
@ -42,11 +42,19 @@ class InitExtEvent extends Event
|
||||
class PageRequestEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array
|
||||
*/
|
||||
public $args;
|
||||
public int $arg_count;
|
||||
public int $part_count;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $arg_count;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $part_count;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
@ -171,12 +179,15 @@ class PageRequestEvent extends Event
|
||||
*/
|
||||
class CommandEvent extends Event
|
||||
{
|
||||
public string $cmd = "help";
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $cmd = "help";
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array
|
||||
*/
|
||||
public array $args = [];
|
||||
public $args = [];
|
||||
|
||||
/**
|
||||
* #param string[] $args
|
||||
@ -245,18 +256,24 @@ class TextFormattingEvent extends Event
|
||||
{
|
||||
/**
|
||||
* For reference
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $original;
|
||||
public $original;
|
||||
|
||||
/**
|
||||
* with formatting applied
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $formatted;
|
||||
public $formatted;
|
||||
|
||||
/**
|
||||
* with formatting removed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $stripped;
|
||||
public $stripped;
|
||||
|
||||
public function __construct(string $text)
|
||||
{
|
||||
@ -279,30 +296,38 @@ class LogEvent extends Event
|
||||
{
|
||||
/**
|
||||
* a category, normally the extension name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $section;
|
||||
public $section;
|
||||
|
||||
/**
|
||||
* See python...
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $priority = 0;
|
||||
public $priority = 0;
|
||||
|
||||
/**
|
||||
* Free text to be logged
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $message;
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* The time that the event was created
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $time;
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* Extra data to be held separate
|
||||
*
|
||||
* @var string[]
|
||||
* @var array
|
||||
*/
|
||||
public array $args;
|
||||
public $args;
|
||||
|
||||
public function __construct(string $section, int $priority, string $message)
|
||||
{
|
||||
|
@ -7,8 +7,11 @@
|
||||
*/
|
||||
class SCoreException extends RuntimeException
|
||||
{
|
||||
public ?string $query;
|
||||
public string $error;
|
||||
/** @var string|null */
|
||||
public $query;
|
||||
|
||||
/** @var string */
|
||||
public $error;
|
||||
|
||||
public function __construct(string $msg, ?string $query=null)
|
||||
{
|
||||
@ -20,16 +23,21 @@ class SCoreException extends RuntimeException
|
||||
|
||||
class InstallerException extends RuntimeException
|
||||
{
|
||||
public string $title;
|
||||
public string $body;
|
||||
public int $exit_code;
|
||||
/** @var string */
|
||||
public $title;
|
||||
|
||||
public function __construct(string $title, string $body, int $exit_code)
|
||||
/** @var string */
|
||||
public $body;
|
||||
|
||||
/** @var int */
|
||||
public $code;
|
||||
|
||||
public function __construct(string $title, string $body, int $code)
|
||||
{
|
||||
parent::__construct($body);
|
||||
$this->title = $title;
|
||||
$this->body = $body;
|
||||
$this->exit_code = $exit_code;
|
||||
$this->code = $code;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,11 +13,16 @@
|
||||
*/
|
||||
abstract class Extension
|
||||
{
|
||||
public string $key;
|
||||
protected ?Themelet $theme;
|
||||
public ?ExtensionInfo $info;
|
||||
/** @var string */
|
||||
public $key;
|
||||
|
||||
private static array $enabled_extensions = [];
|
||||
/** @var Themelet */
|
||||
protected $theme;
|
||||
|
||||
/** @var ExtensionInfo */
|
||||
public $info;
|
||||
|
||||
private static $enabled_extensions = [];
|
||||
|
||||
public function __construct($class = null)
|
||||
{
|
||||
@ -56,7 +61,7 @@ abstract class Extension
|
||||
return 50;
|
||||
}
|
||||
|
||||
public static function determine_enabled_extensions(): void
|
||||
public static function determine_enabled_extensions()
|
||||
{
|
||||
self::$enabled_extensions = [];
|
||||
foreach (array_merge(
|
||||
@ -117,31 +122,34 @@ abstract class ExtensionInfo
|
||||
public const LICENSE_MIT = "MIT";
|
||||
public const LICENSE_WTFPL = "WTFPL";
|
||||
|
||||
public const VISIBLE_DEFAULT = "default";
|
||||
public const VISIBLE_ADMIN = "admin";
|
||||
public const VISIBLE_HIDDEN = "hidden";
|
||||
private const VALID_VISIBILITY = [self::VISIBLE_DEFAULT, self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
|
||||
private const VALID_VISIBILITY = [self::VISIBLE_ADMIN, self::VISIBLE_HIDDEN];
|
||||
|
||||
public string $key;
|
||||
public $key;
|
||||
|
||||
public bool $core = false;
|
||||
public bool $beta = false;
|
||||
public $core = false;
|
||||
|
||||
public string $name;
|
||||
public string $license;
|
||||
public string $description;
|
||||
public array $authors = [];
|
||||
public array $dependencies = [];
|
||||
public array $conflicts = [];
|
||||
public string $visibility = self::VISIBLE_DEFAULT;
|
||||
public ?string $link = null;
|
||||
public ?string $version = null;
|
||||
public ?string $documentation = null;
|
||||
public $beta = false;
|
||||
|
||||
/** @var string[] which DBs this ext supports (blank for 'all') */
|
||||
public array $db_support = [];
|
||||
private ?bool $supported = null;
|
||||
private ?string $support_info = null;
|
||||
public $name;
|
||||
public $authors = [];
|
||||
public $link;
|
||||
public $license;
|
||||
public $version;
|
||||
public $dependencies = [];
|
||||
public $visibility;
|
||||
public $description;
|
||||
public $documentation;
|
||||
|
||||
/** @var array which DBs this ext supports (blank for 'all') */
|
||||
public $db_support = [];
|
||||
|
||||
/** @var bool */
|
||||
private $supported = null;
|
||||
|
||||
/** @var string */
|
||||
private $support_info = null;
|
||||
|
||||
public function is_supported(): bool
|
||||
{
|
||||
@ -159,9 +167,9 @@ abstract class ExtensionInfo
|
||||
return $this->support_info;
|
||||
}
|
||||
|
||||
private static array $all_info_by_key = [];
|
||||
private static array $all_info_by_class = [];
|
||||
private static array $core_extensions = [];
|
||||
private static $all_info_by_key = [];
|
||||
private static $all_info_by_class = [];
|
||||
private static $core_extensions = [];
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
@ -185,13 +193,6 @@ abstract class ExtensionInfo
|
||||
if (!empty($this->db_support) && !in_array($database->get_driver_name(), $this->db_support)) {
|
||||
$this->support_info .= "Database not supported. ";
|
||||
}
|
||||
if (!empty($this->conflicts)) {
|
||||
$intersects = array_intersect($this->conflicts, Extension::get_enabled_extensions());
|
||||
if (!empty($intersects)) {
|
||||
$this->support_info .= "Conflicts with other extension(s): " . join(", ", $intersects);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional checks here as needed
|
||||
|
||||
$this->supported = empty($this->support_info);
|
||||
@ -234,7 +235,7 @@ abstract class ExtensionInfo
|
||||
|
||||
public static function load_all_extension_info()
|
||||
{
|
||||
foreach (get_subclasses_of("ExtensionInfo") as $class) {
|
||||
foreach (getSubclassesOf("ExtensionInfo") as $class) {
|
||||
$extension_info = new $class();
|
||||
if (array_key_exists($extension_info->key, self::$all_info_by_key)) {
|
||||
throw new ScoreException("Extension Info $class with key $extension_info->key has already been loaded");
|
||||
@ -274,7 +275,7 @@ abstract class FormatterExtension extends Extension
|
||||
*/
|
||||
abstract class DataHandlerExtension extends Extension
|
||||
{
|
||||
protected array $SUPPORTED_MIME = [];
|
||||
protected $SUPPORTED_MIME = [];
|
||||
|
||||
protected function move_upload_to_archive(DataUploadEvent $event)
|
||||
{
|
||||
@ -290,11 +291,11 @@ abstract class DataHandlerExtension extends Extension
|
||||
|
||||
public function onDataUpload(DataUploadEvent $event)
|
||||
{
|
||||
$supported_mime = $this->supported_mime($event->mime);
|
||||
$supported_ext = $this->supported_ext($event->type);
|
||||
$check_contents = $this->check_contents($event->tmpname);
|
||||
if ($supported_mime && $check_contents) {
|
||||
if ($supported_ext && $check_contents) {
|
||||
$this->move_upload_to_archive($event);
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->mime));
|
||||
send_event(new ThumbnailGenerationEvent($event->hash, $event->type));
|
||||
|
||||
/* Check if we are replacing an image */
|
||||
if (!is_null($event->replace_id)) {
|
||||
@ -304,20 +305,20 @@ abstract class DataHandlerExtension extends Extension
|
||||
$existing = Image::by_id($event->replace_id);
|
||||
|
||||
if (is_null($existing)) {
|
||||
throw new UploadException("Post to replace does not exist!");
|
||||
throw new UploadException("Image to replace does not exist!");
|
||||
}
|
||||
if ($existing->hash === $event->metadata['hash']) {
|
||||
throw new UploadException("The uploaded post is the same as the one to replace.");
|
||||
throw new UploadException("The uploaded image 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->metadata['hash']), $event->metadata);
|
||||
if (is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create post object from data");
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
if (empty($image->get_mime())) {
|
||||
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
|
||||
if (empty($image->ext)) {
|
||||
throw new UploadException("Unable to determine extension for ". $event->tmpname);
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
@ -326,16 +327,14 @@ abstract class DataHandlerExtension extends Extension
|
||||
}
|
||||
|
||||
send_event(new ImageReplaceEvent($event->replace_id, $image));
|
||||
$_id = $event->replace_id;
|
||||
assert(!is_null($_id));
|
||||
$event->image_id = $_id;
|
||||
$event->image_id = $event->replace_id;
|
||||
} else {
|
||||
$image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata);
|
||||
if (is_null($image)) {
|
||||
throw new UploadException("Data handler failed to create post object from data");
|
||||
throw new UploadException("Data handler failed to create image object from data");
|
||||
}
|
||||
if (empty($image->get_mime())) {
|
||||
throw new UploadException("Unable to determine MIME for ". $event->tmpname);
|
||||
if (empty($image->ext)) {
|
||||
throw new UploadException("Unable to determine extension for ". $event->tmpname);
|
||||
}
|
||||
try {
|
||||
send_event(new MediaCheckPropertiesEvent($image));
|
||||
@ -359,7 +358,7 @@ abstract class DataHandlerExtension extends Extension
|
||||
send_event(new LockSetEvent($image, !empty($locked)));
|
||||
}
|
||||
}
|
||||
} elseif ($supported_mime && !$check_contents) {
|
||||
} elseif ($supported_ext && !$check_contents) {
|
||||
// We DO support this extension - but the file looks corrupt
|
||||
throw new UploadException("Invalid or corrupted file");
|
||||
}
|
||||
@ -368,15 +367,15 @@ abstract class DataHandlerExtension extends Extension
|
||||
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
|
||||
{
|
||||
$result = false;
|
||||
if ($this->supported_mime($event->mime)) {
|
||||
if ($this->supported_ext($event->type)) {
|
||||
if ($event->force) {
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
$result = $this->create_thumb($event->hash, $event->type);
|
||||
} else {
|
||||
$outname = warehouse_path(Image::THUMBNAIL_DIR, $event->hash);
|
||||
if (file_exists($outname)) {
|
||||
return;
|
||||
}
|
||||
$result = $this->create_thumb($event->hash, $event->mime);
|
||||
$result = $this->create_thumb($event->hash, $event->type);
|
||||
}
|
||||
}
|
||||
if ($result) {
|
||||
@ -387,7 +386,7 @@ abstract class DataHandlerExtension extends Extension
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $page;
|
||||
if ($this->supported_mime($event->image->get_mime())) {
|
||||
if ($this->supported_ext($event->image->ext)) {
|
||||
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
|
||||
$this->theme->display_image($page, $event->image);
|
||||
}
|
||||
@ -395,23 +394,25 @@ abstract class DataHandlerExtension extends Extension
|
||||
|
||||
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
|
||||
{
|
||||
if ($this->supported_mime($event->mime)) {
|
||||
if ($this->supported_ext($event->ext)) {
|
||||
$this->media_check_properties($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function create_image_from_data(string $filename, array $metadata): Image
|
||||
{
|
||||
global $config;
|
||||
|
||||
$image = new Image();
|
||||
|
||||
$image->filesize = $metadata['size'];
|
||||
$image->hash = $metadata['hash'];
|
||||
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
|
||||
|
||||
if (array_key_exists("extension", $metadata)) {
|
||||
$image->set_mime(MimeType::get_for_file($filename, $metadata["extension"]));
|
||||
} else {
|
||||
$image->set_mime(MimeType::get_for_file($filename));
|
||||
if ($config->get_bool("upload_use_mime")) {
|
||||
$image->ext = get_extension_for_file($filename);
|
||||
}
|
||||
if (empty($image->ext)) {
|
||||
$image->ext = (($pos = strpos($metadata['extension'], '?')) !== false) ? substr($metadata['extension'], 0, $pos) : $metadata['extension'];
|
||||
}
|
||||
|
||||
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
|
||||
@ -422,35 +423,22 @@ abstract class DataHandlerExtension extends Extension
|
||||
|
||||
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(string $hash, string $type): bool;
|
||||
|
||||
protected function supported_mime(string $mime): bool
|
||||
protected function supported_ext(string $ext): bool
|
||||
{
|
||||
return MimeType::matches_array($mime, $this->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
public static function get_all_supported_mimes(): array
|
||||
{
|
||||
$arr = [];
|
||||
foreach (get_subclasses_of("DataHandlerExtension") as $handler) {
|
||||
$handler = (new $handler());
|
||||
$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("TranscodeImage")) {
|
||||
$arr = array_merge($arr, TranscodeImage::get_enabled_mimes());
|
||||
}
|
||||
|
||||
$arr = array_unique($arr);
|
||||
return $arr;
|
||||
return in_array(get_mime_for_extension($ext), $this->SUPPORTED_MIME);
|
||||
}
|
||||
|
||||
public static function get_all_supported_exts(): array
|
||||
{
|
||||
$arr = [];
|
||||
foreach (self::get_all_supported_mimes() as $mime) {
|
||||
$arr = array_merge($arr, FileExtension::get_all_for_mime($mime));
|
||||
foreach (getSubclassesOf("DataHandlerExtension") as $handler) {
|
||||
$handler = (new $handler());
|
||||
|
||||
foreach ($handler->SUPPORTED_MIME as $mime) {
|
||||
$arr = array_merge($arr, get_all_extension_for_mime($mime));
|
||||
}
|
||||
}
|
||||
$arr = array_unique($arr);
|
||||
return $arr;
|
||||
|
448
core/filetypes.php
Normal file
448
core/filetypes.php
Normal file
@ -0,0 +1,448 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* MIME types and extension information and resolvers *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
const EXTENSION_ANI = 'ani';
|
||||
const EXTENSION_ASC = 'asc';
|
||||
const EXTENSION_ASF = 'asf';
|
||||
const EXTENSION_AVI = 'avi';
|
||||
const EXTENSION_BMP = 'bmp';
|
||||
const EXTENSION_BZIP = 'bz';
|
||||
const EXTENSION_BZIP2 = 'bz2';
|
||||
const EXTENSION_CBR = 'cbr';
|
||||
const EXTENSION_CBZ = 'cbz';
|
||||
const EXTENSION_CBT = 'cbt';
|
||||
const EXTENSION_CBA = 'cbA';
|
||||
const EXTENSION_CB7 = 'cb7';
|
||||
const EXTENSION_CSS = 'css';
|
||||
const EXTENSION_CSV = 'csv';
|
||||
const EXTENSION_CUR = 'cur';
|
||||
const EXTENSION_FLASH = 'swf';
|
||||
const EXTENSION_FLASH_VIDEO = 'flv';
|
||||
const EXTENSION_GIF = 'gif';
|
||||
const EXTENSION_GZIP = 'gz';
|
||||
const EXTENSION_HTML = 'html';
|
||||
const EXTENSION_HTM = 'htm';
|
||||
const EXTENSION_ICO = 'ico';
|
||||
const EXTENSION_JFIF = 'jfif';
|
||||
const EXTENSION_JFI = 'jfi';
|
||||
const EXTENSION_JPEG = 'jpeg';
|
||||
const EXTENSION_JPG = 'jpg';
|
||||
const EXTENSION_JS = 'js';
|
||||
const EXTENSION_JSON = 'json';
|
||||
const EXTENSION_MKV = 'mkv';
|
||||
const EXTENSION_MP3 = 'mp3';
|
||||
const EXTENSION_MP4 = 'mp4';
|
||||
const EXTENSION_M4V = 'm4v';
|
||||
const EXTENSION_M4A = 'm4a';
|
||||
const EXTENSION_MPEG = 'mpeg';
|
||||
const EXTENSION_MPG = 'mpg';
|
||||
const EXTENSION_OGG = 'ogg';
|
||||
const EXTENSION_OGG_VIDEO = 'ogv';
|
||||
const EXTENSION_OGG_AUDIO = 'oga';
|
||||
const EXTENSION_PDF = 'pdf';
|
||||
const EXTENSION_PHP = 'php';
|
||||
const EXTENSION_PHP5 = 'php5';
|
||||
const EXTENSION_PNG = 'png';
|
||||
const EXTENSION_PSD = 'psd';
|
||||
const EXTENSION_MOV = 'mov';
|
||||
const EXTENSION_RSS = 'rss';
|
||||
const EXTENSION_SVG = 'svg';
|
||||
const EXTENSION_TAR = 'tar';
|
||||
const EXTENSION_TEXT = 'txt';
|
||||
const EXTENSION_TIFF = 'tiff';
|
||||
const EXTENSION_TIF = 'tif';
|
||||
const EXTENSION_WAV = 'wav';
|
||||
const EXTENSION_WEBM = 'webm';
|
||||
const EXTENSION_WEBP = 'webp';
|
||||
const EXTENSION_WMA = 'wma';
|
||||
const EXTENSION_WMV = 'wmv';
|
||||
const EXTENSION_XML = 'xml';
|
||||
const EXTENSION_XSL = 'xsl';
|
||||
const EXTENSION_ZIP = 'zip';
|
||||
|
||||
|
||||
// Couldn't find a mimetype for ani, so made one up based on it being a riff container
|
||||
const MIME_TYPE_ANI = 'application/riff+ani';
|
||||
const MIME_TYPE_ASF = 'video/x-ms-asf';
|
||||
const MIME_TYPE_AVI = 'video/x-msvideo';
|
||||
// Went with mime types from http://fileformats.archiveteam.org/wiki/Comic_Book_Archive
|
||||
const MIME_TYPE_COMIC_ZIP = 'application/vnd.comicbook+zip';
|
||||
const MIME_TYPE_COMIC_RAR = 'application/vnd.comicbook-rar';
|
||||
const MIME_TYPE_BMP = 'image/x-ms-bmp';
|
||||
const MIME_TYPE_BZIP = 'application/x-bzip';
|
||||
const MIME_TYPE_BZIP2 = 'application/x-bzip2';
|
||||
const MIME_TYPE_CSS = 'text/css';
|
||||
const MIME_TYPE_CSV = 'text/csv';
|
||||
const MIME_TYPE_FLASH = 'application/x-shockwave-flash';
|
||||
const MIME_TYPE_FLASH_VIDEO = 'video/x-flv';
|
||||
const MIME_TYPE_GIF = 'image/gif';
|
||||
const MIME_TYPE_GZIP = 'application/x-gzip';
|
||||
const MIME_TYPE_HTML = 'text/html';
|
||||
const MIME_TYPE_ICO = 'image/x-icon';
|
||||
const MIME_TYPE_JPEG = 'image/jpeg';
|
||||
const MIME_TYPE_JS = 'text/javascript';
|
||||
const MIME_TYPE_JSON = 'application/json';
|
||||
const MIME_TYPE_MKV = 'video/x-matroska';
|
||||
const MIME_TYPE_MP3 = 'audio/mpeg';
|
||||
const MIME_TYPE_MP4_AUDIO = 'audio/mp4';
|
||||
const MIME_TYPE_MP4_VIDEO = 'video/mp4';
|
||||
const MIME_TYPE_MPEG = 'video/mpeg';
|
||||
const MIME_TYPE_OCTET_STREAM = 'application/octet-stream';
|
||||
const MIME_TYPE_OGG = 'application/ogg';
|
||||
const MIME_TYPE_OGG_VIDEO = 'video/ogg';
|
||||
const MIME_TYPE_OGG_AUDIO = 'audio/ogg';
|
||||
const MIME_TYPE_PDF = 'application/pdf';
|
||||
const MIME_TYPE_PHP = 'text/x-php';
|
||||
const MIME_TYPE_PNG = 'image/png';
|
||||
const MIME_TYPE_PSD = 'image/vnd.adobe.photoshop';
|
||||
const MIME_TYPE_QUICKTIME = 'video/quicktime';
|
||||
const MIME_TYPE_RSS = 'application/rss+xml';
|
||||
const MIME_TYPE_SVG = 'image/svg+xml';
|
||||
const MIME_TYPE_TAR = 'application/x-tar';
|
||||
const MIME_TYPE_TEXT = 'text/plain';
|
||||
const MIME_TYPE_TIFF = 'image/tiff';
|
||||
const MIME_TYPE_WAV = 'audio/x-wav';
|
||||
const MIME_TYPE_WEBM = 'video/webm';
|
||||
const MIME_TYPE_WEBP = 'image/webp';
|
||||
const MIME_TYPE_WIN_BITMAP = 'image/x-win-bitmap';
|
||||
const MIME_TYPE_XML = 'text/xml';
|
||||
const MIME_TYPE_XML_APPLICATION = 'application/xml';
|
||||
const MIME_TYPE_XSL = 'application/xsl+xml';
|
||||
const MIME_TYPE_ZIP = 'application/zip';
|
||||
|
||||
const MIME_TYPE_MAP_NAME = 'name';
|
||||
const MIME_TYPE_MAP_EXT = 'ext';
|
||||
const MIME_TYPE_MAP_MIME = 'mime';
|
||||
|
||||
// Mime type map. Each entry in the MIME_TYPE_ARRAY represents a kind of file, identified by the "correct" mimetype as the key.
|
||||
// The value for each entry is a map of twokeys, ext and mime.
|
||||
// ext's value is an array of all of the extensions that the file type can use, with the "correct" one being first.
|
||||
// mime's value is an array of all mime types that the file type is known to use, with the current "correct" one being first.
|
||||
|
||||
const MIME_TYPE_MAP = [
|
||||
MIME_TYPE_ANI => [
|
||||
MIME_TYPE_MAP_NAME => "ANI Cursor",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_ANI],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_ANI],
|
||||
],
|
||||
MIME_TYPE_AVI => [
|
||||
MIME_TYPE_MAP_NAME => "AVI",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_AVI],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_AVI,'video/avi','video/msvideo'],
|
||||
],
|
||||
MIME_TYPE_ASF => [
|
||||
MIME_TYPE_MAP_NAME => "ASF/WMV",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_ASF,EXTENSION_WMA,EXTENSION_WMV],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_ASF,'audio/x-ms-wma','video/x-ms-wmv'],
|
||||
],
|
||||
MIME_TYPE_BMP => [
|
||||
MIME_TYPE_MAP_NAME => "BMP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_BMP],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_BMP],
|
||||
],
|
||||
MIME_TYPE_BZIP => [
|
||||
MIME_TYPE_MAP_NAME => "BZIP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP],
|
||||
],
|
||||
MIME_TYPE_BZIP2 => [
|
||||
MIME_TYPE_MAP_NAME => "BZIP2",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_BZIP2],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_BZIP2],
|
||||
],
|
||||
MIME_TYPE_COMIC_ZIP => [
|
||||
MIME_TYPE_MAP_NAME => "CBZ",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_CBZ],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_COMIC_ZIP],
|
||||
],
|
||||
MIME_TYPE_CSS => [
|
||||
MIME_TYPE_MAP_NAME => "Cascading Style Sheet",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_CSS],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSS],
|
||||
],
|
||||
MIME_TYPE_CSV => [
|
||||
MIME_TYPE_MAP_NAME => "CSV",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_CSV],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_CSV],
|
||||
],
|
||||
MIME_TYPE_FLASH => [
|
||||
MIME_TYPE_MAP_NAME => "Flash",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH],
|
||||
],
|
||||
MIME_TYPE_FLASH_VIDEO => [
|
||||
MIME_TYPE_MAP_NAME => "Flash Video",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_FLASH_VIDEO],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_FLASH_VIDEO,'video/flv'],
|
||||
],
|
||||
MIME_TYPE_GIF => [
|
||||
MIME_TYPE_MAP_NAME => "GIF",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_GIF],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_GIF],
|
||||
],
|
||||
MIME_TYPE_GZIP => [
|
||||
MIME_TYPE_MAP_NAME => "GZIP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_GZIP],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
|
||||
],
|
||||
MIME_TYPE_HTML => [
|
||||
MIME_TYPE_MAP_NAME => "HTML",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_HTM, EXTENSION_HTML],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_HTML],
|
||||
],
|
||||
MIME_TYPE_ICO => [
|
||||
MIME_TYPE_MAP_NAME => "Icon",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_ICO, EXTENSION_CUR],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_ICO, MIME_TYPE_WIN_BITMAP],
|
||||
],
|
||||
MIME_TYPE_JPEG => [
|
||||
MIME_TYPE_MAP_NAME => "JPEG",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_JPG, EXTENSION_JPEG, EXTENSION_JFIF, EXTENSION_JFI],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_JPEG],
|
||||
],
|
||||
MIME_TYPE_JS => [
|
||||
MIME_TYPE_MAP_NAME => "JavaScript",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_JS],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_JS],
|
||||
],
|
||||
MIME_TYPE_JSON => [
|
||||
MIME_TYPE_MAP_NAME => "JSON",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_JSON],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_JSON],
|
||||
],
|
||||
MIME_TYPE_MKV => [
|
||||
MIME_TYPE_MAP_NAME => "Matroska",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_MKV],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_MKV],
|
||||
],
|
||||
MIME_TYPE_MP3 => [
|
||||
MIME_TYPE_MAP_NAME => "MP3",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_MP3],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP3],
|
||||
],
|
||||
MIME_TYPE_MP4_AUDIO => [
|
||||
MIME_TYPE_MAP_NAME => "MP4 Audio",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_M4A],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_AUDIO,"audio/m4a"],
|
||||
],
|
||||
MIME_TYPE_MP4_VIDEO => [
|
||||
MIME_TYPE_MAP_NAME => "MP4 Video",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_MP4,EXTENSION_M4V],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_MP4_VIDEO,'video/x-m4v'],
|
||||
],
|
||||
MIME_TYPE_MPEG => [
|
||||
MIME_TYPE_MAP_NAME => "MPEG",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_MPG,EXTENSION_MPEG],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_MPEG],
|
||||
],
|
||||
MIME_TYPE_PDF => [
|
||||
MIME_TYPE_MAP_NAME => "PDF",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_PDF],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_PDF],
|
||||
],
|
||||
MIME_TYPE_PHP => [
|
||||
MIME_TYPE_MAP_NAME => "PHP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_PHP,EXTENSION_PHP5],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_PHP],
|
||||
],
|
||||
MIME_TYPE_PNG => [
|
||||
MIME_TYPE_MAP_NAME => "PNG",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_PNG],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_PNG],
|
||||
],
|
||||
MIME_TYPE_PSD => [
|
||||
MIME_TYPE_MAP_NAME => "PSD",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_PSD],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_PSD],
|
||||
],
|
||||
MIME_TYPE_OGG_AUDIO => [
|
||||
MIME_TYPE_MAP_NAME => "Ogg Vorbis",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_AUDIO,EXTENSION_OGG],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_AUDIO,MIME_TYPE_OGG],
|
||||
],
|
||||
MIME_TYPE_OGG_VIDEO => [
|
||||
MIME_TYPE_MAP_NAME => "Ogg Theora",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_OGG_VIDEO],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_OGG_VIDEO],
|
||||
],
|
||||
MIME_TYPE_QUICKTIME => [
|
||||
MIME_TYPE_MAP_NAME => "Quicktime",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_MOV],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_QUICKTIME],
|
||||
],
|
||||
MIME_TYPE_RSS => [
|
||||
MIME_TYPE_MAP_NAME => "RSS",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_RSS],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_RSS],
|
||||
],
|
||||
MIME_TYPE_SVG => [
|
||||
MIME_TYPE_MAP_NAME => "SVG",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_SVG],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_SVG],
|
||||
],
|
||||
MIME_TYPE_TAR => [
|
||||
MIME_TYPE_MAP_NAME => "TAR",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_TAR],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_TAR],
|
||||
],
|
||||
MIME_TYPE_TEXT => [
|
||||
MIME_TYPE_MAP_NAME => "Text",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_TEXT, EXTENSION_ASC],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_TEXT],
|
||||
],
|
||||
MIME_TYPE_TIFF => [
|
||||
MIME_TYPE_MAP_NAME => "TIFF",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_TIF,EXTENSION_TIFF],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_TIFF],
|
||||
],
|
||||
MIME_TYPE_WAV => [
|
||||
MIME_TYPE_MAP_NAME => "Wave",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_WAV],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_WAV],
|
||||
],
|
||||
MIME_TYPE_WEBM => [
|
||||
MIME_TYPE_MAP_NAME => "WebM",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_WEBM],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBM],
|
||||
],
|
||||
MIME_TYPE_WEBP => [
|
||||
MIME_TYPE_MAP_NAME => "WebP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_WEBP],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_WEBP],
|
||||
],
|
||||
MIME_TYPE_XML => [
|
||||
MIME_TYPE_MAP_NAME => "XML",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_XML],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_XML,MIME_TYPE_XML_APPLICATION],
|
||||
],
|
||||
MIME_TYPE_XSL => [
|
||||
MIME_TYPE_MAP_NAME => "XSL",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_XSL],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_XSL],
|
||||
],
|
||||
MIME_TYPE_ZIP => [
|
||||
MIME_TYPE_MAP_NAME => "ZIP",
|
||||
MIME_TYPE_MAP_EXT => [EXTENSION_ZIP],
|
||||
MIME_TYPE_MAP_MIME => [MIME_TYPE_ZIP],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the mimetype that matches the provided extension.
|
||||
*/
|
||||
function get_mime_for_extension(string $ext): ?string
|
||||
{
|
||||
$ext = strtolower($ext);
|
||||
|
||||
foreach (MIME_TYPE_MAP as $key=>$value) {
|
||||
if (in_array($ext, $value[MIME_TYPE_MAP_EXT])) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mimetype for the specified file, trying file inspection methods before falling back on extension-based detection.
|
||||
* @param String $file
|
||||
* @param String $ext The files extension, for if the current filename somehow lacks the extension
|
||||
* @return String The extension that was found.
|
||||
*/
|
||||
function get_mime(string $file, string $ext=""): string
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
throw new SCoreException("File not found: ".$file);
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (extension_loaded('fileinfo')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
try {
|
||||
$type = finfo_file($finfo, $file);
|
||||
} finally {
|
||||
finfo_close($finfo);
|
||||
}
|
||||
} elseif (function_exists('mime_content_type')) {
|
||||
// If anyone is still using mime_content_type()
|
||||
$type = trim(mime_content_type($file));
|
||||
}
|
||||
|
||||
if ($type===false || empty($type)) {
|
||||
// Checking by extension is our last resort
|
||||
if ($ext==null||strlen($ext) == 0) {
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
$type = get_mime_for_extension($ext);
|
||||
}
|
||||
|
||||
if ($type !== false && strlen($type) > 0) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return MIME_TYPE_OCTET_STREAM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension associated with the specified mimetype.
|
||||
*/
|
||||
function get_extension(?string $mime_type): ?string
|
||||
{
|
||||
if (empty($mime_type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($mime_type==MIME_TYPE_OCTET_STREAM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (MIME_TYPE_MAP as $key=>$value) {
|
||||
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
|
||||
return $value[MIME_TYPE_MAP_EXT][0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the file extensions associated with the specified mimetype.
|
||||
*/
|
||||
function get_all_extension_for_mime(?string $mime_type): array
|
||||
{
|
||||
$output = [];
|
||||
if (empty($mime_type)) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
foreach (MIME_TYPE_MAP as $key=>$value) {
|
||||
if (in_array($mime_type, $value[MIME_TYPE_MAP_MIME])) {
|
||||
$output = array_merge($output, $value[MIME_TYPE_MAP_EXT]);
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an the extension defined in MIME_TYPE_MAP for a file.
|
||||
*
|
||||
* @param String $file_path
|
||||
* @return String The extension that was found, or null if one can not be found.
|
||||
*/
|
||||
function get_extension_for_file(String $file_path): ?String
|
||||
{
|
||||
$mime = get_mime($file_path);
|
||||
if (!empty($mime)) {
|
||||
if ($mime==MIME_TYPE_OCTET_STREAM) {
|
||||
return null;
|
||||
} else {
|
||||
$ext = get_extension($mime);
|
||||
}
|
||||
if (!empty($ext)) {
|
||||
return $ext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
@ -5,9 +5,13 @@
|
||||
*/
|
||||
class ImageAdditionEvent extends Event
|
||||
{
|
||||
public User $user;
|
||||
public Image $image;
|
||||
public bool $merged = false;
|
||||
/** @var User */
|
||||
public $user;
|
||||
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
public $merged = false;
|
||||
|
||||
/**
|
||||
* Inserts a new image into the database with its associated
|
||||
@ -30,8 +34,11 @@ class ImageAdditionException extends SCoreException
|
||||
*/
|
||||
class ImageDeletionEvent extends Event
|
||||
{
|
||||
public Image $image;
|
||||
public bool $force = false;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
/** @var bool */
|
||||
public $force = false;
|
||||
|
||||
/**
|
||||
* Deletes an image.
|
||||
@ -52,8 +59,10 @@ class ImageDeletionEvent extends Event
|
||||
*/
|
||||
class ImageReplaceEvent extends Event
|
||||
{
|
||||
public int $id;
|
||||
public Image $image;
|
||||
/** @var int */
|
||||
public $id;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
/**
|
||||
* Replaces an image.
|
||||
@ -79,19 +88,24 @@ class ImageReplaceException extends SCoreException
|
||||
*/
|
||||
class ThumbnailGenerationEvent extends Event
|
||||
{
|
||||
public string $hash;
|
||||
public string $mime;
|
||||
public bool $force;
|
||||
public bool $generated;
|
||||
/** @var string */
|
||||
public $hash;
|
||||
/** @var string */
|
||||
public $type;
|
||||
/** @var bool */
|
||||
public $force;
|
||||
|
||||
/** @var bool */
|
||||
public $generated;
|
||||
|
||||
/**
|
||||
* Request a thumbnail be made for an image object
|
||||
*/
|
||||
public function __construct(string $hash, string $mime, bool $force=false)
|
||||
public function __construct(string $hash, string $type, bool $force=false)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->hash = $hash;
|
||||
$this->mime = $mime;
|
||||
$this->type = $type;
|
||||
$this->force = $force;
|
||||
$this->generated = false;
|
||||
}
|
||||
@ -107,10 +121,14 @@ class ThumbnailGenerationEvent extends Event
|
||||
*/
|
||||
class ParseLinkTemplateEvent extends Event
|
||||
{
|
||||
public string $link;
|
||||
public string $text;
|
||||
public string $original;
|
||||
public Image $image;
|
||||
/** @var string */
|
||||
public $link;
|
||||
/** @var string */
|
||||
public $text;
|
||||
/** @var string */
|
||||
public $original;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
|
||||
public function __construct(string $link, Image $image)
|
||||
{
|
||||
|
@ -13,31 +13,64 @@ class Image
|
||||
public const IMAGE_DIR = "images";
|
||||
public const THUMBNAIL_DIR = "thumbs";
|
||||
|
||||
public ?int $id = null;
|
||||
public int $height;
|
||||
public int $width;
|
||||
public string $hash;
|
||||
public int $filesize;
|
||||
public string $filename;
|
||||
private string $ext;
|
||||
private string $mime;
|
||||
public static $order_sql = null; // this feels ugly
|
||||
|
||||
/** @var null|int */
|
||||
public $id = null;
|
||||
|
||||
/** @var int */
|
||||
public $height;
|
||||
|
||||
/** @var int */
|
||||
public $width;
|
||||
|
||||
/** @var string */
|
||||
public $hash;
|
||||
|
||||
/** @var int */
|
||||
public $filesize;
|
||||
|
||||
/** @var string */
|
||||
public $filename;
|
||||
|
||||
/** @var string */
|
||||
public $ext;
|
||||
|
||||
/** @var string[]|null */
|
||||
public ?array $tag_array;
|
||||
public int $owner_id;
|
||||
public string $owner_ip;
|
||||
public string $posted;
|
||||
public ?string $source;
|
||||
public bool $locked = false;
|
||||
public ?bool $lossless = null;
|
||||
public ?bool $video = null;
|
||||
public ?string $video_codec = null;
|
||||
public ?bool $image = null;
|
||||
public ?bool $audio = null;
|
||||
public ?int $length = null;
|
||||
public $tag_array;
|
||||
|
||||
public static array $bool_props = ["locked", "lossless", "video", "audio", "image"];
|
||||
public static array $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
|
||||
/** @var int */
|
||||
public $owner_id;
|
||||
|
||||
/** @var string */
|
||||
public $owner_ip;
|
||||
|
||||
/** @var string */
|
||||
public $posted;
|
||||
|
||||
/** @var string */
|
||||
public $source;
|
||||
|
||||
/** @var boolean */
|
||||
public $locked = false;
|
||||
|
||||
/** @var boolean */
|
||||
public $lossless = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $video = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $image = null;
|
||||
|
||||
/** @var boolean */
|
||||
public $audio = null;
|
||||
|
||||
/** @var int */
|
||||
public $length = null;
|
||||
|
||||
public static $bool_props = ["locked", "lossless", "video", "audio"];
|
||||
public static $int_props = ["id", "owner_id", "height", "width", "filesize", "length"];
|
||||
|
||||
/**
|
||||
* One will very rarely construct an image directly, more common
|
||||
@ -47,10 +80,6 @@ class Image
|
||||
{
|
||||
if (!is_null($row)) {
|
||||
foreach ($row as $name => $value) {
|
||||
if (is_numeric($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// some databases use table.name rather than name
|
||||
$name = str_replace("images.", "", $name);
|
||||
|
||||
@ -108,7 +137,7 @@ class Image
|
||||
|
||||
private static function find_images_internal(int $start = 0, ?int $limit = null, array $tags=[]): iterable
|
||||
{
|
||||
global $database, $user;
|
||||
global $database, $user, $config;
|
||||
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
@ -123,8 +152,13 @@ class Image
|
||||
}
|
||||
}
|
||||
|
||||
$querylet = Image::build_search_querylet($tags, $limit, $start);
|
||||
return $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
$order = (Image::$order_sql ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
$querylet = Image::build_search_querylet($tags, $order, $limit, $start);
|
||||
$result = $database->get_all_iterable($querylet->sql, $querylet->variables);
|
||||
|
||||
Image::$order_sql = null;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,6 +213,27 @@ class Image
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts consecutive days of image uploads
|
||||
*/
|
||||
public static function count_upload_streak(): int
|
||||
{
|
||||
$now = date_create();
|
||||
$last_date = $now;
|
||||
foreach (self::find_images_iterable() as $img) {
|
||||
$next_date = date_create($img->posted);
|
||||
if (date_diff($next_date, $last_date)->days > 0) {
|
||||
break;
|
||||
}
|
||||
$last_date = $next_date;
|
||||
}
|
||||
if ($last_date === $now) {
|
||||
return 0;
|
||||
}
|
||||
$diff_d = ($now->getTimestamp() - $last_date->getTimestamp()) / 86400;
|
||||
return (int)ceil($diff_d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of image results for a given search
|
||||
*
|
||||
@ -189,11 +244,11 @@ class Image
|
||||
global $cache, $database;
|
||||
$tag_count = count($tags);
|
||||
|
||||
if (SPEED_HAX && $tag_count === 0) {
|
||||
if ($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], "-")) {
|
||||
} elseif ($tag_count === 1 && !preg_match("/[:=><\*\?]/", $tags[0])) {
|
||||
if (!startsWith($tags[0], "-")) {
|
||||
// one tag - we can look that up directly
|
||||
$total = self::count_tag($tags[0]);
|
||||
} else {
|
||||
@ -239,19 +294,14 @@ class Image
|
||||
{
|
||||
$tag_conditions = [];
|
||||
$img_conditions = [];
|
||||
$stpen = 0; // search term parse event number
|
||||
$order = null;
|
||||
|
||||
/*
|
||||
* Turn a bunch of strings into a bunch of TagCondition
|
||||
* and ImgCondition objects
|
||||
*/
|
||||
/** @var $stpe SearchTermParseEvent */
|
||||
$stpe = send_event(new SearchTermParseEvent($stpen++, null, $terms));
|
||||
if ($stpe->order) {
|
||||
$order = $stpe->order;
|
||||
} elseif (!empty($stpe->querylets)) {
|
||||
foreach ($stpe->querylets as $querylet) {
|
||||
$stpe = send_event(new SearchTermParseEvent(null, $terms));
|
||||
if ($stpe->is_querylet_set()) {
|
||||
foreach ($stpe->get_querylets() as $querylet) {
|
||||
$img_conditions[] = new ImgCondition($querylet, true);
|
||||
}
|
||||
}
|
||||
@ -266,12 +316,9 @@ class Image
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var $stpe SearchTermParseEvent */
|
||||
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
|
||||
if ($stpe->order) {
|
||||
$order = $stpe->order;
|
||||
} elseif (!empty($stpe->querylets)) {
|
||||
foreach ($stpe->querylets as $querylet) {
|
||||
$stpe = send_event(new SearchTermParseEvent($term, $terms));
|
||||
if ($stpe->is_querylet_set()) {
|
||||
foreach ($stpe->get_querylets() as $querylet) {
|
||||
$img_conditions[] = new ImgCondition($querylet, $positive);
|
||||
}
|
||||
} else {
|
||||
@ -281,7 +328,7 @@ class Image
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$tag_conditions, $img_conditions, $order];
|
||||
return [$tag_conditions, $img_conditions];
|
||||
}
|
||||
|
||||
/*
|
||||
@ -318,9 +365,8 @@ class Image
|
||||
');
|
||||
} else {
|
||||
$tags[] = 'id'. $gtlt . $this->id;
|
||||
$tags[] = 'order:id_'. strtolower($dir);
|
||||
$querylet = Image::build_search_querylet($tags);
|
||||
$querylet->append_sql(' LIMIT 1');
|
||||
$querylet->append_sql(' ORDER BY images.id '.$dir.' LIMIT 1');
|
||||
$row = $database->get_row($querylet->sql, $querylet->variables);
|
||||
}
|
||||
|
||||
@ -357,7 +403,7 @@ class Image
|
||||
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}");
|
||||
log_info("core_image", "Owner for Image #{$this->id} set to {$owner->name}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,22 +417,22 @@ class Image
|
||||
"INSERT INTO images(
|
||||
owner_id, owner_ip,
|
||||
filename, filesize,
|
||||
hash, mime, ext,
|
||||
hash, ext,
|
||||
width, height,
|
||||
posted, source
|
||||
)
|
||||
VALUES (
|
||||
:owner_id, :owner_ip,
|
||||
:filename, :filesize,
|
||||
:hash, :mime, :ext,
|
||||
:hash, :ext,
|
||||
0, 0,
|
||||
now(), :source
|
||||
)",
|
||||
[
|
||||
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"filename" => $cut_name, "filesize" => $this->filesize,
|
||||
"hash" => $this->hash, "mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext), "source" => $this->source
|
||||
"hash" => $this->hash, "ext" => strtolower($this->ext),
|
||||
"source" => $this->source
|
||||
]
|
||||
);
|
||||
$this->id = $database->get_last_insert_id('images_id_seq');
|
||||
@ -394,13 +440,12 @@ class Image
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"filename = :filename, filesize = :filesize, hash = :hash, ".
|
||||
"mime = :mime, ext = :ext, width = 0, height = 0, source = :source ".
|
||||
"ext = :ext, width = 0, height = 0, source = :source ".
|
||||
"WHERE id = :id",
|
||||
[
|
||||
"filename" => $cut_name,
|
||||
"filesize" => $this->filesize,
|
||||
"hash" => $this->hash,
|
||||
"mime" => strtolower($this->mime),
|
||||
"ext" => strtolower($this->ext),
|
||||
"source" => $this->source,
|
||||
"id" => $this->id,
|
||||
@ -411,18 +456,17 @@ class Image
|
||||
$database->execute(
|
||||
"UPDATE images SET ".
|
||||
"lossless = :lossless, ".
|
||||
"video = :video, video_codec = :video_codec, audio = :audio,image = :image, ".
|
||||
"video = :video, 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,
|
||||
"lossless" => $database->scoresql_value_prepare($this->lossless),
|
||||
"video" => $database->scoresql_value_prepare($this->video),
|
||||
"image" => $database->scoresql_value_prepare($this->image),
|
||||
"audio" => $database->scoresql_value_prepare($this->audio),
|
||||
"length" => $this->length
|
||||
]
|
||||
);
|
||||
@ -444,7 +488,6 @@ class Image
|
||||
WHERE image_id=:id
|
||||
ORDER BY tag
|
||||
", ["id"=>$this->id]);
|
||||
sort($this->tag_array);
|
||||
}
|
||||
return $this->tag_array;
|
||||
}
|
||||
@ -481,8 +524,7 @@ class Image
|
||||
public function get_thumb_link(): string
|
||||
{
|
||||
global $config;
|
||||
$mime = $config->get_string(ImageConfig::THUMB_MIME);
|
||||
$ext = FileExtension::get_for_mime($mime);
|
||||
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
|
||||
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
|
||||
}
|
||||
|
||||
@ -496,7 +538,7 @@ class Image
|
||||
$image_link = $config->get_string($template);
|
||||
|
||||
if (!empty($image_link)) {
|
||||
if (!str_contains($image_link, "://") && !str_starts_with($image_link, "/")) {
|
||||
if (!(strpos($image_link, "://") > 0) && !startsWith($image_link, "/")) {
|
||||
$image_link = make_link($image_link);
|
||||
}
|
||||
$chosen = $image_link;
|
||||
@ -520,19 +562,6 @@ class Image
|
||||
return $plte->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the info for this image, formatted according to the
|
||||
* configured template.
|
||||
*/
|
||||
public function get_info(): string
|
||||
{
|
||||
global $config;
|
||||
$plte = new ParseLinkTemplateEvent($config->get_string(ImageConfig::INFO), $this);
|
||||
send_event($plte);
|
||||
return $plte->text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Figure out where the full size image is on disk.
|
||||
*/
|
||||
@ -558,40 +587,21 @@ class Image
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's extension.
|
||||
* Get the image's mime type.
|
||||
*/
|
||||
public function get_mime_type(): string
|
||||
{
|
||||
return get_mime($this->get_image_filename(), $this->get_ext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's filename extension
|
||||
*/
|
||||
public function get_ext(): string
|
||||
{
|
||||
return $this->ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image's mime type.
|
||||
*/
|
||||
public function get_mime(): ?string
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the image's mime type.
|
||||
*/
|
||||
public function set_mime($mime): void
|
||||
{
|
||||
$this->mime = $mime;
|
||||
$ext = FileExtension::get_for_mime($this->get_mime());
|
||||
assert($ext != null);
|
||||
$this->ext = $ext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the image's source URL
|
||||
*/
|
||||
@ -612,7 +622,7 @@ class Image
|
||||
}
|
||||
if ($new_source != $old_source) {
|
||||
$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)");
|
||||
log_info("core_image", "Source for Image #{$this->id} set to: $new_source (was $old_source)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,12 +634,16 @@ class Image
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
public function set_locked(bool $locked): void
|
||||
public function set_locked(bool $tf): void
|
||||
{
|
||||
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");
|
||||
$ln = $tf ? "Y" : "N";
|
||||
$sln = $database->scoreql_to_sql('SCORE_BOOL_'.$ln);
|
||||
$sln = str_replace("'", "", $sln);
|
||||
$sln = str_replace('"', "", $sln);
|
||||
if (bool_escape($sln) !== $this->locked) {
|
||||
$database->execute("UPDATE images SET locked=:yn WHERE id=:id", ["yn"=>$sln, "id"=>$this->id]);
|
||||
log_info("core_image", "Setting Image #{$this->id} lock to: $ln");
|
||||
}
|
||||
}
|
||||
|
||||
@ -684,7 +698,7 @@ class Image
|
||||
$page->flash("Can't set a tag longer than 255 characters");
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($tag, "-")) {
|
||||
if (startsWith($tag, "-")) {
|
||||
$page->flash("Can't set a tag which starts with a minus");
|
||||
continue;
|
||||
}
|
||||
@ -746,7 +760,7 @@ class Image
|
||||
);
|
||||
}
|
||||
|
||||
log_info("core_image", "Tags for Post #{$this->id} set to: ".Tag::implode($tags));
|
||||
log_info("core_image", "Tags for Image #{$this->id} set to: ".Tag::implode($tags));
|
||||
$cache->delete("image-{$this->id}-tags");
|
||||
}
|
||||
}
|
||||
@ -759,7 +773,7 @@ class Image
|
||||
global $database;
|
||||
$this->delete_tags_from_image();
|
||||
$database->execute("DELETE FROM images WHERE id=:id", ["id"=>$this->id]);
|
||||
log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')');
|
||||
log_info("core_image", 'Deleted Image #'.$this->id.' ('.$this->hash.')');
|
||||
|
||||
unlink($this->get_image_filename());
|
||||
unlink($this->get_thumb_filename());
|
||||
@ -771,7 +785,7 @@ class Image
|
||||
*/
|
||||
public function remove_image_only(): void
|
||||
{
|
||||
log_info("core_image", 'Removed Post File ('.$this->hash.')');
|
||||
log_info("core_image", 'Removed Image File ('.$this->hash.')');
|
||||
@unlink($this->get_image_filename());
|
||||
@unlink($this->get_thumb_filename());
|
||||
}
|
||||
@ -797,14 +811,12 @@ class Image
|
||||
* #param string[] $terms
|
||||
*/
|
||||
private static function build_search_querylet(
|
||||
array $terms,
|
||||
array $tags,
|
||||
?string $order=null,
|
||||
?int $limit=null,
|
||||
?int $offset=null
|
||||
): Querylet {
|
||||
global $config;
|
||||
|
||||
list($tag_conditions, $img_conditions, $order) = self::terms_to_conditions($terms);
|
||||
$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));
|
||||
list($tag_conditions, $img_conditions) = self::terms_to_conditions($tags);
|
||||
|
||||
$positive_tag_count = 0;
|
||||
$negative_tag_count = 0;
|
||||
|
@ -33,6 +33,11 @@ function add_dir(string $base): array
|
||||
|
||||
/**
|
||||
* Sends a DataUploadEvent for a file.
|
||||
*
|
||||
* @param string $tmpname
|
||||
* @param string $filename
|
||||
* @param string $tags
|
||||
* @throws UploadException
|
||||
*/
|
||||
function add_image(string $tmpname, string $filename, string $tags): int
|
||||
{
|
||||
@ -70,12 +75,7 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
|
||||
|
||||
$fit = $config->get_string(ImageConfig::THUMB_FIT);
|
||||
|
||||
if (in_array($fit, [
|
||||
Media::RESIZE_TYPE_FILL,
|
||||
Media::RESIZE_TYPE_STRETCH,
|
||||
Media::RESIZE_TYPE_FIT_BLUR,
|
||||
Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT
|
||||
])) {
|
||||
if (in_array($fit, [Media::RESIZE_TYPE_FILL, Media::RESIZE_TYPE_STRETCH, Media::RESIZE_TYPE_FIT_BLUR])) {
|
||||
return [$config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_HEIGHT)];
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ function get_thumbnail_max_size_scaled(): array
|
||||
}
|
||||
|
||||
|
||||
function create_image_thumb(string $hash, string $mime, string $engine = null)
|
||||
function create_image_thumb(string $hash, string $type, string $engine = null)
|
||||
{
|
||||
global $config;
|
||||
|
||||
@ -147,7 +147,7 @@ function create_image_thumb(string $hash, string $mime, string $engine = null)
|
||||
$inname,
|
||||
$outname,
|
||||
$tsize,
|
||||
$mime,
|
||||
$type,
|
||||
$engine,
|
||||
$config->get_string(ImageConfig::THUMB_FIT)
|
||||
);
|
||||
@ -155,7 +155,7 @@ function create_image_thumb(string $hash, string $mime, string $engine = null)
|
||||
|
||||
|
||||
|
||||
function create_scaled_image(string $inname, string $outname, array $tsize, string $mime, ?string $engine = null, ?string $resize_type = null)
|
||||
function create_scaled_image(string $inname, string $outname, array $tsize, string $type, ?string $engine = null, ?string $resize_type = null)
|
||||
{
|
||||
global $config;
|
||||
if (empty($engine)) {
|
||||
@ -165,44 +165,22 @@ function create_scaled_image(string $inname, string $outname, array $tsize, stri
|
||||
$resize_type = $config->get_string(ImageConfig::THUMB_FIT);
|
||||
}
|
||||
|
||||
$output_mime = $config->get_string(ImageConfig::THUMB_MIME);
|
||||
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
|
||||
if ($output_format==EXTENSION_WEBP) {
|
||||
$output_format = Media::WEBP_LOSSY;
|
||||
}
|
||||
|
||||
send_event(new MediaResizeEvent(
|
||||
$engine,
|
||||
$inname,
|
||||
$mime,
|
||||
$type,
|
||||
$outname,
|
||||
$tsize[0],
|
||||
$tsize[1],
|
||||
$resize_type,
|
||||
$output_mime,
|
||||
$config->get_string(ImageConfig::THUMB_ALPHA_COLOR),
|
||||
$output_format,
|
||||
$config->get_int(ImageConfig::THUMB_QUALITY),
|
||||
true,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
function redirect_to_next_image(Image $image): void
|
||||
{
|
||||
global $page;
|
||||
|
||||
if (isset($_GET['search'])) {
|
||||
$search_terms = Tag::explode(Tag::decaret($_GET['search']));
|
||||
$query = "search=" . url_escape($_GET['search']);
|
||||
} else {
|
||||
$search_terms = [];
|
||||
$query = null;
|
||||
}
|
||||
|
||||
$target_image = $image->get_next($search_terms);
|
||||
|
||||
if ($target_image == null) {
|
||||
$redirect_target = referer_or(make_link("post/list"), ['post/view']);
|
||||
} else {
|
||||
$redirect_target = make_link("post/view/{$target_image->id}", null, $query);
|
||||
}
|
||||
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect($redirect_target);
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php declare(strict_types=1);
|
||||
class Querylet
|
||||
{
|
||||
public string $sql;
|
||||
public array $variables;
|
||||
/** @var string */
|
||||
public $sql;
|
||||
/** @var array */
|
||||
public $variables;
|
||||
|
||||
public function __construct(string $sql, array $variables=[])
|
||||
{
|
||||
@ -29,8 +31,10 @@ class Querylet
|
||||
|
||||
class TagCondition
|
||||
{
|
||||
public string $tag;
|
||||
public bool $positive;
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(string $tag, bool $positive)
|
||||
{
|
||||
@ -41,8 +45,10 @@ class TagCondition
|
||||
|
||||
class ImgCondition
|
||||
{
|
||||
public Querylet $qlet;
|
||||
public bool $positive;
|
||||
/** @var Querylet */
|
||||
public $qlet;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(Querylet $qlet, bool $positive)
|
||||
{
|
||||
|
@ -49,6 +49,7 @@ function get_dsn()
|
||||
{
|
||||
if (getenv("INSTALL_DSN")) {
|
||||
$dsn = getenv("INSTALL_DSN");
|
||||
;
|
||||
} elseif (@$_POST["database_type"] == DatabaseDriver::SQLITE) {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$id = bin2hex(random_bytes(5));
|
||||
@ -68,7 +69,7 @@ function do_install($dsn)
|
||||
create_tables(new Database($dsn));
|
||||
write_config($dsn);
|
||||
} catch (InstallerException $e) {
|
||||
die_nicely($e->title, $e->body, $e->exit_code);
|
||||
die_nicely($e->title, $e->body, $e->code);
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +256,7 @@ function create_tables(Database $db)
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL,
|
||||
posted TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
locked SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N,
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
|
||||
");
|
||||
$db->execute("CREATE INDEX images_owner_id_idx ON images(owner_id)", []);
|
||||
@ -281,19 +282,13 @@ function create_tables(Database $db)
|
||||
$db->execute("CREATE INDEX images_tags_tag_id_idx ON image_tags(tag_id)", []);
|
||||
|
||||
$db->execute("INSERT INTO config(name, value) VALUES('db_version', 11)");
|
||||
|
||||
// mysql auto-commits when creating a table, so the transaction
|
||||
// is closed; other databases need to commit
|
||||
if ($db->is_transaction_open()) {
|
||||
$db->commit();
|
||||
}
|
||||
$db->commit();
|
||||
} catch (PDOException $e) {
|
||||
throw new InstallerException(
|
||||
"PDO Error:",
|
||||
"<p>An error occurred while trying to create the database tables necessary for Shimmie.</p>
|
||||
<p>Please check and ensure that the database configuration options are all correct.</p>
|
||||
<p>{$e->getMessage()}</p>
|
||||
",
|
||||
<p>{$e->getMessage()}</p>",
|
||||
3
|
||||
);
|
||||
}
|
||||
|
@ -7,9 +7,6 @@ abstract class Permissions
|
||||
{
|
||||
public const CHANGE_SETTING = "change_setting"; # modify web-level settings, eg the config table
|
||||
public const OVERRIDE_CONFIG = "override_config"; # modify sys-level settings, eg shimmie.conf.php
|
||||
public const CHANGE_USER_SETTING = "change_user_setting"; # modify own user-level settings
|
||||
public const CHANGE_OTHER_USER_SETTING = "change_other_user_setting"; # modify own user-level settings
|
||||
|
||||
public const BIG_SEARCH = "big_search"; # search for more than 3 tags at once (speed mode only)
|
||||
|
||||
public const MANAGE_EXTENSION_LIST = "manage_extension_list";
|
||||
@ -103,7 +100,6 @@ abstract class Permissions
|
||||
public const SET_PRIVATE_IMAGE = "set_private_image";
|
||||
public const SET_OTHERS_PRIVATE_IMAGES = "set_others_private_images";
|
||||
|
||||
public const CRON_RUN = "cron_run";
|
||||
public const BULK_IMPORT = "bulk_import";
|
||||
public const BULK_EXPORT = "bulk_export";
|
||||
public const BULK_DOWNLOAD = "bulk_download";
|
||||
|
@ -3,6 +3,9 @@
|
||||
* Things which should be in the core API *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
require_once "filetypes.php";
|
||||
|
||||
|
||||
/**
|
||||
* Return the unique elements of an array, case insensitively
|
||||
*/
|
||||
@ -124,16 +127,10 @@ function list_files(string $base, string $_sub_dir=""): array
|
||||
|
||||
$files = [];
|
||||
$dir = opendir("$base/$_sub_dir");
|
||||
if ($dir===false) {
|
||||
throw new SCoreException("Unable to open directory $base/$_sub_dir");
|
||||
}
|
||||
try {
|
||||
while ($f = readdir($dir)) {
|
||||
$files[] = $f;
|
||||
}
|
||||
} finally {
|
||||
closedir($dir);
|
||||
while ($f = readdir($dir)) {
|
||||
$files[] = $f;
|
||||
}
|
||||
closedir($dir);
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $filename) {
|
||||
@ -190,8 +187,8 @@ 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')) {
|
||||
if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
|
||||
|
||||
/**
|
||||
* #return string[]
|
||||
*/
|
||||
@ -222,7 +219,7 @@ 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.
|
||||
*/
|
||||
function find_header(array $headers, string $name): ?string
|
||||
function findHeader(array $headers, string $name): ?string
|
||||
{
|
||||
if (!is_array($headers)) {
|
||||
return null;
|
||||
@ -246,22 +243,21 @@ function find_header(array $headers, string $name): ?string
|
||||
|
||||
if (!function_exists('mb_strlen')) {
|
||||
// TODO: we should warn the admin that they are missing multibyte support
|
||||
/** @noinspection PhpUnusedParameterInspection */
|
||||
function mb_strlen($str, $encoding): int
|
||||
function mb_strlen($str, $encoding)
|
||||
{
|
||||
return strlen($str);
|
||||
}
|
||||
function mb_internal_encoding($encoding): void
|
||||
function mb_internal_encoding($encoding)
|
||||
{
|
||||
}
|
||||
function mb_strtolower($str): string
|
||||
function mb_strtolower($str)
|
||||
{
|
||||
return strtolower($str);
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
function get_subclasses_of(string $parent): array
|
||||
function getSubclassesOf(string $parent)
|
||||
{
|
||||
$result = [];
|
||||
foreach (get_declared_classes() as $class) {
|
||||
@ -328,7 +324,7 @@ function get_base_href(): string
|
||||
/**
|
||||
* The opposite of the standard library's parse_url
|
||||
*/
|
||||
function unparse_url(array $parsed_url): string
|
||||
function unparse_url($parsed_url)
|
||||
{
|
||||
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
|
||||
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
|
||||
@ -342,26 +338,17 @@ function unparse_url(array $parsed_url): string
|
||||
return "$scheme$user$pass$host$port$path$query$fragment";
|
||||
}
|
||||
|
||||
# finally in the core library starting from php8
|
||||
if (!function_exists('str_starts_with')) {
|
||||
function str_starts_with(string $haystack, string $needle): bool
|
||||
{
|
||||
return strncmp($haystack, $needle, strlen($needle)) === 0;
|
||||
}
|
||||
function startsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
return (substr($haystack, 0, $length) === $needle);
|
||||
}
|
||||
|
||||
if (!function_exists('str_ends_with')) {
|
||||
function str_ends_with(string $haystack, string $needle): bool
|
||||
{
|
||||
return $needle === '' || $needle === substr($haystack, - strlen($needle));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('str_contains')) {
|
||||
function str_contains(string $haystack, string $needle): bool
|
||||
{
|
||||
return '' === $needle || false !== strpos($haystack, $needle);
|
||||
}
|
||||
function endsWith(string $haystack, string $needle): bool
|
||||
{
|
||||
$length = strlen($needle);
|
||||
$start = $length * -1; //negative
|
||||
return (substr($haystack, $start) === $needle);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
@ -488,6 +475,25 @@ function clamp(?int $val, ?int $min=null, ?int $max=null): int
|
||||
return $val;
|
||||
}
|
||||
|
||||
function xml_tag(string $name, array $attrs=[], array $children=[]): string
|
||||
{
|
||||
$xml = "<$name ";
|
||||
foreach ($attrs as $k => $v) {
|
||||
$xv = str_replace(''', ''', htmlspecialchars((string)$v, ENT_QUOTES));
|
||||
$xml .= "$k=\"$xv\" ";
|
||||
}
|
||||
if (count($children) > 0) {
|
||||
$xml .= ">\n";
|
||||
foreach ($children as $child) {
|
||||
$xml .= xml_tag($child);
|
||||
}
|
||||
$xml .= "</$name>\n";
|
||||
} else {
|
||||
$xml .= "/>\n";
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original PHP code by Chirp Internet: www.chirp.com.au
|
||||
* Please acknowledge use of this code by including this header.
|
||||
@ -521,9 +527,13 @@ function parse_shorthand_int(string $limit): int
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 't': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
// no break
|
||||
case 'g': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
// no break
|
||||
case 'm': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
// no break
|
||||
case 'k': $value *= 1024; break;
|
||||
default: $value = -1;
|
||||
}
|
||||
@ -553,41 +563,17 @@ function to_shorthand_int(int $int): string
|
||||
return (string)$int;
|
||||
}
|
||||
}
|
||||
abstract class TIME_UNITS
|
||||
{
|
||||
public const MILLISECONDS = "ms";
|
||||
public const SECONDS = "s";
|
||||
public const MINUTES = "m";
|
||||
public const HOURS = "h";
|
||||
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
|
||||
];
|
||||
}
|
||||
function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS): string
|
||||
|
||||
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
|
||||
function format_milliseconds(int $input): string
|
||||
{
|
||||
$output = "";
|
||||
|
||||
$remainder = $input;
|
||||
$remainder = floor($input / 1000);
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
foreach (TIME_UNITS as $unit=>$conversion) {
|
||||
$count = $remainder % $conversion;
|
||||
$remainder = floor($remainder / $conversion);
|
||||
|
||||
if ($found||$unit==$min_unit) {
|
||||
$found = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($count==0&&$remainder<1) {
|
||||
break;
|
||||
}
|
||||
@ -596,32 +582,6 @@ function format_milliseconds(int $input, string $min_unit = TIME_UNITS::SECONDS)
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
function parse_to_milliseconds(string $input): int
|
||||
{
|
||||
$output = 0;
|
||||
$current_multiplier = 1;
|
||||
|
||||
if (preg_match('/^([0-9]+)$/i', $input, $match)) {
|
||||
// If just a number, then we treat it as milliseconds
|
||||
$length = $match[0];
|
||||
if (is_numeric($length)) {
|
||||
$length = floatval($length);
|
||||
$output += $length;
|
||||
}
|
||||
} else {
|
||||
foreach (TIME_UNITS::CONVERSION as $unit=>$conversion) {
|
||||
if (preg_match('/([0-9]+)'.$unit.'/i', $input, $match)) {
|
||||
$length = $match[1];
|
||||
if (is_numeric($length)) {
|
||||
$length = floatval($length);
|
||||
$output += $length * $current_multiplier;
|
||||
}
|
||||
}
|
||||
$current_multiplier *= $conversion;
|
||||
}
|
||||
}
|
||||
return intval($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a date into a time, a date, an "X minutes ago...", etc
|
||||
@ -797,7 +757,7 @@ function iterator_map_to_array(callable $callback, iterator $iter): array
|
||||
return iterator_to_array(iterator_map($callback, $iter));
|
||||
}
|
||||
|
||||
function stringer($s): string
|
||||
function stringer($s)
|
||||
{
|
||||
if (is_array($s)) {
|
||||
if (isset($s[0])) {
|
||||
|
@ -4,6 +4,39 @@
|
||||
* be included right at the very start of index.php and tests/bootstrap.php
|
||||
*/
|
||||
|
||||
$min_php = "7.3";
|
||||
if (version_compare(phpversion(), $min_php, ">=") === false) {
|
||||
print "
|
||||
Shimmie does not support versions of PHP lower than $min_php
|
||||
(PHP reports that it is version ".phpversion().").
|
||||
If your web host is running an older version, they are dangerously out of
|
||||
date and you should plan on moving elsewhere.
|
||||
";
|
||||
exit;
|
||||
}
|
||||
|
||||
# ini_set('zend.assertions', '1'); // generate assertions
|
||||
ini_set('assert.exception', '1'); // throw exceptions when failed
|
||||
set_error_handler(function ($errNo, $errStr) {
|
||||
// Should we turn ALL notices into errors? PHP allows a lot of
|
||||
// terrible things to happen by default...
|
||||
if (strpos($errStr, 'Use of undefined constant ') === 0) {
|
||||
throw new Exception("PHP Error#$errNo: $errStr");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ob_start();
|
||||
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
die("CLI with remote addr? Confused, not taking the risk.");
|
||||
}
|
||||
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
|
||||
$_SERVER['HTTP_HOST'] = "<cli command>";
|
||||
}
|
||||
|
||||
function die_nicely($title, $body, $code=0)
|
||||
{
|
||||
print("<!DOCTYPE html>
|
||||
@ -28,33 +61,3 @@ function die_nicely($title, $body, $code=0)
|
||||
}
|
||||
exit($code);
|
||||
}
|
||||
|
||||
$min_php = "7.3";
|
||||
if (version_compare(phpversion(), $min_php, ">=") === false) {
|
||||
die_nicely("Not Supported", "
|
||||
Shimmie does not support versions of PHP lower than $min_php
|
||||
(PHP reports that it is version ".phpversion().").
|
||||
", 1);
|
||||
}
|
||||
|
||||
# ini_set('zend.assertions', '1'); // generate assertions
|
||||
ini_set('assert.exception', '1'); // throw exceptions when failed
|
||||
set_error_handler(function ($errNo, $errStr) {
|
||||
// Should we turn ALL notices into errors? PHP allows a lot of
|
||||
// terrible things to happen by default...
|
||||
if (str_starts_with($errStr, 'Use of undefined constant ')) {
|
||||
throw new Exception("PHP Error#$errNo: $errStr");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ob_start();
|
||||
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI == 'phpdbg') {
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
die("CLI with remote addr? Confused, not taking the risk.");
|
||||
}
|
||||
$_SERVER['REMOTE_ADDR'] = "0.0.0.0";
|
||||
$_SERVER['HTTP_HOST'] = "cli-command";
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ function _set_event_listeners(): void
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = [];
|
||||
|
||||
foreach (get_subclasses_of("Extension") as $class) {
|
||||
foreach (getSubclassesOf("Extension") as $class) {
|
||||
/** @var Extension $extension */
|
||||
$extension = new $class();
|
||||
|
||||
@ -61,7 +61,7 @@ function _dump_event_listeners(array $event_listeners, string $path): void
|
||||
{
|
||||
$p = "<"."?php\n";
|
||||
|
||||
foreach (get_subclasses_of("Extension") as $class) {
|
||||
foreach (getSubclassesOf("Extension") as $class) {
|
||||
$p .= "\$$class = new $class(); ";
|
||||
}
|
||||
|
||||
|
@ -19,16 +19,16 @@ function _d(string $name, $value): void
|
||||
}
|
||||
}
|
||||
$_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
|
||||
_d("DEBUG", false); // boolean print various debugging details
|
||||
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
|
||||
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
|
||||
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
|
||||
_d("VERSION", "2.8.4$_g"); // string shimmie version
|
||||
_d("TIMEZONE", null); // string timezone
|
||||
_d("EXTRA_EXTS", ""); // string optional extra extensions
|
||||
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
|
||||
_d("TRACE_FILE", null); // string file to log performance data into
|
||||
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
|
||||
_d("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
|
||||
_d("DEBUG", false); // boolean print various debugging details
|
||||
_d("COOKIE_PREFIX", 'shm'); // string if you run multiple galleries with non-shared logins, give them different prefixes
|
||||
_d("SPEED_HAX", false); // boolean do some questionable things in the name of performance
|
||||
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
|
||||
_d("VERSION", "2.8.4$_g"); // string shimmie version
|
||||
_d("TIMEZONE", null); // string timezone
|
||||
_d("EXTRA_EXTS", ""); // string optional extra extensions
|
||||
_d("BASE_HREF", null); // string force a specific base URL (default is auto-detect)
|
||||
_d("TRACE_FILE", null); // string file to log performance data into
|
||||
_d("TRACE_THRESHOLD", 0.0); // float log pages which take more time than this many seconds
|
||||
|
@ -9,13 +9,13 @@ class PolyfillsTest extends TestCase
|
||||
public function test_html_escape()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"Foo & <main>",
|
||||
html_escape("Foo & <main>")
|
||||
html_escape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
"Foo & <main>",
|
||||
html_unescape("Foo & <main>")
|
||||
html_unescape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$x = "Foo & <waffles>";
|
||||
@ -24,17 +24,17 @@ class PolyfillsTest extends TestCase
|
||||
|
||||
public function test_int_escape()
|
||||
{
|
||||
$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));
|
||||
$this->assertEquals(int_escape(""), 0);
|
||||
$this->assertEquals(int_escape("1"), 1);
|
||||
$this->assertEquals(int_escape("-1"), -1);
|
||||
$this->assertEquals(int_escape("-1.5"), -1);
|
||||
$this->assertEquals(int_escape(null), 0);
|
||||
}
|
||||
|
||||
public function test_url_escape()
|
||||
{
|
||||
$this->assertEquals("%5E%5Co%2F%5E", url_escape("^\o/^"));
|
||||
$this->assertEquals("", url_escape(null));
|
||||
$this->assertEquals(url_escape("^\o/^"), "%5E%5Co%2F%5E");
|
||||
$this->assertEquals(url_escape(null), "");
|
||||
}
|
||||
|
||||
public function test_bool_escape()
|
||||
@ -69,33 +69,41 @@ class PolyfillsTest extends TestCase
|
||||
|
||||
public function test_clamp()
|
||||
{
|
||||
$this->assertEquals(5, clamp(0, 5, 10));
|
||||
$this->assertEquals(5, clamp(5, 5, 10));
|
||||
$this->assertEquals(7, clamp(7, 5, 10));
|
||||
$this->assertEquals(10, clamp(10, 5, 10));
|
||||
$this->assertEquals(10, clamp(15, 5, 10));
|
||||
$this->assertEquals(clamp(0, 5, 10), 5);
|
||||
$this->assertEquals(clamp(5, 5, 10), 5);
|
||||
$this->assertEquals(clamp(7, 5, 10), 7);
|
||||
$this->assertEquals(clamp(10, 5, 10), 10);
|
||||
$this->assertEquals(clamp(15, 5, 10), 10);
|
||||
}
|
||||
|
||||
public function test_xml_tag()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<test foo=\"bar\" >\n<cake />\n</test>\n",
|
||||
xml_tag("test", ["foo"=>"bar"], ["cake"])
|
||||
);
|
||||
}
|
||||
|
||||
public function test_truncate()
|
||||
{
|
||||
$this->assertEquals("test words", truncate("test words", 10));
|
||||
$this->assertEquals("test...", truncate("test...", 9));
|
||||
$this->assertEquals("test...", truncate("test...", 6));
|
||||
$this->assertEquals("te...", truncate("te...", 2));
|
||||
$this->assertEquals(truncate("test words", 10), "test words");
|
||||
$this->assertEquals(truncate("test...", 9), "test...");
|
||||
$this->assertEquals(truncate("test...", 6), "test...");
|
||||
$this->assertEquals(truncate("te...", 2), "te...");
|
||||
}
|
||||
|
||||
public function test_to_shorthand_int()
|
||||
{
|
||||
$this->assertEquals("1.1GB", to_shorthand_int(1231231231));
|
||||
$this->assertEquals("2", to_shorthand_int(2));
|
||||
$this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
|
||||
$this->assertEquals(to_shorthand_int(2), "2");
|
||||
}
|
||||
|
||||
public function test_parse_shorthand_int()
|
||||
{
|
||||
$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"));
|
||||
$this->assertEquals(parse_shorthand_int("foo"), -1);
|
||||
$this->assertEquals(parse_shorthand_int("32M"), 33554432);
|
||||
$this->assertEquals(parse_shorthand_int("43.4KB"), 44441);
|
||||
$this->assertEquals(parse_shorthand_int("1231231231"), 1231231231);
|
||||
}
|
||||
|
||||
public function test_format_milliseconds()
|
||||
@ -105,13 +113,6 @@ class PolyfillsTest extends TestCase
|
||||
$this->assertEquals("1y 213d 16h 53m 20s", format_milliseconds(50000000000));
|
||||
}
|
||||
|
||||
public function test_parse_to_milliseconds()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
$this->assertEquals(
|
||||
|
@ -43,13 +43,13 @@ class UrlsTest extends TestCase
|
||||
{
|
||||
// relative to shimmie install
|
||||
$this->assertEquals(
|
||||
"http://cli-command/test/foo",
|
||||
"http://<cli command>/test/foo",
|
||||
make_http("foo")
|
||||
);
|
||||
|
||||
// relative to web server
|
||||
$this->assertEquals(
|
||||
"http://cli-command/foo",
|
||||
"http://<cli command>/foo",
|
||||
make_http("/foo")
|
||||
);
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
class Link
|
||||
{
|
||||
public ?string $page;
|
||||
public ?string $query;
|
||||
public $page;
|
||||
public $query;
|
||||
|
||||
public function __construct(?string $page=null, ?string $query=null)
|
||||
{
|
||||
@ -79,7 +79,7 @@ function modify_url(string $url, array $changes): string
|
||||
*/
|
||||
function make_http(string $link): string
|
||||
{
|
||||
if (str_contains($link, "://")) {
|
||||
if (strpos($link, "://") > 0) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ function referer_or(string $dest, ?array $blacklist=null): string
|
||||
}
|
||||
if ($blacklist) {
|
||||
foreach ($blacklist as $b) {
|
||||
if (str_contains($_SERVER['HTTP_REFERER'], $b)) {
|
||||
if (strstr($_SERVER['HTTP_REFERER'], $b)) {
|
||||
return $dest;
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,22 @@ function _new_user(array $row): User
|
||||
*/
|
||||
class User
|
||||
{
|
||||
public int $id;
|
||||
public string $name;
|
||||
public ?string $email;
|
||||
public string $join_date;
|
||||
public ?string $passhash;
|
||||
public UserClass $class;
|
||||
/** @var int */
|
||||
public $id;
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
public $email;
|
||||
|
||||
public $join_date;
|
||||
|
||||
/** @var string */
|
||||
public $passhash;
|
||||
|
||||
/** @var UserClass */
|
||||
public $class;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Initialisation *
|
||||
@ -108,7 +118,7 @@ class User
|
||||
$my_user = User::by_name($name);
|
||||
|
||||
// If user tried to log in as "foo bar" and failed, try "foo_bar"
|
||||
if (!$my_user && str_contains($name, " ")) {
|
||||
if (!$my_user && strpos($name, " ") !== false) {
|
||||
$my_user = User::by_name(str_replace(" ", "_", $name));
|
||||
}
|
||||
|
||||
@ -153,7 +163,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);
|
||||
}
|
||||
|
||||
@ -165,7 +175,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}");
|
||||
}
|
||||
|
||||
@ -175,7 +185,7 @@ class User
|
||||
$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]);
|
||||
$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");
|
||||
@ -185,7 +195,7 @@ class User
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,21 @@ $_shm_user_classes = [];
|
||||
*/
|
||||
class UserClass
|
||||
{
|
||||
public ?string $name = null;
|
||||
public ?UserClass $parent = null;
|
||||
public array $abilities = [];
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
public $name = null;
|
||||
|
||||
/**
|
||||
* @var ?UserClass
|
||||
*/
|
||||
public $parent = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $abilities = [];
|
||||
|
||||
public function __construct(string $name, string $parent = null, array $abilities = [])
|
||||
{
|
||||
@ -88,7 +100,6 @@ new UserClass("user", "base", [
|
||||
Permissions::READ_PM => true,
|
||||
Permissions::SET_PRIVATE_IMAGE => true,
|
||||
Permissions::BULK_DOWNLOAD => true,
|
||||
Permissions::CHANGE_USER_SETTING => true
|
||||
]);
|
||||
|
||||
new UserClass("hellbanned", "user", [
|
||||
@ -97,8 +108,6 @@ new UserClass("hellbanned", "user", [
|
||||
|
||||
new UserClass("admin", "base", [
|
||||
Permissions::CHANGE_SETTING => true,
|
||||
Permissions::CHANGE_USER_SETTING => true,
|
||||
Permissions::CHANGE_OTHER_USER_SETTING => true,
|
||||
Permissions::OVERRIDE_CONFIG => true,
|
||||
Permissions::BIG_SEARCH => true,
|
||||
|
||||
@ -191,8 +200,6 @@ new UserClass("admin", "base", [
|
||||
Permissions::APPROVE_IMAGE => true,
|
||||
Permissions::APPROVE_COMMENT => true,
|
||||
|
||||
Permissions::CRON_RUN =>true,
|
||||
|
||||
Permissions::BULK_IMPORT =>true,
|
||||
Permissions::BULK_EXPORT =>true,
|
||||
Permissions::BULK_DOWNLOAD => true,
|
||||
|
@ -12,7 +12,6 @@ use function MicroHTML\TFOOT;
|
||||
use function MicroHTML\TR;
|
||||
use function MicroHTML\TH;
|
||||
use function MicroHTML\TD;
|
||||
use MicroHTML\HTMLElement;
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc *
|
||||
@ -21,6 +20,13 @@ use MicroHTML\HTMLElement;
|
||||
const DATA_DIR = "data";
|
||||
|
||||
|
||||
function mtimefile(string $file): string
|
||||
{
|
||||
$data_href = get_base_href();
|
||||
$mtime = filemtime($file);
|
||||
return "$data_href/$file?$mtime";
|
||||
}
|
||||
|
||||
function get_theme(): string
|
||||
{
|
||||
global $config;
|
||||
@ -40,18 +46,18 @@ function contact_link(): ?string
|
||||
}
|
||||
|
||||
if (
|
||||
str_starts_with($text, "http:") ||
|
||||
str_starts_with($text, "https:") ||
|
||||
str_starts_with($text, "mailto:")
|
||||
startsWith($text, "http:") ||
|
||||
startsWith($text, "https:") ||
|
||||
startsWith($text, "mailto:")
|
||||
) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
if (str_contains($text, "@")) {
|
||||
if (strpos($text, "@")) {
|
||||
return "mailto:$text";
|
||||
}
|
||||
|
||||
if (str_contains($text, "/")) {
|
||||
if (strpos($text, "/")) {
|
||||
return "http://$text";
|
||||
}
|
||||
|
||||
@ -253,11 +259,11 @@ function load_balance_url(string $tmpl, string $hash, int $n=0): string
|
||||
return $tmpl;
|
||||
}
|
||||
|
||||
function fetch_url(string $url, string $mfile): ?array
|
||||
function transload(string $url, string $mfile): ?array
|
||||
{
|
||||
global $config;
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "curl" && function_exists("curl_init")) {
|
||||
if ($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
|
||||
$ch = curl_init($url);
|
||||
$fp = fopen($mfile, "w");
|
||||
|
||||
@ -270,7 +276,8 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
|
||||
$response = curl_exec($ch);
|
||||
if ($response === false) {
|
||||
return null;
|
||||
log_warning("core-util", "Failed to transload $url");
|
||||
throw new SCoreException("Failed to fetch $url");
|
||||
}
|
||||
|
||||
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
@ -284,7 +291,7 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
return $headers;
|
||||
}
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "wget") {
|
||||
if ($config->get_string("transload_engine") === "wget") {
|
||||
$s_url = escapeshellarg($url);
|
||||
$s_mfile = escapeshellarg($mfile);
|
||||
system("wget --no-check-certificate $s_url --output-document=$s_mfile");
|
||||
@ -292,14 +299,14 @@ function fetch_url(string $url, string $mfile): ?array
|
||||
return file_exists($mfile) ? ["ok"=>"true"] : null;
|
||||
}
|
||||
|
||||
if ($config->get_string(UploadConfig::TRANSLOAD_ENGINE) === "fopen") {
|
||||
if ($config->get_string("transload_engine") === "fopen") {
|
||||
$fp_in = @fopen($url, "r");
|
||||
$fp_out = fopen($mfile, "w");
|
||||
if (!$fp_in || !$fp_out) {
|
||||
return null;
|
||||
}
|
||||
$length = 0;
|
||||
while (!feof($fp_in) && $length <= $config->get_int(UploadConfig::SIZE)) {
|
||||
while (!feof($fp_in) && $length <= $config->get_int('upload_size')) {
|
||||
$data = fread($fp_in, 8192);
|
||||
$length += strlen($data);
|
||||
fwrite($fp_out, $data);
|
||||
@ -341,7 +348,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!=""&&strpos($tag, ":") === false) {
|
||||
// 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.
|
||||
@ -360,7 +367,7 @@ function path_to_tags(string $path): string
|
||||
}
|
||||
|
||||
|
||||
function join_url(string $base, string ...$paths): string
|
||||
function join_url(string $base, string ...$paths)
|
||||
{
|
||||
$output = $base;
|
||||
foreach ($paths as $path) {
|
||||
@ -411,7 +418,7 @@ function remove_empty_dirs(string $dir): bool
|
||||
}
|
||||
}
|
||||
if ($result===true) {
|
||||
$result = rmdir($dir);
|
||||
$result = $result && rmdir($dir);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@ -585,6 +592,7 @@ function _get_themelet_files(string $_theme): array
|
||||
|
||||
/**
|
||||
* Used to display fatal errors to the web user.
|
||||
* @noinspection PhpPossiblePolymorphicInvocationInspection
|
||||
*/
|
||||
function _fatal_error(Exception $e): void
|
||||
{
|
||||
@ -703,7 +711,7 @@ function make_form(string $target, string $method="POST", bool $multipart=false,
|
||||
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
|
||||
}
|
||||
|
||||
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit=""): HTMLElement
|
||||
function SHM_FORM(string $target, string $method="POST", bool $multipart=false, string $form_id="", string $onsubmit="")
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -728,19 +736,19 @@ function SHM_FORM(string $target, string $method="POST", bool $multipart=false,
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_SIMPLE_FORM($target, ...$children): HTMLElement
|
||||
function SHM_SIMPLE_FORM($target, ...$children)
|
||||
{
|
||||
$form = SHM_FORM($target);
|
||||
$form->appendChild(emptyHTML(...$children));
|
||||
return $form;
|
||||
}
|
||||
|
||||
function SHM_SUBMIT(string $text): HTMLElement
|
||||
function SHM_SUBMIT(string $text)
|
||||
{
|
||||
return INPUT(["type"=>"submit", "value"=>$text]);
|
||||
}
|
||||
|
||||
function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
|
||||
function SHM_COMMAND_EXAMPLE(string $ex, string $desc)
|
||||
{
|
||||
return DIV(
|
||||
["class"=>"command_example"],
|
||||
@ -749,7 +757,7 @@ function SHM_COMMAND_EXAMPLE(string $ex, string $desc): HTMLElement
|
||||
);
|
||||
}
|
||||
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot): HTMLElement
|
||||
function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
|
||||
{
|
||||
if (is_string($foot)) {
|
||||
$foot = TFOOT(TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>$foot]))));
|
||||
@ -769,16 +777,16 @@ function SHM_USER_FORM(User $duser, string $target, string $title, $body, $foot)
|
||||
}
|
||||
|
||||
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, $decimals = 2)
|
||||
{
|
||||
$factor = floor((strlen(strval($bytes)) - 1) / 3);
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @BYTE_DENOMINATIONS[$factor];
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Generates a unique key for the website to prevent unauthorized access.
|
||||
*/
|
||||
function generate_key(int $length = 20): string
|
||||
function generate_key(int $length = 20)
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$randomString = '';
|
||||
|
@ -4,12 +4,20 @@ class AdminPageInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "admin";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Admin Controls";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Provides a base for various small admin functions";
|
||||
public bool $core = true;
|
||||
public string $visibility = self::VISIBLE_HIDDEN;
|
||||
public $key = self::KEY;
|
||||
public $name = "Admin Controls";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Various things to make admins' lives easier";
|
||||
public $documentation =
|
||||
"Various moderate-level tools for admins; for advanced, obscure, and possibly dangerous tools see the shimmie2-utils script set
|
||||
<p>Lowercase all tags:
|
||||
<br>Set all tags to lowercase for consistency
|
||||
<p>Recount tag use:
|
||||
<br>If the counts of images per tag get messed up somehow, this will reset them, and remove any unused tags
|
||||
<p>Database dump:
|
||||
<br>Download the contents of the database in plain text format, useful for backups.
|
||||
<p>Image dump:
|
||||
<br>Download all the images as a .zip file (Requires ZipArchive)";
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
<?php declare(strict_types=1);
|
||||
<?php /** @noinspection PhpUnusedPrivateMethodInspection */
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Sent when the admin page is ready to be added to
|
||||
*/
|
||||
class AdminBuildingEvent extends Event
|
||||
{
|
||||
public Page $page;
|
||||
/** @var Page */
|
||||
public $page;
|
||||
|
||||
public function __construct(Page $page)
|
||||
{
|
||||
@ -16,8 +18,10 @@ class AdminBuildingEvent extends Event
|
||||
|
||||
class AdminActionEvent extends Event
|
||||
{
|
||||
public string $action;
|
||||
public bool $redirect = true;
|
||||
/** @var string */
|
||||
public $action;
|
||||
/** @var bool */
|
||||
public $redirect = true;
|
||||
|
||||
public function __construct(string $action)
|
||||
{
|
||||
@ -29,11 +33,11 @@ class AdminActionEvent extends Event
|
||||
class AdminPage extends Extension
|
||||
{
|
||||
/** @var AdminPageTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $database, $page, $user;
|
||||
global $page, $user;
|
||||
|
||||
if ($event->page_matches("admin")) {
|
||||
if (!$user->can(Permissions::MANAGE_ADMINTOOLS)) {
|
||||
@ -48,7 +52,6 @@ class AdminPage extends Extension
|
||||
if ($user->check_auth_token()) {
|
||||
log_info("admin", "Util: $action");
|
||||
set_time_limit(0);
|
||||
$database->set_timeout(300000);
|
||||
send_event($aae);
|
||||
}
|
||||
|
||||
@ -77,10 +80,8 @@ class AdminPage extends Extension
|
||||
}
|
||||
if ($event->cmd == "get-page") {
|
||||
global $page;
|
||||
$_SERVER['REQUEST_URI'] = $event->args[0];
|
||||
if (isset($event->args[1])) {
|
||||
parse_str($event->args[1], $_GET);
|
||||
$_SERVER['REQUEST_URI'] .= "?" . $event->args[1];
|
||||
}
|
||||
send_event(new PageRequestEvent($event->args[0]));
|
||||
$page->display();
|
||||
@ -102,7 +103,7 @@ class AdminPage extends Extension
|
||||
$uid = $event->args[0];
|
||||
$image = Image::by_id_or_hash($uid);
|
||||
if ($image) {
|
||||
send_event(new ThumbnailGenerationEvent($image->hash, $image->get_mime(), true));
|
||||
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
|
||||
} else {
|
||||
print("No post with ID '$uid'\n");
|
||||
}
|
||||
@ -128,6 +129,7 @@ class AdminPage extends Extension
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$this->theme->display_page();
|
||||
$this->theme->display_form();
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
@ -147,4 +149,46 @@ class AdminPage extends Extension
|
||||
$event->add_link("Board Admin", make_link("admin"));
|
||||
}
|
||||
}
|
||||
|
||||
public function onAdminAction(AdminActionEvent $event)
|
||||
{
|
||||
$action = $event->action;
|
||||
if (method_exists($this, $action)) {
|
||||
$event->redirect = $this->$action();
|
||||
}
|
||||
}
|
||||
|
||||
private function set_tag_case()
|
||||
{
|
||||
global $database;
|
||||
$database->execute(
|
||||
"UPDATE tags SET tag=:tag1 WHERE LOWER(tag) = LOWER(:tag2)",
|
||||
["tag1" => $_POST['tag'], "tag2" => $_POST['tag']]
|
||||
);
|
||||
log_info("admin", "Fixed the case of {$_POST['tag']}", "Fixed case");
|
||||
return true;
|
||||
}
|
||||
|
||||
private function lowercase_all_tags()
|
||||
{
|
||||
global $database;
|
||||
$database->execute("UPDATE tags SET tag=lower(tag)");
|
||||
log_warning("admin", "Set all tags to lowercase", "Set all tags to lowercase");
|
||||
return true;
|
||||
}
|
||||
|
||||
private function recount_tag_use()
|
||||
{
|
||||
global $database;
|
||||
$database->Execute("
|
||||
UPDATE tags
|
||||
SET count = COALESCE(
|
||||
(SELECT COUNT(image_id) FROM image_tags WHERE tag_id=tags.id GROUP BY tag_id),
|
||||
0
|
||||
)
|
||||
");
|
||||
$database->Execute("DELETE FROM tags WHERE count=0");
|
||||
log_warning("admin", "Re-counted tags", "Re-counted tags");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,59 @@ class AdminPageTest extends ShimmiePHPUnitTestCase
|
||||
$this->assertEquals("Admin Tools", $page->title);
|
||||
}
|
||||
|
||||
public function testLowercaseAndSetCase()
|
||||
{
|
||||
// Create a problem
|
||||
$ts = time(); // we need a tag that hasn't been used before
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$image_id_1 = $this->post_image("tests/pbx_screenshot.jpg", "TeStCase$ts");
|
||||
|
||||
// Validate problem
|
||||
$page = $this->get_page("post/view/$image_id_1");
|
||||
$this->assertEquals("Image $image_id_1: TeStCase$ts", $page->title);
|
||||
|
||||
// Fix
|
||||
send_event(new AdminActionEvent('lowercase_all_tags'));
|
||||
|
||||
// Validate fix
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_title("Image $image_id_1: testcase$ts");
|
||||
|
||||
// Change
|
||||
$_POST["tag"] = "TestCase$ts";
|
||||
send_event(new AdminActionEvent('set_tag_case'));
|
||||
|
||||
// Validate change
|
||||
$this->get_page("post/view/$image_id_1");
|
||||
$this->assert_title("Image $image_id_1: TestCase$ts");
|
||||
}
|
||||
|
||||
# FIXME: make sure the admin tools actually work
|
||||
public function testRecount()
|
||||
{
|
||||
global $database;
|
||||
|
||||
// Create a problem
|
||||
$ts = time(); // we need a tag that hasn't been used before
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$database->execute(
|
||||
"INSERT INTO tags(tag, count) VALUES(:tag, :count)",
|
||||
["tag"=>"tes$ts", "count"=>42]
|
||||
);
|
||||
|
||||
// Fix
|
||||
send_event(new AdminActionEvent('recount_tag_use'));
|
||||
|
||||
// Validate fix
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$database->get_one(
|
||||
"SELECT count FROM tags WHERE tag = :tag",
|
||||
["tag"=>"tes$ts"]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testCommands()
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php declare(strict_types=1);
|
||||
use function MicroHTML\INPUT;
|
||||
|
||||
class AdminPageTheme extends Themelet
|
||||
{
|
||||
@ -13,4 +14,41 @@ class AdminPageTheme extends Themelet
|
||||
$page->set_heading("Admin Tools");
|
||||
$page->add_block(new NavBlock());
|
||||
}
|
||||
|
||||
protected function button(string $name, string $action, bool $protected=false): string
|
||||
{
|
||||
$c_protected = $protected ? " protected" : "";
|
||||
$html = make_form(make_link("admin/$action"), "POST", false, "admin$c_protected");
|
||||
if ($protected) {
|
||||
$html .= "<input type='submit' id='$action' value='$name' disabled='disabled'>";
|
||||
$html .= "<input type='checkbox' onclick='$(\"#$action\").attr(\"disabled\", !$(this).is(\":checked\"))'>";
|
||||
} else {
|
||||
$html .= "<input type='submit' id='$action' value='$name'>";
|
||||
}
|
||||
$html .= "</form>\n";
|
||||
return $html;
|
||||
}
|
||||
|
||||
/*
|
||||
* Show a form which links to admin_utils with POST[action] set to one of:
|
||||
* 'lowercase all tags'
|
||||
* 'recount tag use'
|
||||
* etc
|
||||
*/
|
||||
public function display_form()
|
||||
{
|
||||
global $page;
|
||||
|
||||
$html = "";
|
||||
$html .= $this->button("All tags to lowercase", "lowercase_all_tags", true);
|
||||
$html .= $this->button("Recount tag use", "recount_tag_use", false);
|
||||
$page->add_block(new Block("Misc Admin Tools", $html));
|
||||
|
||||
$html = (string)SHM_SIMPLE_FORM(
|
||||
"admin/set_tag_case",
|
||||
INPUT(["type"=>'text', "name"=>'tag', "placeholder"=>'Enter tag with correct case', "class"=>'autocomplete_tags', "autocomplete"=>'off']),
|
||||
SHM_SUBMIT('Set Tag Case'),
|
||||
);
|
||||
$page->add_block(new Block("Set Tag Case", $html));
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ class AliasEditorInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "alias_editor";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Alias Editor";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Edit the alias list";
|
||||
public ?string $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
|
||||
public bool $core = true;
|
||||
public $key = self::KEY;
|
||||
public $name = "Alias Editor";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Edit the alias list";
|
||||
public $documentation = 'The list is visible at <a href="$site/alias/list">/alias/list</a>; only site admins can edit it, other people can view and download it';
|
||||
public $core = true;
|
||||
}
|
||||
|
@ -26,8 +26,10 @@ class AliasTable extends Table
|
||||
|
||||
class AddAliasEvent extends Event
|
||||
{
|
||||
public string $oldtag;
|
||||
public string $newtag;
|
||||
/** @var string */
|
||||
public $oldtag;
|
||||
/** @var string */
|
||||
public $newtag;
|
||||
|
||||
public function __construct(string $oldtag, string $newtag)
|
||||
{
|
||||
@ -39,7 +41,7 @@ class AddAliasEvent extends Event
|
||||
|
||||
class DeleteAliasEvent extends Event
|
||||
{
|
||||
public string $oldtag;
|
||||
public $oldtag;
|
||||
|
||||
public function __construct(string $oldtag)
|
||||
{
|
||||
@ -55,7 +57,7 @@ class AddAliasException extends SCoreException
|
||||
class AliasEditor extends Extension
|
||||
{
|
||||
/** @var AliasEditorTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
@ -94,7 +96,7 @@ class AliasEditor extends Extension
|
||||
$this->theme->display_aliases($t->table($t->query()), $t->paginator());
|
||||
} elseif ($event->get_arg(0) == "export") {
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_mime(MimeType::CSV);
|
||||
$page->set_type(MIME_TYPE_CSV);
|
||||
$page->set_filename("aliases.csv");
|
||||
$page->set_data($this->get_alias_csv($database));
|
||||
} elseif ($event->get_arg(0) == "import") {
|
||||
|
@ -36,7 +36,7 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Post $image_id: test2");
|
||||
$this->assert_title("Image $image_id: test2");
|
||||
$this->get_page("post/list/test1/1"); # searching for an alias should find the master tag
|
||||
$this->assert_response(302);
|
||||
$this->get_page("post/list/test2/1"); # check that searching for the main tag still works
|
||||
@ -67,13 +67,13 @@ class AliasEditorTest extends ShimmiePHPUnitTestCase
|
||||
$image_id_2 = $this->post_image("tests/bedroom_workshop.jpg", "onetag");
|
||||
$this->get_page("post/list/onetag/1"); # searching for an aliased tag should find its aliases
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Posts Found");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi/1");
|
||||
$this->assert_title("multi");
|
||||
$this->assert_no_text("No Posts Found");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->get_page("post/list/multi tag/1");
|
||||
$this->assert_title("multi tag");
|
||||
$this->assert_no_text("No Posts Found");
|
||||
$this->assert_no_text("No Images Found");
|
||||
$this->delete_image($image_id_1);
|
||||
$this->delete_image($image_id_2);
|
||||
|
||||
|
@ -4,9 +4,9 @@ class ApprovalInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "approval";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Approval";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Adds an approval step to the upload/import process.";
|
||||
public $key = self::KEY;
|
||||
public $name = "Approval";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Adds an approval step to the upload/import process.";
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ abstract class ApprovalConfig
|
||||
class Approval extends Extension
|
||||
{
|
||||
/** @var ApprovalTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
@ -33,7 +33,7 @@ class Approval extends Extension
|
||||
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
|
||||
}
|
||||
if (empty($image_id)) {
|
||||
throw new SCoreException("Can not approve post: No valid Post ID given.");
|
||||
throw new SCoreException("Can not approve image: No valid Image ID given.");
|
||||
}
|
||||
|
||||
self::approve_image($image_id);
|
||||
@ -48,7 +48,7 @@ class Approval extends Extension
|
||||
$image_id = isset($_POST['image_id']) ? $_POST['image_id'] : null;
|
||||
}
|
||||
if (empty($image_id)) {
|
||||
throw new SCoreException("Can not disapprove image: No valid Post ID given.");
|
||||
throw new SCoreException("Can not disapprove image: No valid Image ID given.");
|
||||
}
|
||||
|
||||
self::disapprove_image($image_id);
|
||||
@ -99,9 +99,9 @@ class Approval extends Extension
|
||||
|
||||
public function onDisplayingImage(DisplayingImageEvent $event)
|
||||
{
|
||||
global $page;
|
||||
global $user, $page, $config;
|
||||
|
||||
if (!$this->check_permissions(($event->image))) {
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES) && $event->image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("post/list"));
|
||||
}
|
||||
@ -121,13 +121,13 @@ class Approval extends Extension
|
||||
const SEARCH_REGEXP = "/^approved:(yes|no)/";
|
||||
public function onSearchTermParse(SearchTermParseEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
global $user, $database, $config;
|
||||
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES)) {
|
||||
$matches = [];
|
||||
|
||||
if (is_null($event->term) && $this->no_approval_query($event->context)) {
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
|
||||
}
|
||||
|
||||
if (is_null($event->term)) {
|
||||
@ -135,9 +135,9 @@ class Approval extends Extension
|
||||
}
|
||||
if (preg_match(self::SEARCH_REGEXP, strtolower($event->term), $matches)) {
|
||||
if ($user->can(Permissions::APPROVE_IMAGE) && $matches[1] == "no") {
|
||||
$event->add_querylet(new Querylet("approved != :true", ["true"=>true]));
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_N ")));
|
||||
} else {
|
||||
$event->add_querylet(new Querylet("approved = :true", ["true"=>true]));
|
||||
$event->add_querylet(new Querylet($database->scoreql_to_sql("approved = SCORE_BOOL_Y ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -187,26 +187,6 @@ class Approval extends Extension
|
||||
);
|
||||
}
|
||||
|
||||
private function check_permissions(Image $image): bool
|
||||
{
|
||||
global $user, $config;
|
||||
|
||||
if ($config->get_bool(ApprovalConfig::IMAGES) && $image->approved===false && !$user->can(Permissions::APPROVE_IMAGE)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onImageDownloading(ImageDownloadingEvent $event)
|
||||
{
|
||||
/**
|
||||
* Deny images upon insufficient permissions.
|
||||
**/
|
||||
if (!$this->check_permissions($event->image)) {
|
||||
throw new SCoreException("Access denied");
|
||||
}
|
||||
}
|
||||
|
||||
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
|
||||
{
|
||||
global $user, $config;
|
||||
@ -261,15 +241,15 @@ class Approval extends Extension
|
||||
global $database;
|
||||
|
||||
if ($this->get_version(ApprovalConfig::VERSION) < 1) {
|
||||
$database->execute("ALTER TABLE images ADD COLUMN approved BOOLEAN NOT NULL DEFAULT FALSE");
|
||||
$database->execute("ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL");
|
||||
$database->execute("CREATE INDEX images_approved_idx ON images(approved)");
|
||||
$this->set_version(ApprovalConfig::VERSION, 2);
|
||||
}
|
||||
$database->execute($database->scoreql_to_sql(
|
||||
"ALTER TABLE images ADD COLUMN approved SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N"
|
||||
));
|
||||
$database->execute(
|
||||
"ALTER TABLE images ADD COLUMN approved_by_id INTEGER NULL"
|
||||
);
|
||||
|
||||
if ($this->get_version(ApprovalConfig::VERSION) < 2) {
|
||||
$database->standardise_boolean("images", "approved");
|
||||
$this->set_version(ApprovalConfig::VERSION, 2);
|
||||
$database->execute("CREATE INDEX images_approved_idx ON images(approved)");
|
||||
$this->set_version(ApprovalConfig::VERSION, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use function MicroHTML\INPUT;
|
||||
|
||||
class ApprovalTheme extends Themelet
|
||||
{
|
||||
public function get_image_admin_html(Image $image): string
|
||||
public function get_image_admin_html(Image $image)
|
||||
{
|
||||
if ($image->approved===true) {
|
||||
$html = SHM_SIMPLE_FORM(
|
||||
@ -24,24 +24,26 @@ class ApprovalTheme extends Themelet
|
||||
return (string)$html;
|
||||
}
|
||||
|
||||
public function get_help_html(): string
|
||||
|
||||
public function get_help_html()
|
||||
{
|
||||
return '<p>Search for posts that are approved/not approved.</p>
|
||||
return '<p>Search for images that are approved/not approved.</p>
|
||||
<div class="command_example">
|
||||
<pre>approved:yes</pre>
|
||||
<p>Returns posts that have been approved.</p>
|
||||
<p>Returns images that have been approved.</p>
|
||||
</div>
|
||||
<div class="command_example">
|
||||
<pre>approved:no</pre>
|
||||
<p>Returns posts that have not been approved.</p>
|
||||
<p>Returns images that have not been approved.</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
||||
public function display_admin_block(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Approval");
|
||||
$sb->add_bool_option(ApprovalConfig::IMAGES, "Posts: ");
|
||||
$sb = new SetupBlock("Approval");
|
||||
$sb->add_bool_option(ApprovalConfig::IMAGES, "Images: ");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function display_admin_form()
|
||||
@ -50,9 +52,9 @@ class ApprovalTheme extends Themelet
|
||||
|
||||
$html = (string)SHM_SIMPLE_FORM(
|
||||
"admin/approval",
|
||||
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Posts"),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'approve_all'], "Approve All Images"),
|
||||
BR(),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Posts"),
|
||||
BUTTON(["name"=>'approval_action', "value"=>'disapprove_all'], "Disapprove All Images"),
|
||||
);
|
||||
$page->add_block(new Block("Approval", $html));
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ class ArtistsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "artists";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Artists System";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Simple artists extension";
|
||||
public bool $beta = true;
|
||||
public $key = self::KEY;
|
||||
public $name = "Artists System";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = ["Sein Kraft"=>"mail@seinkraft.info","Alpha"=>"alpha@furries.com.ar"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Simple artists extension";
|
||||
public $beta = true;
|
||||
}
|
||||
|
@ -2,9 +2,12 @@
|
||||
|
||||
class AuthorSetEvent extends Event
|
||||
{
|
||||
public Image $image;
|
||||
public User $user;
|
||||
public string $author;
|
||||
/** @var Image */
|
||||
public $image;
|
||||
/** @var User */
|
||||
public $user;
|
||||
/** @var string */
|
||||
public $author;
|
||||
|
||||
public function __construct(Image $image, User $user, string $author)
|
||||
{
|
||||
@ -18,7 +21,7 @@ class AuthorSetEvent extends Event
|
||||
class Artists extends Extension
|
||||
{
|
||||
/** @var ArtistsTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onImageInfoSet(ImageInfoSetEvent $event)
|
||||
{
|
||||
@ -550,7 +553,7 @@ class Artists extends Extension
|
||||
$urlsAsString = $inputs["urls"];
|
||||
$urlsIDsAsString = $inputs["urlsIDs"];
|
||||
|
||||
if (str_contains($name, " ")) {
|
||||
if (strpos($name, " ")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -680,7 +683,7 @@ class Artists extends Extension
|
||||
);
|
||||
}
|
||||
|
||||
private function add_artist(): int
|
||||
private function add_artist()
|
||||
{
|
||||
global $user;
|
||||
$inputs = validate_input([
|
||||
@ -692,7 +695,7 @@ class Artists extends Extension
|
||||
]);
|
||||
|
||||
$name = $inputs["name"];
|
||||
if (str_contains($name, " ")) {
|
||||
if (strpos($name, " ")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -27,19 +27,19 @@ class ArtistsTheme extends Themelet
|
||||
<input type='submit' name='edit' id='edit' value='New Artist'/>
|
||||
</form>";
|
||||
}
|
||||
|
||||
|
||||
if ($mode == "editor") {
|
||||
$html = "<form method='post' action='".make_link("artist/new_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' value='New Artist'/>
|
||||
</form>
|
||||
|
||||
|
||||
<form method='post' action='".make_link("artist/edit_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' value='Edit Artist'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>";
|
||||
|
||||
|
||||
if ($is_admin) {
|
||||
$html .= "<form method='post' action='".make_link("artist/nuke_artist")."'>
|
||||
".$user->get_auth_html()."
|
||||
@ -47,19 +47,19 @@ class ArtistsTheme extends Themelet
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>";
|
||||
}
|
||||
|
||||
|
||||
$html .= "<form method='post' action='".make_link("artist/add_alias")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' value='Add Alias'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>
|
||||
|
||||
|
||||
<form method='post' action='".make_link("artist/add_member")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' value='Add Member'/>
|
||||
<input type='hidden' name='artist_id' value='".$artistID."'>
|
||||
</form>
|
||||
|
||||
|
||||
<form method='post' action='".make_link("artist/add_url")."'>
|
||||
".$user->get_auth_html()."
|
||||
<input type='submit' name='edit' value='Add Url'/>
|
||||
@ -131,7 +131,7 @@ class ArtistsTheme extends Themelet
|
||||
global $page;
|
||||
$page->add_block(new Block("Edit artist", $html, "main", 10));
|
||||
}
|
||||
|
||||
|
||||
public function new_artist_composer()
|
||||
{
|
||||
global $page, $user;
|
||||
@ -152,7 +152,7 @@ class ArtistsTheme extends Themelet
|
||||
$page->set_heading("Artists");
|
||||
$page->add_block(new Block("Artists", $html, "main", 10));
|
||||
}
|
||||
|
||||
|
||||
public function list_artists($artists, $pageNumber, $totalPages)
|
||||
{
|
||||
global $user, $page;
|
||||
@ -167,7 +167,7 @@ class ArtistsTheme extends Themelet
|
||||
if (!$user->is_anonymous()) {
|
||||
$html .= "<th colspan='2'>Action</th>";
|
||||
} // space for edit link
|
||||
|
||||
|
||||
$html .= "</tr></thead>";
|
||||
|
||||
$deletionLinkActionArray = [
|
||||
@ -244,7 +244,7 @@ class ArtistsTheme extends Themelet
|
||||
<input type="hidden" name="artistID" value='.$artistID.' /></td></tr>
|
||||
<tr><td colspan="2"><input type="submit" value="Submit" /></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
</form>
|
||||
';
|
||||
|
||||
global $page;
|
||||
@ -354,7 +354,7 @@ class ArtistsTheme extends Themelet
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>";
|
||||
|
||||
|
||||
if ($userIsLogged) {
|
||||
$html .= "<th></th>";
|
||||
}
|
||||
@ -402,13 +402,13 @@ class ArtistsTheme extends Themelet
|
||||
$artist_images = "";
|
||||
foreach ($images as $image) {
|
||||
$thumb_html = $this->build_thumb_html($image);
|
||||
|
||||
|
||||
$artist_images .= '<span class="thumb">'.
|
||||
'<a href="$image_link">'.$thumb_html.'</a>'.
|
||||
'</span>';
|
||||
}
|
||||
|
||||
$page->add_block(new Block("Artist Posts", $artist_images, "main", 20));
|
||||
|
||||
$page->add_block(new Block("Artist Images", $artist_images, "main", 20));
|
||||
}
|
||||
|
||||
private function render_aliases(array $aliases, bool $userIsLogged, bool $userIsAdmin): string
|
||||
@ -546,13 +546,13 @@ class ArtistsTheme extends Themelet
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function get_help_html(): string
|
||||
public function get_help_html()
|
||||
{
|
||||
return '<p>Search for posts with a particular artist.</p>
|
||||
return '<p>Search for images with a particular artist.</p>
|
||||
<div class="command_example">
|
||||
<pre>artist=leonardo</pre>
|
||||
<p>Returns posts with the artist "leonardo".</p>
|
||||
</div>
|
||||
<p>Returns images with the artist "leonardo".</p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ class AutoTaggerInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "auto_tagger";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Auto-Tagger";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Provides several automatic tagging functions";
|
||||
public $key = self::KEY;
|
||||
public $name = "Auto-Tagger";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Provides several automatic tagging functions";
|
||||
}
|
||||
|
@ -28,8 +28,10 @@ class AutoTaggerTable extends Table
|
||||
|
||||
class AddAutoTagEvent extends Event
|
||||
{
|
||||
public string $tag;
|
||||
public string $additional_tags;
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var string */
|
||||
public $additional_tags;
|
||||
|
||||
public function __construct(string $tag, string $additional_tags)
|
||||
{
|
||||
@ -41,7 +43,7 @@ class AddAutoTagEvent extends Event
|
||||
|
||||
class DeleteAutoTagEvent extends Event
|
||||
{
|
||||
public string $tag;
|
||||
public $tag;
|
||||
|
||||
public function __construct(string $tag)
|
||||
{
|
||||
@ -61,7 +63,7 @@ class AddAutoTagException extends SCoreException
|
||||
class AutoTagger extends Extension
|
||||
{
|
||||
/** @var AutoTaggerTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
@ -100,7 +102,7 @@ class AutoTagger extends Extension
|
||||
$this->theme->display_auto_tagtable($t->table($t->query()), $t->paginator());
|
||||
} elseif ($event->get_arg(0) == "export") {
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_mime(MimeType::CSV);
|
||||
$page->set_type(MIME_TYPE_CSV);
|
||||
$page->set_filename("auto_tag.csv");
|
||||
$page->set_data($this->get_auto_tag_csv($database));
|
||||
} elseif ($event->get_arg(0) == "import") {
|
||||
@ -312,10 +314,14 @@ class AutoTagger extends Extension
|
||||
$tags_mixed = array_merge($tags_mixed, $new_tags);
|
||||
}
|
||||
|
||||
return array_intersect_key(
|
||||
$results = array_intersect_key(
|
||||
$tags_mixed,
|
||||
array_unique(array_map('strtolower', $tags_mixed))
|
||||
);
|
||||
|
||||
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,14 +37,14 @@ class AutoTaggerTest extends ShimmiePHPUnitTestCase
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Post $image_id: test1 test2");
|
||||
$this->assert_title("Image $image_id: test1 test2");
|
||||
$this->delete_image($image_id);
|
||||
|
||||
send_event(new AddAutoTagEvent("test2", "test3"));
|
||||
|
||||
$image_id = $this->post_image("tests/pbx_screenshot.jpg", "test1");
|
||||
$this->get_page("post/view/$image_id"); # check that the tag has been replaced
|
||||
$this->assert_title("Post $image_id: test1 test2 test3");
|
||||
$this->assert_title("Image $image_id: test1 test2 test3");
|
||||
$this->delete_image($image_id);
|
||||
|
||||
send_event(new DeleteAutoTagEvent("test1"));
|
||||
|
@ -4,8 +4,8 @@ class AutoCompleteInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "autocomplete";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Autocomplete";
|
||||
public array $authors = ["Daku"=>"admin@codeanimu.net"];
|
||||
public string $description = "Adds autocomplete to search & tagging.";
|
||||
public $key = self::KEY;
|
||||
public $name = "Autocomplete";
|
||||
public $authors = ["Daku"=>"admin@codeanimu.net"];
|
||||
public $description = "Adds autocomplete to search & tagging.";
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
class AutoComplete extends Extension
|
||||
{
|
||||
/** @var AutoCompleteTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function get_priority(): int
|
||||
{
|
||||
@ -12,68 +12,58 @@ class AutoComplete extends Extension
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page;
|
||||
global $cache, $page, $database;
|
||||
|
||||
if ($event->page_matches("api/internal/autocomplete")) {
|
||||
$limit = $_GET["limit"] ?? 0;
|
||||
$s = $_GET["s"] ?? null;
|
||||
|
||||
$res = $this->complete($s, $limit);
|
||||
if (!isset($_GET["s"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_mime(MimeType::JSON);
|
||||
$page->set_type(MIME_TYPE_JSON);
|
||||
|
||||
$s = strtolower($_GET["s"]);
|
||||
if (
|
||||
$s == '' ||
|
||||
$s[0] == '_' ||
|
||||
$s[0] == '%' ||
|
||||
strlen($s) > 32
|
||||
) {
|
||||
$page->set_data("{}");
|
||||
return;
|
||||
}
|
||||
|
||||
//$limit = 0;
|
||||
$cache_key = "autocomplete-$s";
|
||||
$limitSQL = "";
|
||||
$s = str_replace('_', '\_', $s);
|
||||
$s = str_replace('%', '\%', $s);
|
||||
$SQLarr = ["search"=>"$s%"]; #, "cat_search"=>"%:$s%"];
|
||||
if (isset($_GET["limit"]) && $_GET["limit"] !== 0) {
|
||||
$limitSQL = "LIMIT :limit";
|
||||
$SQLarr['limit'] = $_GET["limit"];
|
||||
$cache_key .= "-" . $_GET["limit"];
|
||||
}
|
||||
|
||||
$res = $cache->get($cache_key);
|
||||
if (!$res) {
|
||||
$res = $database->get_pairs(
|
||||
"
|
||||
SELECT tag, count
|
||||
FROM tags
|
||||
WHERE LOWER(tag) LIKE LOWER(:search)
|
||||
-- OR LOWER(tag) LIKE LOWER(:cat_search)
|
||||
AND count > 0
|
||||
ORDER BY count DESC
|
||||
$limitSQL",
|
||||
$SQLarr
|
||||
);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
|
||||
$page->set_data(json_encode($res));
|
||||
}
|
||||
|
||||
$this->theme->build_autocomplete($page);
|
||||
}
|
||||
|
||||
private function complete(string $search, int $limit): array
|
||||
{
|
||||
global $cache, $database;
|
||||
|
||||
if (!$search) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$search = strtolower($search);
|
||||
if (
|
||||
$search == '' ||
|
||||
$search[0] == '_' ||
|
||||
$search[0] == '%' ||
|
||||
strlen($search) > 32
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cache_key = "autocomplete-$search";
|
||||
$limitSQL = "";
|
||||
$search = str_replace('_', '\_', $search);
|
||||
$search = str_replace('%', '\%', $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 (!$res) {
|
||||
$res = $database->get_pairs(
|
||||
"
|
||||
SELECT tag, count
|
||||
FROM tags
|
||||
WHERE LOWER(tag) LIKE LOWER(:search)
|
||||
-- OR LOWER(tag) LIKE LOWER(:cat_search)
|
||||
AND count > 0
|
||||
ORDER BY count DESC
|
||||
$limitSQL
|
||||
",
|
||||
$SQLarr
|
||||
);
|
||||
$cache->set($cache_key, $res, 600);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
@ -63,12 +63,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
var keyCode = e.keyCode || e.which;
|
||||
|
||||
//Stop tags containing space.
|
||||
if(keyCode === 32) {
|
||||
if(keyCode == 32) {
|
||||
e.preventDefault();
|
||||
|
||||
$('.autocomplete_tags').tagit('createTag', $(this).val());
|
||||
$(this).autocomplete('close');
|
||||
} else if (keyCode === 9) {
|
||||
} else if (keyCode == 9) {
|
||||
e.preventDefault();
|
||||
|
||||
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();
|
||||
|
@ -4,13 +4,13 @@ class BanWordsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "ban_words";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Comment Word Ban";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "For stopping spam and other comment abuse";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Comment Word Ban";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "For stopping spam and other comment abuse";
|
||||
public $documentation =
|
||||
"Allows an administrator to ban certain words
|
||||
from comments. This can be a very simple but effective way
|
||||
of stopping spam; just add \"viagra\", \"porn\", etc to the
|
||||
|
@ -55,7 +55,7 @@ xanax
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Banned Phrases");
|
||||
$sb = new SetupBlock("Banned Phrases");
|
||||
$sb->add_label("One per line, lines that start with slashes are treated as regex<br/>");
|
||||
$sb->add_longtext_option("banned_words");
|
||||
$failed = [];
|
||||
@ -69,6 +69,7 @@ xanax
|
||||
if ($failed) {
|
||||
$sb->add_label("Failed regexes: ".join(", ", $failed));
|
||||
}
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,7 +87,7 @@ xanax
|
||||
}
|
||||
} else {
|
||||
// other words are literal
|
||||
if (str_contains($comment, $word)) {
|
||||
if (strpos($comment, $word) !== false) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ class BanWordsTest extends ShimmiePHPUnitTestCase
|
||||
send_event(new CommentPostingEvent($image_id, $user, $words));
|
||||
$this->fail("Exception not thrown");
|
||||
} catch (CommentPostingException $e) {
|
||||
$this->assertEquals("Comment contains banned terms", $e->getMessage());
|
||||
$this->assertEquals($e->getMessage(), "Comment contains banned terms");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,52 +4,29 @@ class BBCodeInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bbcode";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "BBCode";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public bool $core = true;
|
||||
public string $description = "Turns BBCode into HTML";
|
||||
public ?string $documentation =
|
||||
" Basic formatting tags:
|
||||
public $key = self::KEY;
|
||||
public $name = "BBCode";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $core = true;
|
||||
public $description = "Turns BBCode into HTML";
|
||||
public $documentation =
|
||||
" Supported tags:
|
||||
<ul>
|
||||
<li>[img]url[/img]
|
||||
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
|
||||
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
|
||||
<li>[b]<b>bold</b>[/b]
|
||||
<li>[i]<i>italic</i>[/i]
|
||||
<li>[u]<u>underline</u>[/u]
|
||||
<li>[s]<s>strikethrough</s>[/s]
|
||||
<li>[sup]<sup>superscript</sup>[/sup]
|
||||
<li>[sub]<sub>subscript</sub>[/sub]
|
||||
<li>[h1]Heading 1[/h1]
|
||||
<li>[h2]Heading 2[/h2]
|
||||
<li>[h3]Heading 3[/h3]
|
||||
<li>[h4]Heading 4[/h4]
|
||||
<li>[align=left|center|right]Aligned Text[/align]
|
||||
</ul>
|
||||
<br>
|
||||
Link tags:
|
||||
<ul>
|
||||
<li>[img]url[/img]
|
||||
<li>[url]<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>[/url]
|
||||
<li>[url=<a href=\"{self::SHIMMIE_URL}\">https://code.shishnet.org/</a>]some text[/url]
|
||||
<li>[url]site://ext_doc/bbcode[/url]
|
||||
<li>[url=site://ext_doc/bbcode]Link to BBCode docs[/url]
|
||||
<li>[email]<a href=\"mailto:{self::SHISH_EMAIL}\">webmaster@shishnet.org</a>[/email]
|
||||
<li>[[wiki article]]
|
||||
<li>[[wiki article|with some text]]
|
||||
<li>>>123 (link to post #123)
|
||||
<li>[anchor=target]Scroll to #bb-target[/anchor]
|
||||
</ul>
|
||||
<br>
|
||||
More format Tags:
|
||||
<ul>
|
||||
<li>[list]Unordered list[/list]
|
||||
<li>[ul]Unordered list[/ul]
|
||||
<li>[ol]Ordered list[/ol]
|
||||
<li>[li]List Item[/li]
|
||||
<li>[code]<pre>print(\"Hello World!\");</pre>[/code]
|
||||
<li>[spoiler]<span style=\"background-color:#000; color:#000;\">Voldemort is bad</span>[/spoiler]
|
||||
<li>[quote]<blockquote><small>To be or not to be...</small></blockquote>[/quote]
|
||||
<li>[quote=Shakespeare]<blockquote><em>Shakespeare said:</em><br><small>... That is the question</small></blockquote>[/quote]
|
||||
<li>[quote]text[/quote]
|
||||
<li>[quote=Username]text[/quote]
|
||||
<li>>>123 (link to image #123)
|
||||
</ul>";
|
||||
}
|
||||
|
@ -4,60 +4,60 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
public function testBasics()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<b>bold</b><i>italic</i>",
|
||||
$this->filter("[b]bold[/b][i]italic[/i]")
|
||||
$this->filter("[b]bold[/b][i]italic[/i]"),
|
||||
"<b>bold</b><i>italic</i>"
|
||||
);
|
||||
}
|
||||
|
||||
public function testStacking()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<b>B</b><i>I</i><b>B</b>",
|
||||
$this->filter("[b]B[/b][i]I[/i][b]B[/b]")
|
||||
$this->filter("[b]B[/b][i]I[/i][b]B[/b]"),
|
||||
"<b>B</b><i>I</i><b>B</b>"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"<b>bold<i>bolditalic</i>bold</b>",
|
||||
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]")
|
||||
$this->filter("[b]bold[i]bolditalic[/i]bold[/b]"),
|
||||
"<b>bold<i>bolditalic</i>bold</b>"
|
||||
);
|
||||
}
|
||||
|
||||
public function testFailure()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"[b]bold[i]italic",
|
||||
$this->filter("[b]bold[i]italic")
|
||||
$this->filter("[b]bold[i]italic"),
|
||||
"[b]bold[i]italic"
|
||||
);
|
||||
}
|
||||
|
||||
public function testCode()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<pre>[b]bold[/b]</pre>",
|
||||
$this->filter("[code][b]bold[/b][/code]")
|
||||
$this->filter("[code][b]bold[/b][/code]"),
|
||||
"<pre>[b]bold[/b]</pre>"
|
||||
);
|
||||
}
|
||||
|
||||
public function testNestedList()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>",
|
||||
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]")
|
||||
$this->filter("[list][*]a[list][*]a[*]b[/list][*]b[/list]"),
|
||||
"<ul><li>a<ul><li>a<li>b</ul><li>b</ul>"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>",
|
||||
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]")
|
||||
$this->filter("[ul][*]a[ol][*]a[*]b[/ol][*]b[/ul]"),
|
||||
"<ul><li>a<ol><li>a<li>b</ol><li>b</ul>"
|
||||
);
|
||||
}
|
||||
|
||||
public function testSpoiler()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>",
|
||||
$this->filter("[spoiler]ShishNet[/spoiler]")
|
||||
$this->filter("[spoiler]ShishNet[/spoiler]"),
|
||||
"<span style=\"background-color:#000; color:#000;\">ShishNet</span>"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"FuvfuArg",
|
||||
$this->strip("[spoiler]ShishNet[/spoiler]")
|
||||
$this->strip("[spoiler]ShishNet[/spoiler]"),
|
||||
"FuvfuArg"
|
||||
);
|
||||
#$this->assertEquals(
|
||||
# $this->filter("[spoiler]ShishNet"),
|
||||
@ -67,42 +67,42 @@ class BBCodeTest extends ShimmiePHPUnitTestCase
|
||||
public function testURL()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<a href=\"https://shishnet.org\">https://shishnet.org</a>",
|
||||
$this->filter("[url]https://shishnet.org[/url]")
|
||||
$this->filter("[url]https://shishnet.org[/url]"),
|
||||
"<a href=\"https://shishnet.org\">https://shishnet.org</a>"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"<a href=\"https://shishnet.org\">ShishNet</a>",
|
||||
$this->filter("[url=https://shishnet.org]ShishNet[/url]")
|
||||
$this->filter("[url=https://shishnet.org]ShishNet[/url]"),
|
||||
"<a href=\"https://shishnet.org\">ShishNet</a>"
|
||||
);
|
||||
$this->assertEquals(
|
||||
"[url=javascript:alert(\"owned\")]click to fail[/url]",
|
||||
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]")
|
||||
$this->filter("[url=javascript:alert(\"owned\")]click to fail[/url]"),
|
||||
"[url=javascript:alert(\"owned\")]click to fail[/url]"
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmailURL()
|
||||
{
|
||||
$this->assertEquals(
|
||||
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>",
|
||||
$this->filter("[email]spam@shishnet.org[/email]")
|
||||
$this->filter("[email]spam@shishnet.org[/email]"),
|
||||
"<a href=\"mailto:spam@shishnet.org\">spam@shishnet.org</a>"
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnchor()
|
||||
{
|
||||
$this->assertEquals(
|
||||
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>',
|
||||
$this->filter("[anchor=rules]Rules[/anchor]")
|
||||
$this->filter("[anchor=rules]Rules[/anchor]"),
|
||||
'<span class="anchor">Rules <a class="alink" href="#bb-rules" name="bb-rules" title="link to this anchor"> ¶ </a></span>'
|
||||
);
|
||||
}
|
||||
|
||||
private function filter($in): string
|
||||
private function filter($in)
|
||||
{
|
||||
$bb = new BBCode();
|
||||
return $bb->format($in);
|
||||
}
|
||||
|
||||
private function strip($in): string
|
||||
private function strip($in)
|
||||
{
|
||||
$bb = new BBCode();
|
||||
return $bb->strip($in);
|
||||
|
@ -1,13 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class BiographyInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "biography";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "User Bios";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Allow users to write a bit about themselves";
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
class Biography extends Extension
|
||||
{
|
||||
/** @var BiographyTheme */
|
||||
protected ?Themelet $theme;
|
||||
|
||||
public function onUserPageBuilding(UserPageBuildingEvent $event)
|
||||
{
|
||||
global $page, $user;
|
||||
$duser = $event->display_user;
|
||||
$duser_config = UserConfig::get_for_user($event->display_user->id);
|
||||
$bio = $duser_config->get_string("biography", "");
|
||||
|
||||
if ($user->id == $duser->id) {
|
||||
$this->theme->display_composer($page, $bio);
|
||||
} else {
|
||||
$this->theme->display_biography($page, $bio);
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
global $page, $user, $user_config;
|
||||
if ($event->page_matches("biography")) {
|
||||
if ($user->check_auth_token()) {
|
||||
$user_config->set_string("biography", $_POST['biography']);
|
||||
$page->flash("Bio Updated");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(referer_or(make_link()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
class BiographyTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
public function testBio()
|
||||
{
|
||||
$this->log_in_as_user();
|
||||
$this->post_page("biography", ["biography"=>"My bio goes here"]);
|
||||
$this->get_page("user/" . self::$user_name);
|
||||
$this->assert_text("My bio goes here");
|
||||
|
||||
$this->log_in_as_admin();
|
||||
$this->get_page("user/" . self::$user_name);
|
||||
$this->assert_text("My bio goes here");
|
||||
|
||||
$this->get_page("user/" . self::$admin_name);
|
||||
$this->assert_no_text("My bio goes here");
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?php declare(strict_types=1);
|
||||
use function MicroHTML\TEXTAREA;
|
||||
|
||||
class BiographyTheme extends Themelet
|
||||
{
|
||||
public function display_biography(Page $page, string $bio)
|
||||
{
|
||||
$page->add_block(new Block("About Me", format_text($bio), "main", 30, "about-me"));
|
||||
}
|
||||
|
||||
public function display_composer(Page $page, string $bio)
|
||||
{
|
||||
global $user;
|
||||
$post_url = make_link("biography");
|
||||
$auth = $user->get_auth_html();
|
||||
|
||||
$html = SHM_SIMPLE_FORM(
|
||||
$post_url,
|
||||
TEXTAREA(["style"=>"width: 100%", "rows"=>"6", "name"=>"biography"], $bio),
|
||||
SHM_SUBMIT("Save")
|
||||
);
|
||||
|
||||
$page->add_block(new Block("About Me", (string)$html, "main", 30));
|
||||
}
|
||||
}
|
@ -4,10 +4,10 @@ class BlocksInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "blocks";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Generic Blocks";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Add HTML to some space (News, Ads, etc)";
|
||||
public $key = self::KEY;
|
||||
public $name = "Generic Blocks";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Add HTML to some space (News, Ads, etc)";
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
class Blocks extends Extension
|
||||
{
|
||||
/** @var BlocksTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
|
@ -4,12 +4,12 @@ class BlotterInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "blotter";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Blotter";
|
||||
public string $url = "http://seemslegit.com/";
|
||||
public array $authors = ["Zach Hall"=>"zach@sosguy.net"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Displays brief updates about whatever you want on every page.
|
||||
public $key = self::KEY;
|
||||
public $name = "Blotter";
|
||||
public $url = "http://seemslegit.com/";
|
||||
public $authors = ["Zach Hall"=>"zach@sosguy.net"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Displays brief updates about whatever you want on every page.
|
||||
Colors and positioning can be configured to match your site's design.
|
||||
|
||||
Development TODO at https://github.com/zshall/shimmie2/issues";
|
||||
|
@ -3,7 +3,7 @@
|
||||
class Blotter extends Extension
|
||||
{
|
||||
/** @var BlotterTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
@ -15,35 +15,41 @@ class Blotter extends Extension
|
||||
|
||||
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
if ($this->get_version("blotter_version") < 1) {
|
||||
global $config;
|
||||
$version = $config->get_int("blotter_version", 0);
|
||||
/**
|
||||
* If this version is less than "1", it's time to install.
|
||||
*
|
||||
* REMINDER: If I change the database tables, I must change up version by 1.
|
||||
*/
|
||||
if ($version < 1) {
|
||||
/**
|
||||
* Installer
|
||||
*/
|
||||
global $database, $config;
|
||||
$database->create_table("blotter", "
|
||||
id SCORE_AIPK,
|
||||
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
entry_text TEXT NOT NULL,
|
||||
important BOOLEAN NOT NULL DEFAULT FALSE
|
||||
");
|
||||
id SCORE_AIPK,
|
||||
entry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
entry_text TEXT NOT NULL,
|
||||
important SCORE_BOOL NOT NULL DEFAULT SCORE_BOOL_N
|
||||
");
|
||||
// Insert sample data:
|
||||
$database->execute(
|
||||
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
|
||||
["text"=>"Installed the blotter extension!", "important"=>true]
|
||||
["text"=>"Installed the blotter extension!", "important"=>"Y"]
|
||||
);
|
||||
log_info("blotter", "Installed tables for blotter extension.");
|
||||
$this->set_version("blotter_version", 2);
|
||||
}
|
||||
if ($this->get_version("blotter_version") < 2) {
|
||||
$database->standardise_boolean("blotter", "important");
|
||||
$this->set_version("blotter_version", 2);
|
||||
$config->set_int("blotter_version", 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Blotter");
|
||||
$sb = new SetupBlock("Blotter");
|
||||
$sb->add_int_option("blotter_recent", "<br />Number of recent entries to display: ");
|
||||
$sb->add_text_option("blotter_color", "<br />Color of important updates: (ABCDEF format) ");
|
||||
$sb->add_choice_option("blotter_position", ["Top of page" => "subheading", "In navigation bar" => "left"], "<br>Position: ");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
@ -92,7 +98,11 @@ class Blotter extends Extension
|
||||
if ($entry_text == "") {
|
||||
die("No entry message!");
|
||||
}
|
||||
$important = isset($_POST['important']);
|
||||
if (isset($_POST['important'])) {
|
||||
$important = 'Y';
|
||||
} else {
|
||||
$important = 'N';
|
||||
}
|
||||
// Now insert into db:
|
||||
$database->execute(
|
||||
"INSERT INTO blotter (entry_date, entry_text, important) VALUES (now(), :text, :important)",
|
||||
@ -114,7 +124,7 @@ class Blotter extends Extension
|
||||
if (!isset($id)) {
|
||||
die("No ID!");
|
||||
}
|
||||
$database->execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
|
||||
$database->Execute("DELETE FROM blotter WHERE id=:id", ["id"=>$id]);
|
||||
log_info("blotter", "Removed Entry #$id");
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(make_link("blotter/editor"));
|
||||
|
@ -20,7 +20,7 @@ class BlotterTheme extends Themelet
|
||||
$page->add_block(new Block("Blotter Entries", $html, "main", 10));
|
||||
}
|
||||
|
||||
public function display_blotter(array $entries): void
|
||||
public function display_blotter($entries)
|
||||
{
|
||||
global $page, $config;
|
||||
$html = $this->get_html_for_blotter($entries);
|
||||
@ -28,7 +28,7 @@ class BlotterTheme extends Themelet
|
||||
$page->add_block(new Block(null, $html, $position, 20));
|
||||
}
|
||||
|
||||
private function get_html_for_blotter_editor(array $entries): string
|
||||
private function get_html_for_blotter_editor($entries)
|
||||
{
|
||||
global $user;
|
||||
|
||||
@ -99,7 +99,7 @@ class BlotterTheme extends Themelet
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function get_html_for_blotter_page(array $entries): string
|
||||
private function get_html_for_blotter_page($entries)
|
||||
{
|
||||
/**
|
||||
* This one displays a list of all blotter entries.
|
||||
@ -130,7 +130,7 @@ class BlotterTheme extends Themelet
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function get_html_for_blotter(array $entries): string
|
||||
private function get_html_for_blotter($entries)
|
||||
{
|
||||
global $config;
|
||||
$i_color = $config->get_string("blotter_color", "#FF0000");
|
||||
|
@ -4,14 +4,14 @@ class BrowserSearchInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "browser_search";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Browser Search";
|
||||
public string $url = "http://atravelinggeek.com/";
|
||||
public array $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public ?string $version = "0.1c, October 26, 2007";
|
||||
public string $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Browser Search";
|
||||
public $url = "http://atravelinggeek.com/";
|
||||
public $authors = ["ATravelingGeek"=>"atg@atravelinggeek.com"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $version = "0.1c, October 26, 2007";
|
||||
public $description = "Allows the user to add a browser 'plugin' to search the site with real-time suggestions";
|
||||
public $documentation =
|
||||
"Once installed, users with an opensearch compatible browser should see their search box light up with whatever \"click here to add a search engine\" notification they have
|
||||
|
||||
Some code (and lots of help) by Artanis (Erik Youngren <artanis.00@gmail.com>) from the 'tagger' extension - Used with permission";
|
||||
|
@ -42,7 +42,7 @@ class BrowserSearch extends Extension
|
||||
|
||||
// And now to send it to the browser
|
||||
$page->set_mode(PageMode::DATA);
|
||||
$page->set_mime(MimeType::XML);
|
||||
$page->set_type(MIME_TYPE_XML);
|
||||
$page->set_data($xml);
|
||||
} elseif ($event->page_matches("browser_search")) {
|
||||
$suggestions = $config->get_string("search_suggestions_results_order");
|
||||
@ -78,7 +78,8 @@ class BrowserSearch extends Extension
|
||||
$sort_by['Tag Count'] = 't';
|
||||
$sort_by['Disabled'] = 'n';
|
||||
|
||||
$sb = $event->panel->create_new_block("Browser Search");
|
||||
$sb = new SetupBlock("Browser Search");
|
||||
$sb->add_choice_option("search_suggestions_results_order", $sort_by, "Sort the suggestions by:");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ class BulkActionsInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bulk_actions";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Bulk Actions";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Provides query and selection-based bulk action support";
|
||||
public ?string $documentation = "Provides bulk action section in list view. Allows performing actions against a set of posts based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
|
||||
public $key = self::KEY;
|
||||
public $name = "Bulk Actions";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Provides query and selection-based bulk action support";
|
||||
public $documentation = "Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa.";
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ class BulkActionException extends SCoreException
|
||||
}
|
||||
class BulkActionBlockBuildingEvent extends Event
|
||||
{
|
||||
public array $actions = [];
|
||||
public array $search_terms = [];
|
||||
/** @var array */
|
||||
public $actions = [];
|
||||
|
||||
public $search_terms = [];
|
||||
|
||||
public function add_action(String $action, string $button_text, string $access_key = null, String $confirmation_message = "", String $block = "", int $position = 40)
|
||||
{
|
||||
@ -36,9 +38,12 @@ class BulkActionBlockBuildingEvent extends Event
|
||||
|
||||
class BulkActionEvent extends Event
|
||||
{
|
||||
public string $action;
|
||||
public Generator $items;
|
||||
public bool $redirect = true;
|
||||
/** @var string */
|
||||
public $action;
|
||||
/** @var array */
|
||||
public $items;
|
||||
/** @var bool */
|
||||
public $redirect = true;
|
||||
|
||||
public function __construct(String $action, Generator $items)
|
||||
{
|
||||
@ -51,7 +56,7 @@ class BulkActionEvent extends Event
|
||||
class BulkActions extends Extension
|
||||
{
|
||||
/** @var BulkActionsTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPostListBuilding(PostListBuildingEvent $event)
|
||||
{
|
||||
@ -122,8 +127,8 @@ class BulkActions extends Extension
|
||||
switch ($event->action) {
|
||||
case "bulk_delete":
|
||||
if ($user->can(Permissions::DELETE_IMAGE)) {
|
||||
$i = $this->delete_posts($event->items);
|
||||
$page->flash("Deleted $i[0] items, totaling ".human_filesize($i[1]));
|
||||
$i = $this->delete_items($event->items);
|
||||
$page->flash("Deleted $i items");
|
||||
}
|
||||
break;
|
||||
case "bulk_tag":
|
||||
@ -188,14 +193,14 @@ class BulkActions extends Extension
|
||||
if (is_iterable($items)) {
|
||||
send_event($bae);
|
||||
}
|
||||
|
||||
if ($bae->redirect) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(referer_or(make_link()));
|
||||
}
|
||||
} catch (BulkActionException $e) {
|
||||
log_error(BulkActionsInfo::KEY, $e->getMessage(), $e->getMessage());
|
||||
}
|
||||
|
||||
if ($bae->redirect) {
|
||||
$page->set_mode(PageMode::REDIRECT);
|
||||
$page->set_redirect(referer_or(make_link()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,27 +227,25 @@ class BulkActions extends Extension
|
||||
return $a["position"] - $b["position"];
|
||||
}
|
||||
|
||||
private function delete_posts(iterable $posts): array
|
||||
private function delete_items(iterable $items): int
|
||||
{
|
||||
global $page;
|
||||
$total = 0;
|
||||
$size = 0;
|
||||
foreach ($posts as $post) {
|
||||
foreach ($items as $image) {
|
||||
try {
|
||||
if (class_exists("ImageBan") && isset($_POST['bulk_ban_reason'])) {
|
||||
$reason = $_POST['bulk_ban_reason'];
|
||||
if ($reason) {
|
||||
send_event(new AddImageHashBanEvent($post->hash, $reason));
|
||||
send_event(new AddImageHashBanEvent($image->hash, $reason));
|
||||
}
|
||||
}
|
||||
send_event(new ImageDeletionEvent($post));
|
||||
send_event(new ImageDeletionEvent($image));
|
||||
$total++;
|
||||
$size += $post->filesize;
|
||||
} catch (Exception $e) {
|
||||
$page->flash("Error while removing {$post->id}: " . $e->getMessage());
|
||||
$page->flash("Error while removing {$image->id}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return [$total, $size];
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function tag_items(iterable $items, string $tags, bool $replace): int
|
||||
@ -252,7 +255,7 @@ class BulkActions extends Extension
|
||||
$pos_tag_array = [];
|
||||
$neg_tag_array = [];
|
||||
foreach ($tags as $new_tag) {
|
||||
if (str_starts_with($new_tag, '-')) {
|
||||
if (strpos($new_tag, '-') === 0) {
|
||||
$neg_tag_array[] = substr($new_tag, 1);
|
||||
} else {
|
||||
$pos_tag_array[] = $new_tag;
|
||||
|
@ -8,13 +8,13 @@ function validate_selections(form, confirmationMessage) {
|
||||
var queryOnly = false;
|
||||
if(bulk_selector_active) {
|
||||
var data = get_selected_items();
|
||||
if(data.length===0) {
|
||||
if(data.length==0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
var query = $(form).find('input[name="bulk_query"]').val();
|
||||
|
||||
if (query == null || query === "") {
|
||||
if (query == null || query == "") {
|
||||
return false;
|
||||
} else {
|
||||
queryOnly = true;
|
||||
@ -22,7 +22,7 @@ function validate_selections(form, confirmationMessage) {
|
||||
}
|
||||
|
||||
|
||||
if(confirmationMessage!=null&&confirmationMessage!=="") {
|
||||
if(confirmationMessage!=null&&confirmationMessage!="") {
|
||||
return confirm(confirmationMessage);
|
||||
} else if(queryOnly) {
|
||||
var action = $(form).find('input[name="submit_button"]').val();
|
||||
@ -59,7 +59,7 @@ function deactivate_bulk_selector() {
|
||||
|
||||
function get_selected_items() {
|
||||
var data = $('#bulk_selected_ids').val();
|
||||
if(data===""||data==null) {
|
||||
if(data==""||data==null) {
|
||||
data = [];
|
||||
} else {
|
||||
data = JSON.parse(data);
|
||||
@ -97,11 +97,11 @@ function toggle_selection( id ) {
|
||||
var data = get_selected_items();
|
||||
if(data.includes(id)) {
|
||||
data.splice(data.indexOf(id),1);
|
||||
set_selected_items(data);
|
||||
set_selected_items(data);
|
||||
return false;
|
||||
} else {
|
||||
data.push(id);
|
||||
set_selected_items(data);
|
||||
set_selected_items(data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -116,7 +116,7 @@ function select_all() {
|
||||
items.push(id);
|
||||
}
|
||||
);
|
||||
set_selected_items(items);
|
||||
set_selected_items(items);
|
||||
}
|
||||
|
||||
function select_invert() {
|
||||
@ -131,11 +131,11 @@ function select_invert() {
|
||||
}
|
||||
}
|
||||
);
|
||||
set_selected_items(items);
|
||||
set_selected_items(items);
|
||||
}
|
||||
|
||||
function select_none() {
|
||||
set_selected_items([]);
|
||||
set_selected_items([]);
|
||||
}
|
||||
|
||||
function select_range(start, end) {
|
||||
@ -145,7 +145,7 @@ function select_range(start, end) {
|
||||
function ( index, block ) {
|
||||
block = $(block);
|
||||
var id = block.data("post-id");
|
||||
if(id===start)
|
||||
if(id==start)
|
||||
selecting = true;
|
||||
|
||||
if(selecting) {
|
||||
@ -153,7 +153,7 @@ function select_range(start, end) {
|
||||
data.push(id);
|
||||
}
|
||||
|
||||
if(id===end) {
|
||||
if(id==end) {
|
||||
selecting = false;
|
||||
}
|
||||
}
|
||||
@ -163,14 +163,14 @@ function select_range(start, end) {
|
||||
|
||||
var last_clicked_item;
|
||||
|
||||
function add_selector_button($block) {
|
||||
function add_selector_button($block) {
|
||||
var c = function(e) {
|
||||
if(!bulk_selector_active)
|
||||
return true;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
var id = $block.data("post-id");
|
||||
if(e.shiftKey) {
|
||||
if(last_clicked_item<id) {
|
||||
@ -182,7 +182,7 @@ function add_selector_button($block) {
|
||||
last_clicked_item = id;
|
||||
toggle_selection(id);
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
|
||||
$block.find("A").click(c);
|
||||
|
@ -8,7 +8,7 @@ class BulkActionsTheme extends Themelet
|
||||
<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate (M)anual Select' accesskey='m'/>
|
||||
<div id='bulk_selector_controls' style='display: none;'>
|
||||
<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate (M)anual Select' accesskey='m'/>
|
||||
Click on posts to mark them.
|
||||
Click on images to mark them.
|
||||
<br />
|
||||
<table><tr><td>
|
||||
<input id='bulk_selector_select_all' type='button'
|
||||
@ -45,7 +45,7 @@ class BulkActionsTheme extends Themelet
|
||||
$page->add_block($block);
|
||||
}
|
||||
|
||||
public function render_ban_reason_input(): string
|
||||
public function render_ban_reason_input()
|
||||
{
|
||||
if (class_exists("ImageBan")) {
|
||||
return "<input type='text' name='bulk_ban_reason' placeholder='Ban reason (leave blank to not ban)' />";
|
||||
@ -54,13 +54,13 @@ class BulkActionsTheme extends Themelet
|
||||
}
|
||||
}
|
||||
|
||||
public function render_tag_input(): string
|
||||
public function render_tag_input()
|
||||
{
|
||||
return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" .
|
||||
"<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />";
|
||||
}
|
||||
|
||||
public function render_source_input(): string
|
||||
public function render_source_input()
|
||||
{
|
||||
return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />";
|
||||
}
|
||||
|
@ -4,13 +4,13 @@ class BulkAddInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bulk_add";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Bulk Add";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Bulk add server-side images";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Bulk Add";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Bulk add server-side images";
|
||||
public $documentation =
|
||||
"Upload the images into a new directory via ftp or similar, go to
|
||||
shimmie's admin page and put that directory in the bulk add box.
|
||||
If there are subdirectories, they get used as tags (eg if you
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
class BulkAddEvent extends Event
|
||||
{
|
||||
public string $dir;
|
||||
public array $results;
|
||||
public $dir;
|
||||
public $results;
|
||||
|
||||
public function __construct(string $dir)
|
||||
{
|
||||
@ -16,7 +16,7 @@ class BulkAddEvent extends Event
|
||||
class BulkAdd extends Extension
|
||||
{
|
||||
/** @var BulkAddTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ class BulkAddTest extends ShimmiePHPUnitTestCase
|
||||
{
|
||||
send_event(new UserLoginEvent(User::by_name(self::$admin_name)));
|
||||
$bae = send_event(new BulkAddEvent('asdf'));
|
||||
$this->assertContainsEquals(
|
||||
$this->assertContains(
|
||||
"Error, asdf is not a readable directory",
|
||||
$bae->results,
|
||||
implode("\n", $bae->results)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class BulkAddTheme extends Themelet
|
||||
{
|
||||
private array $messages = [];
|
||||
private $messages = [];
|
||||
|
||||
/*
|
||||
* Show a standard page for results to be put into
|
||||
|
@ -4,21 +4,21 @@ class BulkAddCSVInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bulk_add_csv";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Bulk Add CSV";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = ["velocity37"=>"velocity37@gmail.com"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Bulk add server-side posts with metadata from CSV file";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Bulk Add CSV";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = ["velocity37"=>"velocity37@gmail.com"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Bulk add server-side images with metadata from CSV file";
|
||||
public $documentation =
|
||||
"Modification of \"Bulk Add\" by Shish.<br><br>
|
||||
Adds posts from a CSV with the five following values: <br>
|
||||
Adds images from a CSV with the five following values: <br>
|
||||
\"/path/to/image.jpg\",\"spaced tags\",\"source\",\"rating s/q/e\",\"/path/thumbnail.jpg\" <br>
|
||||
<b>e.g.</b> \"/tmp/cat.png\",\"shish oekaki\",\"shimmie.shishnet.org\",\"s\",\"tmp/custom.jpg\" <br><br>
|
||||
Any value but the first may be omitted, but there must be five values per line.<br>
|
||||
<b>e.g.</b> \"/why/not/try/bulk_add.jpg\",\"\",\"\",\"\",\"\"<br><br>
|
||||
Post thumbnails will be displayed at the AR of the full post. Thumbnails that are
|
||||
Image thumbnails will be displayed at the AR of the full image. Thumbnails that are
|
||||
normally static (e.g. SWF) will be displayed at the board's max thumbnail size<br><br>
|
||||
Useful for importing tagged posts without having to do database manipulation.<br>
|
||||
<p><b>Note:</b> requires \"Admin Controls\" and optionally \"Post Ratings\" to be enabled<br><br>";
|
||||
Useful for importing tagged images without having to do database manipulation.<br>
|
||||
<p><b>Note:</b> requires \"Admin Controls\" and optionally \"Image Ratings\" to be enabled<br><br>";
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
class BulkAddCSV extends Extension
|
||||
{
|
||||
/** @var BulkAddCSVTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
{
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
class BulkAddCSVTheme extends Themelet
|
||||
{
|
||||
private array $messages = [];
|
||||
private $messages = [];
|
||||
|
||||
/*
|
||||
* Show a standard page for results to be put into
|
||||
*/
|
||||
public function display_upload_results(Page $page)
|
||||
{
|
||||
$page->set_title("Adding posts from csv");
|
||||
$page->set_heading("Adding posts from csv");
|
||||
$page->set_title("Adding images from csv");
|
||||
$page->set_heading("Adding images from csv");
|
||||
$page->add_block(new NavBlock());
|
||||
foreach ($this->messages as $block) {
|
||||
$page->add_block($block);
|
||||
@ -26,8 +26,8 @@ class BulkAddCSVTheme extends Themelet
|
||||
{
|
||||
global $page;
|
||||
$html = "
|
||||
Add posts from a csv. Posts will be tagged and have their
|
||||
source and rating set (if \"Post Ratings\" is enabled)
|
||||
Add images from a csv. Images will be tagged and have their
|
||||
source and rating set (if \"Image Ratings\" is enabled)
|
||||
<br>Specify the absolute or relative path to a local .csv file. Check <a href=\"" . make_link("ext_doc/bulk_add_csv") . "\">here</a> for the expected format.
|
||||
|
||||
<p>".make_form(make_link("bulk_add_csv"))."
|
||||
|
@ -5,10 +5,10 @@ class BulkDownloadInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bulk_download";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Bulk Download";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Allows bulk downloading images.";
|
||||
public array $dependencies = [BulkActionsInfo::KEY];
|
||||
public $key = self::KEY;
|
||||
public $name = "Bulk Download";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Allows bulk downloading images.";
|
||||
public $dependencies = [BulkActionsInfo::KEY];
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class BulkDownload extends Extension
|
||||
|
||||
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
global $user, $config;
|
||||
|
||||
if ($user->can(Permissions::BULK_DOWNLOAD)) {
|
||||
$event->add_action(BulkDownload::DOWNLOAD_ACTION_NAME, "Download ZIP");
|
||||
@ -30,11 +30,13 @@ class BulkDownload extends Extension
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Bulk Download");
|
||||
$sb = new SetupBlock("Bulk Download");
|
||||
|
||||
$sb->start_table();
|
||||
$sb->add_shorthand_int_option(BulkDownloadConfig::SIZE_LIMIT, "Size Limit", true);
|
||||
$sb->end_table();
|
||||
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onBulkAction(BulkActionEvent $event)
|
||||
|
@ -2,12 +2,11 @@
|
||||
|
||||
class BulkExportEvent extends Event
|
||||
{
|
||||
public Image $image;
|
||||
public array $fields = [];
|
||||
public $image;
|
||||
public $fields = [];
|
||||
|
||||
public function __construct(Image $image)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->image = $image;
|
||||
}
|
||||
}
|
||||
@ -15,12 +14,11 @@ class BulkExportEvent extends Event
|
||||
|
||||
class BulkImportEvent extends Event
|
||||
{
|
||||
public Image $image;
|
||||
public array $fields = [];
|
||||
public $image;
|
||||
public $fields = [];
|
||||
|
||||
public function __construct(Image $image, $fields)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->image = $image;
|
||||
$this->fields = $fields;
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ class BulkImportExportInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "bulk_import_export";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Bulk Import/Export";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "Allows bulk exporting/importing of images and associated data.";
|
||||
public array $dependencies = [BulkActionsInfo::KEY];
|
||||
public $key = self::KEY;
|
||||
public $name = "Bulk Import/Export";
|
||||
public $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_WTFPL;
|
||||
public $description = "Allows bulk exporting/importing of images and associated data.";
|
||||
public $dependencies = [BulkActionsInfo::KEY];
|
||||
}
|
||||
|
@ -5,24 +5,30 @@ class BulkImportExport extends DataHandlerExtension
|
||||
{
|
||||
const EXPORT_ACTION_NAME = "bulk_export";
|
||||
const EXPORT_INFO_FILE_NAME = "export.json";
|
||||
protected array $SUPPORTED_MIME = [MimeType::ZIP];
|
||||
protected $SUPPORTED_MIME = [MIME_TYPE_ZIP];
|
||||
|
||||
|
||||
public function onDataUpload(DataUploadEvent $event)
|
||||
{
|
||||
global $user, $database;
|
||||
|
||||
if ($this->supported_mime($event->mime) &&
|
||||
if ($this->supported_ext($event->type) &&
|
||||
$user->can(Permissions::BULK_IMPORT)) {
|
||||
$zip = new ZipArchive;
|
||||
|
||||
if ($zip->open($event->tmpname) === true) {
|
||||
$json_data = $this->get_export_data($zip);
|
||||
|
||||
if (empty($json_data)) {
|
||||
return;
|
||||
$info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
|
||||
$json_data = [];
|
||||
if ($info !== false) {
|
||||
try {
|
||||
$json_string = stream_get_contents($info);
|
||||
$json_data = json_decode($json_string);
|
||||
} finally {
|
||||
fclose($info);
|
||||
}
|
||||
} else {
|
||||
throw new SCoreException("Could not get " . self::EXPORT_INFO_FILE_NAME . " from archive");
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
@ -36,7 +42,7 @@ class BulkImportExport extends DataHandlerExtension
|
||||
$image = Image::by_hash($item->hash);
|
||||
if ($image!=null) {
|
||||
$skipped++;
|
||||
log_info(BulkImportExportInfo::KEY, "Post $item->hash already present, skipping");
|
||||
log_info(BulkImportExportInfo::KEY, "Image $item->hash already present, skipping");
|
||||
$database->commit();
|
||||
continue;
|
||||
}
|
||||
@ -100,7 +106,7 @@ class BulkImportExport extends DataHandlerExtension
|
||||
|
||||
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
|
||||
{
|
||||
global $user;
|
||||
global $user, $config;
|
||||
|
||||
if ($user->can(Permissions::BULK_EXPORT)) {
|
||||
$event->add_action(self::EXPORT_ACTION_NAME, "Export");
|
||||
@ -159,24 +165,8 @@ class BulkImportExport extends DataHandlerExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function create_thumb(string $hash, string $mime): bool
|
||||
protected function create_thumb(string $hash, string $type): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function get_export_data(ZipArchive $zip): ?array
|
||||
{
|
||||
$info = $zip->getStream(self::EXPORT_INFO_FILE_NAME);
|
||||
if ($info !== false) {
|
||||
try {
|
||||
$json_string = stream_get_contents($info);
|
||||
$json_data = json_decode($json_string);
|
||||
} finally {
|
||||
fclose($info);
|
||||
}
|
||||
return $json_data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ class CommentListInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "comment";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Post Comments";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Allow users to make comments on images";
|
||||
public ?string $documentation = "Formatting is done with the standard formatting API (normally BBCode)";
|
||||
public bool $core = true;
|
||||
public $key = self::KEY;
|
||||
public $name = "Image Comments";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Allow users to make comments on images";
|
||||
public $documentation = "Formatting is done with the standard formatting API (normally BBCode)";
|
||||
public $core = true;
|
||||
}
|
||||
|
@ -4,9 +4,12 @@ require_once "vendor/ifixit/php-akismet/akismet.class.php";
|
||||
|
||||
class CommentPostingEvent extends Event
|
||||
{
|
||||
public int $image_id;
|
||||
public User $user;
|
||||
public string $comment;
|
||||
/** @var int */
|
||||
public $image_id;
|
||||
/** @var User */
|
||||
public $user;
|
||||
/** @var string */
|
||||
public $comment;
|
||||
|
||||
public function __construct(int $image_id, User $user, string $comment)
|
||||
{
|
||||
@ -24,7 +27,8 @@ class CommentPostingEvent extends Event
|
||||
*/
|
||||
class CommentDeletionEvent extends Event
|
||||
{
|
||||
public int $comment_id;
|
||||
/** @var int */
|
||||
public $comment_id;
|
||||
|
||||
public function __construct(int $comment_id)
|
||||
{
|
||||
@ -39,27 +43,46 @@ class CommentPostingException extends SCoreException
|
||||
|
||||
class Comment
|
||||
{
|
||||
public ?User $owner;
|
||||
public int $owner_id;
|
||||
public string $owner_name;
|
||||
public ?string $owner_email;
|
||||
public string $owner_class;
|
||||
public string $comment;
|
||||
public int $comment_id;
|
||||
public int $image_id;
|
||||
public string $poster_ip;
|
||||
public string $posted;
|
||||
/** @var User */
|
||||
public $owner;
|
||||
|
||||
/** @var int */
|
||||
public $owner_id;
|
||||
|
||||
/** @var string */
|
||||
public $owner_name;
|
||||
|
||||
/** @var string */
|
||||
public $owner_email;
|
||||
|
||||
/** @var string */
|
||||
public $owner_class;
|
||||
|
||||
/** @var string */
|
||||
public $comment;
|
||||
|
||||
/** @var int */
|
||||
public $comment_id;
|
||||
|
||||
/** @var int */
|
||||
public $image_id;
|
||||
|
||||
/** @var string */
|
||||
public $poster_ip;
|
||||
|
||||
/** @var string */
|
||||
public $posted;
|
||||
|
||||
public function __construct($row)
|
||||
{
|
||||
$this->owner = null;
|
||||
$this->owner_id = (int)$row['user_id'];
|
||||
$this->owner_id = $row['user_id'];
|
||||
$this->owner_name = $row['user_name'];
|
||||
$this->owner_email = $row['user_email']; // deprecated
|
||||
$this->owner_class = $row['user_class'];
|
||||
$this->comment = $row['comment'];
|
||||
$this->comment_id = (int)$row['comment_id'];
|
||||
$this->image_id = (int)$row['image_id'];
|
||||
$this->comment_id = $row['comment_id'];
|
||||
$this->image_id = $row['image_id'];
|
||||
$this->poster_ip = $row['poster_ip'];
|
||||
$this->posted = $row['posted'];
|
||||
}
|
||||
@ -86,7 +109,7 @@ class Comment
|
||||
class CommentList extends Extension
|
||||
{
|
||||
/** @var CommentListTheme $theme */
|
||||
public ?Themelet $theme;
|
||||
public $theme;
|
||||
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
@ -135,15 +158,15 @@ class CommentList extends Extension
|
||||
}
|
||||
|
||||
if ($this->get_version("ext_comments_version") == 1) {
|
||||
$database->execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)");
|
||||
$database->execute("CREATE INDEX comments_posted ON comments(posted)");
|
||||
$database->Execute("CREATE INDEX comments_owner_ip ON comments(owner_ip)");
|
||||
$database->Execute("CREATE INDEX comments_posted ON comments(posted)");
|
||||
$this->set_version("ext_comments_version", 2);
|
||||
}
|
||||
|
||||
if ($this->get_version("ext_comments_version") == 2) {
|
||||
$this->set_version("ext_comments_version", 3);
|
||||
$database->execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE");
|
||||
$database->execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT");
|
||||
$database->Execute("ALTER TABLE comments ADD FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE");
|
||||
$database->Execute("ALTER TABLE comments ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT");
|
||||
}
|
||||
|
||||
// FIXME: add foreign keys, bump to v3
|
||||
@ -243,7 +266,7 @@ class CommentList extends Extension
|
||||
|
||||
$total_pages = $cache->get("comment_pages");
|
||||
if (empty($total_pages)) {
|
||||
$total_pages = (int)ceil($database->get_one("
|
||||
$total_pages = (int)($database->get_one("
|
||||
SELECT COUNT(c1)
|
||||
FROM (SELECT COUNT(image_id) AS c1 FROM comments $where GROUP BY image_id) AS s1
|
||||
") / 10);
|
||||
@ -255,7 +278,7 @@ class CommentList extends Extension
|
||||
$threads_per_page = 10;
|
||||
$start = $threads_per_page * $current_page;
|
||||
|
||||
$result = $database->execute("
|
||||
$result = $database->Execute("
|
||||
SELECT image_id,MAX(posted) AS latest
|
||||
FROM comments
|
||||
$where
|
||||
@ -347,7 +370,7 @@ class CommentList extends Extension
|
||||
public function onCommentDeletion(CommentDeletionEvent $event)
|
||||
{
|
||||
global $database;
|
||||
$database->execute("
|
||||
$database->Execute("
|
||||
DELETE FROM comments
|
||||
WHERE id=:comment_id
|
||||
", ["comment_id"=>$event->comment_id]);
|
||||
@ -356,7 +379,7 @@ class CommentList extends Extension
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Comment Options");
|
||||
$sb = new SetupBlock("Comment Options");
|
||||
$sb->add_bool_option("comment_captcha", "Require CAPTCHA for anonymous comments: ");
|
||||
$sb->add_label("<br>Limit to ");
|
||||
$sb->add_int_option("comment_limit");
|
||||
@ -371,6 +394,7 @@ class CommentList extends Extension
|
||||
$sb->add_label(" comments per image on the list");
|
||||
$sb->add_label("<br>Make samefags public ");
|
||||
$sb->add_bool_option("comment_samefags_public");
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onSearchTermParse(SearchTermParseEvent $event)
|
||||
@ -571,7 +595,7 @@ class CommentList extends Extension
|
||||
if ($user->is_anonymous()) {
|
||||
$page->add_cookie("nocache", "Anonymous Commenter", time()+60*60*24, "/");
|
||||
}
|
||||
$database->execute(
|
||||
$database->Execute(
|
||||
"INSERT INTO comments(image_id, owner_id, owner_ip, posted, comment) ".
|
||||
"VALUES(:image_id, :user_id, :remote_addr, now(), :comment)",
|
||||
["image_id"=>$image_id, "user_id"=>$user->id, "remote_addr"=>$_SERVER['REMOTE_ADDR'], "comment"=>$comment]
|
||||
@ -580,7 +604,7 @@ class CommentList extends Extension
|
||||
$snippet = substr($comment, 0, 100);
|
||||
$snippet = str_replace("\n", " ", $snippet);
|
||||
$snippet = str_replace("\r", " ", $snippet);
|
||||
log_info("comment", "Comment #$cid added to >>$image_id: $snippet");
|
||||
log_info("comment", "Comment #$cid added to Image #$image_id: $snippet");
|
||||
}
|
||||
|
||||
private function comment_checks(int $image_id, User $user, string $comment)
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?php declare(strict_types=1);
|
||||
class CommentListTheme extends Themelet
|
||||
{
|
||||
private bool $show_anon_id = false;
|
||||
private int $anon_id = 1;
|
||||
private array $anon_map = [];
|
||||
private $show_anon_id = false;
|
||||
private $anon_id = 1;
|
||||
private $anon_map = [];
|
||||
|
||||
/**
|
||||
* Display a page with a list of images, and for each image, the image's comments.
|
||||
@ -86,6 +86,7 @@ class CommentListTheme extends Themelet
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function display_admin_block()
|
||||
{
|
||||
global $page;
|
||||
@ -103,6 +104,7 @@ class CommentListTheme extends Themelet
|
||||
$page->add_block(new Block("Mass Comment Delete", $html));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add some comments to the page, probably in a sidebar.
|
||||
*
|
||||
@ -120,6 +122,7 @@ class CommentListTheme extends Themelet
|
||||
$page->add_block(new Block("Comments", $html, "left", 50, "comment-list-recent"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show comments for an image.
|
||||
*
|
||||
@ -139,6 +142,7 @@ class CommentListTheme extends Themelet
|
||||
$page->add_block(new Block("Comments", $html, "main", 30, "comment-list-image"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show comments made by a user.
|
||||
*
|
||||
@ -283,25 +287,25 @@ class CommentListTheme extends Themelet
|
||||
';
|
||||
}
|
||||
|
||||
public function get_help_html(): string
|
||||
public function get_help_html()
|
||||
{
|
||||
return '<p>Search for posts containing a certain number of comments, or comments by a particular individual.</p>
|
||||
return '<p>Search for images containing a certain number of comments, or comments by a particular individual.</p>
|
||||
<div class="command_example">
|
||||
<pre>comments=1</pre>
|
||||
<p>Returns posts with exactly 1 comment.</p>
|
||||
<p>Returns images with exactly 1 comment.</p>
|
||||
</div>
|
||||
<div class="command_example">
|
||||
<pre>comments>0</pre>
|
||||
<p>Returns posts with 1 or more comments. </p>
|
||||
<p>Returns images with 1 or more comments. </p>
|
||||
</div>
|
||||
<p>Can use <, <=, >, >=, or =.</p>
|
||||
<div class="command_example">
|
||||
<pre>commented_by:username</pre>
|
||||
<p>Returns posts that have been commented on by "username". </p>
|
||||
<p>Returns images that have been commented on by "username". </p>
|
||||
</div>
|
||||
<div class="command_example">
|
||||
<pre>commented_by_userno:123</pre>
|
||||
<p>Returns posts that have been commented on by user 123. </p>
|
||||
<p>Returns images that have been commented on by user 123. </p>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
@ -5,8 +5,66 @@ abstract class CronUploaderConfig
|
||||
{
|
||||
public const DEFAULT_PATH = "cron_uploader";
|
||||
|
||||
public const KEY = "cron_uploader_key";
|
||||
public const DIR = "cron_uploader_dir";
|
||||
public const USER = "cron_uploader_user";
|
||||
public const STOP_ON_ERROR = "cron_uploader_stop_on_error";
|
||||
public const INCLUDE_ALL_LOGS = "cron_uploader_include_all_logs";
|
||||
public const LOG_LEVEL = "cron_uploader_log_level";
|
||||
|
||||
public static function set_defaults(): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_default_string(self::DIR, data_path(self::DEFAULT_PATH));
|
||||
$config->set_default_bool(self::INCLUDE_ALL_LOGS, false);
|
||||
$config->set_default_bool(self::STOP_ON_ERROR, false);
|
||||
$config->set_default_int(self::LOG_LEVEL, SCORE_LOG_INFO);
|
||||
$upload_key = $config->get_string(self::KEY, "");
|
||||
if (empty($upload_key)) {
|
||||
$upload_key = generate_key();
|
||||
|
||||
$config->set_string(self::KEY, $upload_key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_user(): int
|
||||
{
|
||||
global $config;
|
||||
return $config->get_int(self::USER);
|
||||
}
|
||||
|
||||
public static function set_user(int $value): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_int(self::USER, $value);
|
||||
}
|
||||
|
||||
public static function get_key(): string
|
||||
{
|
||||
global $config;
|
||||
return $config->get_string(self::KEY);
|
||||
}
|
||||
|
||||
public static function set_key(string $value): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_string(self::KEY, $value);
|
||||
}
|
||||
|
||||
public static function get_dir(): string
|
||||
{
|
||||
global $config;
|
||||
$value = $config->get_string(self::DIR);
|
||||
if (empty($value)) {
|
||||
$value = data_path("cron_uploader");
|
||||
self::set_dir($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function set_dir(string $value): void
|
||||
{
|
||||
global $config;
|
||||
$config->set_string(self::DIR, $value);
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,12 @@ class CronUploaderInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "cron_uploader";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Cron Uploader";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = ["YaoiFox"=>"admin@yaoifox.com", "Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Uploads images automatically using Cron Jobs";
|
||||
public $key = self::KEY;
|
||||
public $name = "Cron Uploader";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = ["YaoiFox"=>"admin@yaoifox.com", "Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Uploads images automatically using Cron Jobs";
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ require_once "config.php";
|
||||
class CronUploader extends Extension
|
||||
{
|
||||
/** @var CronUploaderTheme */
|
||||
protected ?Themelet $theme;
|
||||
protected $theme;
|
||||
|
||||
public const NAME = "cron_uploader";
|
||||
|
||||
@ -15,42 +15,14 @@ class CronUploader extends Extension
|
||||
const UPLOADED_DIR = "uploaded";
|
||||
const FAILED_DIR = "failed_to_upload";
|
||||
|
||||
private static bool $IMPORT_RUNNING = false;
|
||||
private static $IMPORT_RUNNING = false;
|
||||
|
||||
public function onInitUserConfig(InitUserConfigEvent $event)
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
{
|
||||
$event->user_config->set_default_string(
|
||||
CronUploaderConfig::DIR,
|
||||
data_path(CronUploaderConfig::DEFAULT_PATH.DIRECTORY_SEPARATOR.$event->user->name)
|
||||
);
|
||||
$event->user_config->set_default_bool(CronUploaderConfig::INCLUDE_ALL_LOGS, false);
|
||||
$event->user_config->set_default_bool(CronUploaderConfig::STOP_ON_ERROR, false);
|
||||
$event->user_config->set_default_int(CronUploaderConfig::LOG_LEVEL, SCORE_LOG_INFO);
|
||||
// Set default values
|
||||
CronUploaderConfig::set_defaults();
|
||||
}
|
||||
|
||||
public function onUserOptionsBuilding(UserOptionsBuildingEvent $event)
|
||||
{
|
||||
if ($event->user->can(Permissions::CRON_ADMIN)) {
|
||||
$documentation_link = make_http(make_link("cron_upload"));
|
||||
|
||||
$sb = $event->panel->create_new_block("Cron Uploader");
|
||||
$sb->start_table();
|
||||
$sb->add_text_option(CronUploaderConfig::DIR, "Root dir", true);
|
||||
$sb->add_bool_option(CronUploaderConfig::STOP_ON_ERROR, "Stop On Error", true);
|
||||
$sb->add_choice_option(CronUploaderConfig::LOG_LEVEL, [
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
|
||||
], "Output Log Level: ", true);
|
||||
$sb->add_bool_option(CronUploaderConfig::INCLUDE_ALL_LOGS, "Include All Logs", true);
|
||||
$sb->end_table();
|
||||
$sb->add_label("<a href='$documentation_link'>Read the documentation</a> for cron setup instructions.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function onPageSubNavBuilding(PageSubNavBuildingEvent $event)
|
||||
{
|
||||
if ($event->parent=="system") {
|
||||
@ -67,14 +39,42 @@ class CronUploader extends Extension
|
||||
global $user;
|
||||
|
||||
if ($event->page_matches("cron_upload")) {
|
||||
if ($event->count_args() == 1 && $event->get_arg(0) =="run") {
|
||||
$this->process_upload(); // Start upload
|
||||
} elseif ($user->can(Permissions::CRON_RUN)) {
|
||||
if ($event->count_args() == 1) {
|
||||
$this->process_upload($event->get_arg(0)); // Start upload
|
||||
} elseif ($user->can(Permissions::CRON_ADMIN)) {
|
||||
$this->display_documentation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
global $database;
|
||||
|
||||
$documentation_link = make_http(make_link("cron_upload"));
|
||||
|
||||
$users = $database->get_pairs("SELECT name, id FROM users UNION ALL SELECT '', null order by name");
|
||||
|
||||
$sb = new SetupBlock("Cron Uploader");
|
||||
$sb->start_table();
|
||||
$sb->add_text_option(CronUploaderConfig::DIR, "Root dir", true);
|
||||
$sb->add_text_option(CronUploaderConfig::KEY, "Key", true);
|
||||
$sb->add_choice_option(CronUploaderConfig::USER, $users, "User", true);
|
||||
$sb->add_bool_option(CronUploaderConfig::STOP_ON_ERROR, "Stop On Error", true);
|
||||
$sb->add_choice_option(CronUploaderConfig::LOG_LEVEL, [
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_DEBUG] => SCORE_LOG_DEBUG,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_INFO] => SCORE_LOG_INFO,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_WARNING] => SCORE_LOG_WARNING,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_ERROR] => SCORE_LOG_ERROR,
|
||||
LOGGING_LEVEL_NAMES[SCORE_LOG_CRITICAL] => SCORE_LOG_CRITICAL,
|
||||
], "Output Log Level: ", true);
|
||||
$sb->add_bool_option(CronUploaderConfig::INCLUDE_ALL_LOGS, "Include All Logs", true);
|
||||
$sb->end_table();
|
||||
$sb->add_label("<a href='$documentation_link'>Read the documentation</a> for cron setup instructions.");
|
||||
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onAdminBuilding(AdminBuildingEvent $event)
|
||||
{
|
||||
$failed_dir = $this->get_failed_dir();
|
||||
@ -118,20 +118,19 @@ class CronUploader extends Extension
|
||||
|
||||
public function onLog(LogEvent $event)
|
||||
{
|
||||
global $user_config;
|
||||
global $config;
|
||||
$all = $config->get_bool(CronUploaderConfig::INCLUDE_ALL_LOGS);
|
||||
if (self::$IMPORT_RUNNING &&
|
||||
$event->priority >= $config->get_int(CronUploaderConfig::LOG_LEVEL) &&
|
||||
($event->section==self::NAME || $all)
|
||||
) {
|
||||
$output = "[" . date('Y-m-d H:i:s') . "] " . ($all ? '['. $event->section .'] ' :'') . "[" . LOGGING_LEVEL_NAMES[$event->priority] . "] " . $event->message ;
|
||||
|
||||
if (self::$IMPORT_RUNNING) {
|
||||
$all = $user_config->get_bool(CronUploaderConfig::INCLUDE_ALL_LOGS);
|
||||
if ($event->priority >= $user_config->get_int(CronUploaderConfig::LOG_LEVEL) &&
|
||||
($event->section==self::NAME || $all)) {
|
||||
$output = "[" . date('Y-m-d H:i:s') . "] " . ($all ? '[' . $event->section . '] ' : '') . "[" . LOGGING_LEVEL_NAMES[$event->priority] . "] " . $event->message;
|
||||
echo $output . "\r\n";
|
||||
flush_output();
|
||||
|
||||
echo $output . "\r\n";
|
||||
flush_output();
|
||||
|
||||
$log_path = $this->get_log_file();
|
||||
file_put_contents($log_path, $output);
|
||||
}
|
||||
$log_path = $this->get_log_file();
|
||||
file_put_contents($log_path, $output);
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,28 +193,24 @@ class CronUploader extends Extension
|
||||
|
||||
private function clear_folder($folder)
|
||||
{
|
||||
global $page, $user_config;
|
||||
$path = join_path($user_config->get_string(CronUploaderConfig::DIR), $folder);
|
||||
global $page;
|
||||
$path = join_path(CronUploaderConfig::get_dir(), $folder);
|
||||
deltree($path);
|
||||
$page->flash("Cleared $path");
|
||||
}
|
||||
|
||||
|
||||
private function get_cron_url(): string
|
||||
private function get_cron_url()
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$user_api_key = $user_config->get_string(UserConfig::API_KEY);
|
||||
|
||||
return make_http(make_link("/cron_upload/run", "api_key=".urlencode($user_api_key)));
|
||||
return make_http(make_link("/cron_upload/" . CronUploaderConfig::get_key()));
|
||||
}
|
||||
|
||||
private function get_cron_cmd(): string
|
||||
private function get_cron_cmd()
|
||||
{
|
||||
return "curl --silent " . $this->get_cron_url();
|
||||
}
|
||||
|
||||
private function display_documentation(): void
|
||||
private function display_documentation()
|
||||
{
|
||||
global $database;
|
||||
|
||||
@ -261,36 +256,28 @@ class CronUploader extends Extension
|
||||
);
|
||||
}
|
||||
|
||||
public function get_queue_dir(): string
|
||||
public function get_queue_dir()
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$dir = CronUploaderConfig::get_dir();
|
||||
return join_path($dir, self::QUEUE_DIR);
|
||||
}
|
||||
|
||||
public function get_uploaded_dir(): string
|
||||
public function get_uploaded_dir()
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$dir = CronUploaderConfig::get_dir();
|
||||
return join_path($dir, self::UPLOADED_DIR);
|
||||
}
|
||||
|
||||
public function get_failed_dir(): string
|
||||
public function get_failed_dir()
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$dir = CronUploaderConfig::get_dir();
|
||||
return join_path($dir, self::FAILED_DIR);
|
||||
}
|
||||
|
||||
private function prep_root_dir(): string
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
// Determine directory (none = default)
|
||||
$dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$dir = CronUploaderConfig::get_dir();
|
||||
|
||||
// Make the directory if it doesn't exist yet
|
||||
if (!is_dir($this->get_queue_dir())) {
|
||||
@ -308,36 +295,35 @@ class CronUploader extends Extension
|
||||
|
||||
private function get_lock_file(): string
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$root_dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$root_dir = CronUploaderConfig::get_dir();
|
||||
return join_path($root_dir, ".lock");
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the image & handles everything
|
||||
*/
|
||||
public function process_upload(): bool
|
||||
public function process_upload(string $key, ?int $upload_count = null): bool
|
||||
{
|
||||
global $database, $user, $user_config, $config, $_shm_load_start;
|
||||
global $database, $config, $_shm_load_start;
|
||||
|
||||
$max_time = intval(ini_get('max_execution_time'))*.8;
|
||||
|
||||
$this->set_headers();
|
||||
|
||||
if (!$config->get_bool(UserConfig::ENABLE_API_KEYS)) {
|
||||
throw new SCoreException("User API keys are note enabled. Please enable them for the cron upload functionality to work.");
|
||||
if ($key!=CronUploaderConfig::get_key()) {
|
||||
throw new SCoreException("Cron upload key incorrect");
|
||||
}
|
||||
$user_id = CronUploaderConfig::get_user();
|
||||
if (empty($user_id)) {
|
||||
throw new SCoreException("Cron upload user not set");
|
||||
}
|
||||
$my_user = User::by_id($user_id);
|
||||
if ($my_user == null) {
|
||||
throw new SCoreException("No user found for cron upload user $user_id");
|
||||
}
|
||||
|
||||
if ($user->is_anonymous()) {
|
||||
throw new SCoreException("User not present. Please specify the api_key for the user to run cron upload as.");
|
||||
}
|
||||
|
||||
$this->log_message(SCORE_LOG_INFO, "Logged in as user {$user->name}");
|
||||
|
||||
if (!$user->can(Permissions::CRON_RUN)) {
|
||||
throw new SCoreException("User does not have permission to run cron upload");
|
||||
}
|
||||
send_event(new UserLoginEvent($my_user));
|
||||
$this->log_message(SCORE_LOG_INFO, "Logged in as user {$my_user->name}");
|
||||
|
||||
$lockfile = fopen($this->get_lock_file(), "w");
|
||||
if (!flock($lockfile, LOCK_EX | LOCK_NB)) {
|
||||
@ -349,7 +335,7 @@ class CronUploader extends Extension
|
||||
//set_time_limit(0);
|
||||
|
||||
$output_subdir = date('Ymd-His', time());
|
||||
$image_queue = $this->generate_image_queue($user_config->get_string(CronUploaderConfig::DIR));
|
||||
$image_queue = $this->generate_image_queue(CronUploaderConfig::get_dir());
|
||||
|
||||
// Randomize Images
|
||||
//shuffle($this->image_queue);
|
||||
@ -363,9 +349,6 @@ class CronUploader extends Extension
|
||||
$execution_time = microtime(true) - $_shm_load_start;
|
||||
if ($execution_time>$max_time) {
|
||||
break;
|
||||
} else {
|
||||
$remaining = $max_time - $execution_time;
|
||||
$this->log_message(SCORE_LOG_DEBUG, "Max run time remaining: $remaining");
|
||||
}
|
||||
try {
|
||||
$database->begin_transaction();
|
||||
@ -391,7 +374,7 @@ class CronUploader extends Extension
|
||||
$failed++;
|
||||
$this->log_message(SCORE_LOG_ERROR, "(" . gettype($e) . ") " . $e->getMessage());
|
||||
$this->log_message(SCORE_LOG_ERROR, $e->getTraceAsString());
|
||||
if ($user_config->get_bool(CronUploaderConfig::STOP_ON_ERROR)) {
|
||||
if ($config->get_bool(CronUploaderConfig::STOP_ON_ERROR)) {
|
||||
break;
|
||||
} else {
|
||||
$this->move_uploaded($img[0], $img[1], $output_subdir, true);
|
||||
@ -420,9 +403,7 @@ class CronUploader extends Extension
|
||||
|
||||
private function move_uploaded(string $path, string $filename, string $output_subdir, bool $corrupt = false)
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$rootDir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
$rootDir = CronUploaderConfig::get_dir();
|
||||
$rootLength = strlen($rootDir);
|
||||
if ($rootDir[$rootLength-1]=="/"||$rootDir[$rootLength-1]=="\\") {
|
||||
$rootLength--;
|
||||
@ -438,10 +419,10 @@ class CronUploader extends Extension
|
||||
if ($corrupt) {
|
||||
// Move to corrupt dir
|
||||
$newDir = join_path($this->get_failed_dir(), $output_subdir, $relativeDir);
|
||||
$info = "ERROR: Post was not uploaded. ";
|
||||
$info = "ERROR: Image was not uploaded. ";
|
||||
} else {
|
||||
$newDir = join_path($this->get_uploaded_dir(), $output_subdir, $relativeDir);
|
||||
$info = "Post successfully uploaded. ";
|
||||
$info = "Image successfully uploaded. ";
|
||||
}
|
||||
$newDir = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $newDir);
|
||||
|
||||
@ -453,7 +434,7 @@ class CronUploader extends Extension
|
||||
// move file to correct dir
|
||||
rename($path, $newFile);
|
||||
|
||||
$this->log_message(SCORE_LOG_INFO, $info . "Post \"$filename\" moved from queue to \"$newDir\".");
|
||||
$this->log_message(SCORE_LOG_INFO, $info . "Image \"$filename\" moved from queue to \"$newDir\".");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -481,14 +462,11 @@ class CronUploader extends Extension
|
||||
|
||||
// Generate info message
|
||||
if ($event->image_id == -1) {
|
||||
if (array_key_exists("mime", $event->metadata)) {
|
||||
throw new UploadException("File type not recognised (".$event->metadata["mime"]."). Filename: {$filename}");
|
||||
}
|
||||
throw new UploadException("File type not recognised. Filename: {$filename}");
|
||||
} elseif ($event->merged === true) {
|
||||
$infomsg = "Post merged. ID: {$event->image_id} - Filename: {$filename}";
|
||||
$infomsg = "Image merged. ID: {$event->image_id} - Filename: {$filename}";
|
||||
} else {
|
||||
$infomsg = "Post uploaded. ID: {$event->image_id} - Filename: {$filename}";
|
||||
$infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}";
|
||||
}
|
||||
$this->log_message(SCORE_LOG_INFO, $infomsg);
|
||||
|
||||
@ -499,7 +477,7 @@ class CronUploader extends Extension
|
||||
private const SKIPPABLE_FILES = ['.ds_store','thumbs.db'];
|
||||
private const SKIPPABLE_DIRECTORIES = ['__macosx'];
|
||||
|
||||
private function is_skippable_dir(string $path): bool
|
||||
private function is_skippable_dir(string $path)
|
||||
{
|
||||
$info = pathinfo($path);
|
||||
|
||||
@ -510,7 +488,7 @@ class CronUploader extends Extension
|
||||
return false;
|
||||
}
|
||||
|
||||
private function is_skippable_file(string $path): bool
|
||||
private function is_skippable_file(string $path)
|
||||
{
|
||||
$info = pathinfo($path);
|
||||
|
||||
@ -530,7 +508,7 @@ class CronUploader extends Extension
|
||||
$base = $this->get_queue_dir();
|
||||
|
||||
if (!is_dir($base)) {
|
||||
$this->log_message(SCORE_LOG_WARNING, "Post Queue Directory could not be found at \"$base\".");
|
||||
$this->log_message(SCORE_LOG_WARNING, "Image Queue Directory could not be found at \"$base\".");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -559,11 +537,7 @@ class CronUploader extends Extension
|
||||
|
||||
private function get_log_file(): string
|
||||
{
|
||||
global $user_config;
|
||||
|
||||
$dir = $user_config->get_string(CronUploaderConfig::DIR);
|
||||
|
||||
return join_path($dir, "uploads.log");
|
||||
return join_path(CronUploaderConfig::get_dir(), "uploads.log");
|
||||
}
|
||||
|
||||
private function set_headers(): void
|
||||
@ -571,7 +545,7 @@ class CronUploader extends Extension
|
||||
global $page;
|
||||
|
||||
$page->set_mode(PageMode::MANUAL);
|
||||
$page->set_mime(MimeType::TEXT);
|
||||
$page->set_type(MIME_TYPE_TEXT);
|
||||
$page->send_headers();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
function copyInputToClipboard(inputId) {
|
||||
// Referenced from https://www.w3schools.com/howto/howto_js_copy_clipboard.asp
|
||||
let source = document.getElementById(inputId);
|
||||
source.select();
|
||||
source.setSelectionRange(0, 99999); /*For mobile devices*/
|
||||
document.execCommand("copy");
|
||||
}
|
@ -1,16 +1,5 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use function MicroHTML\LABEL;
|
||||
use function MicroHTML\TABLE;
|
||||
use function MicroHTML\TBODY;
|
||||
use function MicroHTML\TFOOT;
|
||||
use function MicroHTML\TR;
|
||||
use function MicroHTML\TH;
|
||||
use function MicroHTML\TD;
|
||||
use function MicroHTML\INPUT;
|
||||
use function MicroHTML\rawHTML;
|
||||
use function MicroHTML\emptyHTML;
|
||||
|
||||
class CronUploaderTheme extends Themelet
|
||||
{
|
||||
public function display_documentation(
|
||||
@ -22,18 +11,11 @@ class CronUploaderTheme extends Themelet
|
||||
string $cron_url,
|
||||
?array $log_entries
|
||||
) {
|
||||
global $page, $config, $user_config;
|
||||
global $page;
|
||||
|
||||
$info_html = "";
|
||||
|
||||
$page->set_title("Cron Uploader");
|
||||
$page->set_heading("Cron Uploader");
|
||||
|
||||
if (!$config->get_bool(UserConfig::ENABLE_API_KEYS)) {
|
||||
$info_html .= "<b style='color:red'>THIS EXTENSION REQUIRES USER API KEYS TO BE ENABLED IN <a href=''>BOARD ADMIN</a></b>";
|
||||
}
|
||||
|
||||
$info_html .= "<b>Information</b>
|
||||
$info_html = "<b>Information</b>
|
||||
<br>
|
||||
<table style='width:470px;'>
|
||||
" . ($running ? "<tr><td colspan='4'><b style='color:red'>Cron upload is currently running</b></td></tr>" : "") . "
|
||||
@ -59,13 +41,9 @@ class CronUploaderTheme extends Themelet
|
||||
<td>{$failed_dirinfo['path']}</td>
|
||||
</tr></table>
|
||||
|
||||
<div>Cron Command: <input type='text' size='60' value='$cron_cmd' id='cron_command'>
|
||||
<button onclick='copyInputToClipboard(\"cron_command\")'>Copy</button></div>
|
||||
<div>Create a cron job with the command above.
|
||||
Read the documentation if you're not sure what to do.</div>
|
||||
<div>URL: <input type='text' size='60' value='$cron_url' id='cron_url'>
|
||||
<button onclick='copyInputToClipboard(\"cron_url\")'>Copy</button></div>";
|
||||
|
||||
<br>Cron Command: <input type='text' size='60' value='$cron_cmd'><br>
|
||||
Create a cron job with the command above.<br/>
|
||||
Read the documentation if you're not sure what to do.<br>";
|
||||
|
||||
$install_html = "
|
||||
This cron uploader is fairly easy to use but has to be configured first.
|
||||
@ -76,7 +54,7 @@ class CronUploaderTheme extends Themelet
|
||||
<li>Create a cron job or something else that can open a url on specified times.
|
||||
<br/>cron is a service that runs commands over and over again on a a schedule. You can set up cron (or any similar tool) to run the command above to trigger the import on whatever schedule you desire.
|
||||
<br />If you're not sure how to do this, you can give the command to your web host and you can ask them to create the cron job for you.
|
||||
<br />When you create the cron job, you choose when to upload new posts.</li>
|
||||
<br />When you create the cron job, you choose when to upload new images.</li>
|
||||
</ol>";
|
||||
|
||||
|
||||
@ -98,10 +76,12 @@ class CronUploaderTheme extends Themelet
|
||||
<li>If an import is already running, another cannot start until it is done.</li>
|
||||
<li>Each time it runs it will import for up to ".number_format($max_time)." seconds. This is controlled by the PHP max execution time.</li>
|
||||
<li>Uploaded images will be moved to the 'uploaded' directory into a subfolder named after the time the import started. It's recommended that you remove everything out of this directory from time to time. If you have admin controls enabled, this can be done from <a href='".make_link("admin")."'>Board Admin</a>.</li>
|
||||
<li>If you enable the db logging extension, you can view the log output on this screen. Otherwise the log will be written to a file at ".$user_config->get_string(CronUploaderConfig::DIR).DIRECTORY_SEPARATOR."uploads.log</li>
|
||||
<li>If you enable the db logging extension, you can view the log output on this screen. Otherwise the log will be written to a file at ".CronUploaderConfig::get_dir().DIRECTORY_SEPARATOR."uploads.log</li>
|
||||
</ul>
|
||||
";
|
||||
|
||||
$page->set_title("Cron Uploader");
|
||||
$page->set_heading("Cron Uploader");
|
||||
|
||||
$block = new Block("Cron Uploader", $info_html, "main", 10);
|
||||
$block_install = new Block("Setup Guide", $install_html, "main", 30);
|
||||
@ -121,40 +101,6 @@ class CronUploaderTheme extends Themelet
|
||||
}
|
||||
}
|
||||
|
||||
public function get_user_options(string $dir, bool $stop_on_error, int $log_level, bool $all_logs): string
|
||||
{
|
||||
$form = SHM_SIMPLE_FORM(
|
||||
"user_admin/cron_uploader",
|
||||
TABLE(
|
||||
["class"=>"form"],
|
||||
TBODY(
|
||||
TR(
|
||||
TH("Cron Uploader")
|
||||
),
|
||||
TR(
|
||||
TH("Root dir"),
|
||||
TD(INPUT(["type"=>'text', "name"=>'name', "required"=>true]))
|
||||
),
|
||||
TR(
|
||||
TH(),
|
||||
TD(
|
||||
LABEL(INPUT(["type"=>'checkbox', "name"=>'stop_on_error']), "Stop On Error")
|
||||
)
|
||||
),
|
||||
TR(
|
||||
TH(rawHTML("Repeat Password")),
|
||||
TD(INPUT(["type"=>'password', "name"=>'pass2', "required"=>true]))
|
||||
)
|
||||
),
|
||||
TFOOT(
|
||||
TR(TD(["colspan"=>"2"], INPUT(["type"=>"submit", "value"=>"Save Settings"])))
|
||||
)
|
||||
)
|
||||
);
|
||||
$html = emptyHTML($form);
|
||||
return (string)$html;
|
||||
}
|
||||
|
||||
public function display_form(array $failed_dirs)
|
||||
{
|
||||
global $page;
|
||||
|
@ -4,13 +4,13 @@ class CustomHtmlHeadersInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "custom_html_headers";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Custom HTML Headers";
|
||||
public string $url = "http://www.drudexsoftware.com";
|
||||
public array $authors = ["Drudex Software"=>"support@drudexsoftware.com"];
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Allows admins to modify & set custom <head> content";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Custom HTML Headers";
|
||||
public $url = "http://www.drudexsoftware.com";
|
||||
public $authors = ["Drudex Software"=>"support@drudexsoftware.com"];
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Allows admins to modify & set custom <head> content";
|
||||
public $documentation =
|
||||
"When you go to board config you can find a block named Custom HTML Headers.
|
||||
In that block you can simply place any thing you can place within <head></head>
|
||||
|
||||
|
@ -5,7 +5,7 @@ class CustomHtmlHeaders extends Extension
|
||||
# Adds setup block for custom <head> content
|
||||
public function onSetupBuilding(SetupBuildingEvent $event)
|
||||
{
|
||||
$sb = $event->panel->create_new_block("Custom HTML Headers");
|
||||
$sb = new SetupBlock("Custom HTML Headers");
|
||||
|
||||
// custom headers
|
||||
$sb->add_longtext_option(
|
||||
@ -19,6 +19,8 @@ class CustomHtmlHeaders extends Extension
|
||||
"as prefix" => "prefix",
|
||||
"as suffix" => "suffix"
|
||||
], "<br>Add website name in title");
|
||||
|
||||
$event->panel->add_block($sb);
|
||||
}
|
||||
|
||||
public function onInitExt(InitExtEvent $event)
|
||||
@ -53,7 +55,7 @@ class CustomHtmlHeaders extends Extension
|
||||
$sitename_in_title = $config->get_string("sitename_in_title");
|
||||
|
||||
// sitename is already in title (can occur on index & other pages)
|
||||
if (str_contains($page->title, $site_title)) {
|
||||
if (strstr($page->title, $site_title)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@ class DanbooruApiInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "danbooru_api";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Danbooru Client API";
|
||||
public array $authors = ["JJS"=>"jsutinen@gmail.com"];
|
||||
public string $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Danbooru Client API";
|
||||
public $authors = ["JJS"=>"jsutinen@gmail.com"];
|
||||
public $description = "Allow Danbooru apps like Danbooru Uploader for Firefox to communicate with Shimmie";
|
||||
public $documentation =
|
||||
"<p>Notes:
|
||||
<br>danbooru API based on documentation from danbooru 1.0 -
|
||||
http://attachr.com/7569
|
||||
|
@ -1,25 +1,5 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use \MicroHTML\HTMLElement;
|
||||
|
||||
function TAGS(...$args): HTMLElement
|
||||
{
|
||||
return new HTMLElement("tags", $args);
|
||||
}
|
||||
function TAG(...$args): HTMLElement
|
||||
{
|
||||
return new HTMLElement("tag", $args);
|
||||
}
|
||||
function POSTS(...$args): HTMLElement
|
||||
{
|
||||
return new HTMLElement("posts", $args);
|
||||
}
|
||||
function POST(...$args): HTMLElement
|
||||
{
|
||||
return new HTMLElement("post", $args);
|
||||
}
|
||||
|
||||
|
||||
class DanbooruApi extends Extension
|
||||
{
|
||||
public function onPageRequest(PageRequestEvent $event)
|
||||
@ -30,14 +10,14 @@ class DanbooruApi extends Extension
|
||||
|
||||
if ($event->page_matches("api/danbooru/add_post") || $event->page_matches("api/danbooru/post/create.xml")) {
|
||||
// No XML data is returned from this function
|
||||
$page->set_mime(MimeType::TEXT);
|
||||
$page->set_type(MIME_TYPE_TEXT);
|
||||
$this->api_add_post();
|
||||
} elseif ($event->page_matches("api/danbooru/find_posts") || $event->page_matches("api/danbooru/post/index.xml")) {
|
||||
$page->set_mime(MimeType::XML_APPLICATION);
|
||||
$page->set_data((string)$this->api_find_posts());
|
||||
$page->set_type(MIME_TYPE_XML_APPLICATION);
|
||||
$page->set_data($this->api_find_posts());
|
||||
} elseif ($event->page_matches("api/danbooru/find_tags")) {
|
||||
$page->set_mime(MimeType::XML_APPLICATION);
|
||||
$page->set_data((string)$this->api_find_tags());
|
||||
$page->set_type(MIME_TYPE_XML_APPLICATION);
|
||||
$page->set_data($this->api_find_tags());
|
||||
}
|
||||
|
||||
// Hackery for danbooruup 0.3.2 providing the wrong view url. This simply redirects to the proper
|
||||
@ -57,7 +37,7 @@ class DanbooruApi extends Extension
|
||||
* Authenticates a user based on the contents of the login and password parameters
|
||||
* or makes them anonymous. Does not set any cookies or anything permanent.
|
||||
*/
|
||||
private function authenticate_user(): void
|
||||
private function authenticate_user()
|
||||
{
|
||||
global $config, $user;
|
||||
|
||||
@ -86,7 +66,7 @@ class DanbooruApi extends Extension
|
||||
* - tags: any typical tag query. See Tag#parse_query for details.
|
||||
* - after_id: limit results to tags with an id number after after_id. Useful if you only want to refresh
|
||||
*/
|
||||
private function api_find_tags(): HTMLElement
|
||||
private function api_find_tags(): string
|
||||
{
|
||||
global $database;
|
||||
$results = [];
|
||||
@ -114,14 +94,11 @@ class DanbooruApi extends Extension
|
||||
}
|
||||
}
|
||||
// Currently disabled to maintain identical functionality to danbooru 1.0's own "broken" find_tags
|
||||
/*
|
||||
elseif (isset($_GET['tags'])) {
|
||||
elseif (false && isset($_GET['tags'])) {
|
||||
$start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
|
||||
$tags = Tag::explode($_GET['tags']);
|
||||
assert(!is_null($start) && !is_null($tags));
|
||||
}
|
||||
*/
|
||||
else {
|
||||
} else {
|
||||
$start = isset($_GET['after_id']) ? int_escape($_GET['offset']) : 0;
|
||||
$sqlresult = $database->get_all(
|
||||
"SELECT id,tag,count FROM tags WHERE count > 0 AND id >= :id ORDER BY id DESC",
|
||||
@ -133,15 +110,16 @@ class DanbooruApi extends Extension
|
||||
}
|
||||
|
||||
// Tag results collected, build XML output
|
||||
$xml = TAGS();
|
||||
$xml = "<tags>\n";
|
||||
foreach ($results as $tag) {
|
||||
$xml->appendChild(TAG([
|
||||
$xml .= xml_tag("tag", [
|
||||
"type" => "0",
|
||||
"counts" => $tag[0],
|
||||
"name" => $tag[1],
|
||||
"id" => $tag[2],
|
||||
]));
|
||||
]);
|
||||
}
|
||||
$xml .= "</tags>";
|
||||
return $xml;
|
||||
}
|
||||
|
||||
@ -159,7 +137,7 @@ class DanbooruApi extends Extension
|
||||
*
|
||||
* #return string
|
||||
*/
|
||||
private function api_find_posts(): HTMLElement
|
||||
private function api_find_posts()
|
||||
{
|
||||
$results = [];
|
||||
|
||||
@ -197,7 +175,7 @@ class DanbooruApi extends Extension
|
||||
|
||||
// Now we have the array $results filled with Image objects
|
||||
// Let's display them
|
||||
$xml = POSTS(["count"=>$count, "offset"=>$start]);
|
||||
$xml = "<posts count=\"{$count}\" offset=\"{$start}\">\n";
|
||||
foreach ($results as $img) {
|
||||
// Sanity check to see if $img is really an image object
|
||||
// If it isn't (e.g. someone requested an invalid md5 or id), break out of the this
|
||||
@ -207,7 +185,7 @@ class DanbooruApi extends Extension
|
||||
$taglist = $img->get_tag_list();
|
||||
$owner = $img->get_owner();
|
||||
$previewsize = get_thumbnail_size($img->width, $img->height);
|
||||
$xml->appendChild(TAG([
|
||||
$xml .= xml_tag("post", [
|
||||
"id" => $img->id,
|
||||
"md5" => $img->hash,
|
||||
"file_name" => $img->filename,
|
||||
@ -224,8 +202,9 @@ class DanbooruApi extends Extension
|
||||
"source" => $img->source,
|
||||
"score" => 0,
|
||||
"author" => $owner->name
|
||||
]));
|
||||
]);
|
||||
}
|
||||
$xml .= "</posts>";
|
||||
return $xml;
|
||||
}
|
||||
|
||||
@ -256,7 +235,7 @@ class DanbooruApi extends Extension
|
||||
* Get:
|
||||
* - Redirected to the newly uploaded post.
|
||||
*/
|
||||
private function api_add_post(): void
|
||||
private function api_add_post()
|
||||
{
|
||||
global $user, $page;
|
||||
$danboorup_kludge = 1; // danboorup for firefox makes broken links out of location: /path
|
||||
@ -292,8 +271,8 @@ class DanbooruApi extends Extension
|
||||
}
|
||||
} elseif (isset($_REQUEST['source']) || isset($_REQUEST['post']['source'])) { // A url was provided
|
||||
$source = isset($_REQUEST['source']) ? $_REQUEST['source'] : $_REQUEST['post']['source'];
|
||||
$file = tempnam(sys_get_temp_dir(), "shimmie_transload");
|
||||
$ok = fetch_url($source, $file);
|
||||
$file = tempnam("/tmp", "shimmie_transload");
|
||||
$ok = transload($source, $file);
|
||||
if (!$ok) {
|
||||
$page->set_code(409);
|
||||
$page->add_http_header("X-Danbooru-Errors: fopen read error");
|
||||
|
@ -8,12 +8,12 @@ class DanbooruApiTest extends ShimmiePHPUnitTestCase
|
||||
$image_id = $this->post_image("tests/bedroom_workshop.jpg", "data");
|
||||
|
||||
$this->get_page("api/danbooru/find_posts");
|
||||
$this->get_page("api/danbooru/find_posts", ["id"=>$image_id]);
|
||||
$this->get_page("api/danbooru/find_posts", ["md5"=>"17fc89f372ed3636e28bd25cc7f3bac1"]);
|
||||
$this->get_page("api/danbooru/find_posts?id=$image_id");
|
||||
$this->get_page("api/danbooru/find_posts?md5=17fc89f372ed3636e28bd25cc7f3bac1");
|
||||
|
||||
$this->get_page("api/danbooru/find_tags");
|
||||
$this->get_page("api/danbooru/find_tags", ["id"=>1]);
|
||||
$this->get_page("api/danbooru/find_tags", ["name"=>"data"]);
|
||||
$this->get_page("api/danbooru/find_tags?id=1");
|
||||
$this->get_page("api/danbooru/find_tags?name=data");
|
||||
|
||||
$page = $this->get_page("api/danbooru/post/show/$image_id");
|
||||
$this->assertEquals(302, $page->code);
|
||||
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ImageDownloadingEvent extends Event
|
||||
{
|
||||
public Image $image;
|
||||
public string $mime;
|
||||
public string $path;
|
||||
public bool $file_modified = false;
|
||||
|
||||
public function __construct(Image $image, string $path, string $mime)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->image = $image;
|
||||
$this->path = $path;
|
||||
$this->mime = $mime;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
class DownloadInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "download";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Download";
|
||||
public array $authors = ["Matthew Barbour"=>"matthew@darkholme.net"];
|
||||
public string $license = self::LICENSE_WTFPL;
|
||||
public string $description = "System-wide download functions";
|
||||
public bool $core = true;
|
||||
public string $visibility = self::VISIBLE_HIDDEN;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once "events.php";
|
||||
|
||||
class Download extends Extension
|
||||
{
|
||||
public function get_priority(): int
|
||||
{
|
||||
// Set near the end to give everything else a chance to process
|
||||
return 99;
|
||||
}
|
||||
|
||||
|
||||
public function onImageDownloading(ImageDownloadingEvent $event)
|
||||
{
|
||||
global $page;
|
||||
|
||||
$page->set_mime($event->mime);
|
||||
|
||||
$page->set_mode(PageMode::FILE);
|
||||
|
||||
$page->set_file($event->path, $event->file_modified);
|
||||
|
||||
$event->stop_processing = true;
|
||||
}
|
||||
}
|
@ -4,13 +4,13 @@ class DowntimeInfo extends ExtensionInfo
|
||||
{
|
||||
public const KEY = "downtime";
|
||||
|
||||
public string $key = self::KEY;
|
||||
public string $name = "Downtime";
|
||||
public string $url = self::SHIMMIE_URL;
|
||||
public array $authors = self::SHISH_AUTHOR;
|
||||
public string $license = self::LICENSE_GPLV2;
|
||||
public string $description = "Show a \"down for maintenance\" page";
|
||||
public ?string $documentation =
|
||||
public $key = self::KEY;
|
||||
public $name = "Downtime";
|
||||
public $url = self::SHIMMIE_URL;
|
||||
public $authors = self::SHISH_AUTHOR;
|
||||
public $license = self::LICENSE_GPLV2;
|
||||
public $description = "Show a \"down for maintenance\" page";
|
||||
public $documentation =
|
||||
"Once installed there will be some more options on the config page --
|
||||
Ticking \"disable non-admin access\" will mean that regular and anonymous
|
||||
users will be blocked from accessing the site, only able to view the
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user