Merge branch 'develop' of https://github.com/shish/shimmie2 into develop
This commit is contained in:
commit
7a7dc86cfc
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
vendor
|
||||
.git
|
||||
*.phar
|
||||
data
|
||||
images
|
||||
thumbs
|
||||
composer.lock
|
||||
*.sqlite
|
10
.travis.yml
10
.travis.yml
@ -1,7 +1,7 @@
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.1
|
||||
- 7.2
|
||||
|
||||
sudo: false
|
||||
|
||||
@ -10,8 +10,6 @@ env:
|
||||
- DB=mysql
|
||||
- DB=pgsql
|
||||
- DB=sqlite
|
||||
allow_failures:
|
||||
- DB=sqlite
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@ -36,9 +34,11 @@ install:
|
||||
mysql -e "CREATE DATABASE shimmie;" -uroot ;
|
||||
echo '<?php define("DATABASE_DSN", "mysql:user=root;password=;host=localhost;dbname=shimmie");' > data/config/auto_install.conf.php ;
|
||||
fi
|
||||
- if [[ "$DB" == "sqlite" ]]; then echo '<?php define("DATABASE_DSN", "sqlite:shimmie.sqlite");' > data/config/auto_install.conf.php ; fi
|
||||
- if [[ "$DB" == "sqlite" ]]; then
|
||||
echo '<?php define("DATABASE_DSN", "sqlite:data/shimmie.sqlite");' > data/config/auto_install.conf.php ;
|
||||
fi
|
||||
- composer install
|
||||
- php install.php
|
||||
- php index.php
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover=data/coverage.clover
|
||||
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM debian:testing-slim
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
EXPOSE 8000
|
||||
RUN apt update && apt install -y curl
|
||||
HEALTHCHECK --interval=5m --timeout=3s CMD curl --fail http://127.0.0.1:8000/ || exit 1
|
||||
|
||||
RUN apt install -y php7.3-cli php7.3-gd php7.3-pgsql php7.3-mysql php7.3-sqlite3 php7.3-zip php7.3-dom php7.3-mbstring php-xdebug
|
||||
RUN apt install -y composer imagemagick vim zip unzip
|
||||
|
||||
COPY composer.json /app/
|
||||
WORKDIR /app
|
||||
RUN composer install
|
||||
|
||||
COPY . /app/
|
||||
RUN mkdir -p data/config && \
|
||||
echo "<?php define(\"DATABASE_DSN\", \"sqlite:data/shimmie.sqlite\");" > data/config/auto_install.conf.php && \
|
||||
php index.php && \
|
||||
./vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text && \
|
||||
rm -rf data
|
||||
CMD ["/usr/bin/php", "-d", "upload_max_filesize=50M", "-d", "post_max_size=50M", "-S", "0.0.0.0:8000", "tests/router.php"]
|
@ -29,7 +29,7 @@ check out one of the versioned branches.
|
||||
# Requirements
|
||||
|
||||
- MySQL/MariaDB 5.1+ (with experimental support for PostgreSQL 9+ and SQLite 3)
|
||||
- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.0+ as of writing)
|
||||
- [Stable PHP](https://en.wikipedia.org/wiki/PHP#Release_history) (7.1+ as of writing)
|
||||
- GD or ImageMagick
|
||||
|
||||
# Installation
|
||||
|
@ -23,7 +23,8 @@
|
||||
],
|
||||
|
||||
"require" : {
|
||||
"php" : ">=7.0",
|
||||
"php" : ">=7.1",
|
||||
"ext-pdo": "*",
|
||||
|
||||
"flexihash/flexihash" : "^2.0.0",
|
||||
"ifixit/php-akismet" : "1.*",
|
||||
@ -41,30 +42,5 @@
|
||||
|
||||
"require-dev" : {
|
||||
"phpunit/phpunit" : "6.*"
|
||||
},
|
||||
|
||||
"vendor-copy": {
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js" : "lib/vendor/js/jquery-1.12.3.min.js",
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.map" : "lib/vendor/js/jquery-1.12.3.min.map",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js" : "lib/vendor/js/jquery.timeago.js",
|
||||
"vendor/bower-asset/tablesorter/jquery.tablesorter.min.js" : "lib/vendor/js/jquery.tablesorter.min.js",
|
||||
"vendor/bower-asset/mediaelement/build/flashmediaelement.swf" : "lib/vendor/swf/flashmediaelement.swf",
|
||||
"vendor/bower-asset/js-cookie/src/js.cookie.js" : "lib/vendor/js/js.cookie.js"
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"pre-install-cmd" : [
|
||||
"php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\""
|
||||
],
|
||||
"pre-update-cmd" : [
|
||||
"php -r \"array_map('unlink', array_merge(glob('lib/vendor/js/j*.{js,map}', GLOB_BRACE), glob('lib/vendor/css/*.css'), glob('lib/vendor/swf/*.swf')));\""
|
||||
],
|
||||
|
||||
"post-install-cmd" : [
|
||||
"php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\""
|
||||
],
|
||||
"post-update-cmd" : [
|
||||
"php -r \"array_map('copy', array_keys(json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']), json_decode(file_get_contents('composer.json'), TRUE)['vendor-copy']);\""
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,11 @@
|
||||
|
||||
global $config, $database, $user, $page, $_shm_ctx;
|
||||
|
||||
require_once "core/sys_config.inc.php";
|
||||
require_once "core/util.inc.php";
|
||||
require_once "core/sys_config.php";
|
||||
require_once "core/polyfills.php";
|
||||
require_once "core/util.php";
|
||||
require_once "vendor/shish/libcontext-php/context.php";
|
||||
require_once "vendor/autoload.php";
|
||||
require_once "core/imageboard.pack.php";
|
||||
|
||||
// set up and purify the environment
|
||||
_version_check();
|
||||
@ -20,6 +20,7 @@ _sanitise_environment();
|
||||
$_shm_ctx->log_start("Opening files");
|
||||
$_shm_files = array_merge(
|
||||
zglob("core/*.php"),
|
||||
zglob("core/{".ENABLED_MODS."}/*.php"),
|
||||
zglob("ext/{".ENABLED_EXTS."}/main.php")
|
||||
);
|
||||
foreach($_shm_files as $_shm_filename) {
|
File diff suppressed because one or more lines are too long
207
core/cacheengine.php
Normal file
207
core/cacheengine.php
Normal file
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
interface CacheEngine {
|
||||
public function get(string $key);
|
||||
public function set(string $key, $val, int $time=0);
|
||||
public function delete(string $key);
|
||||
public function get_hits(): int;
|
||||
public function get_misses(): int;
|
||||
}
|
||||
|
||||
class NoCache implements CacheEngine {
|
||||
public function get(string $key) {return false;}
|
||||
public function set(string $key, $val, int $time=0) {}
|
||||
public function delete(string $key) {}
|
||||
public function get_hits(): int {return 0;}
|
||||
public function get_misses(): int {return 0;}
|
||||
}
|
||||
|
||||
class MemcacheCache implements CacheEngine {
|
||||
/** @var \Memcache|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcache;
|
||||
@$this->memcache->pconnect($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = $this->memcache->get($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $val === false ? "miss" : "hit";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($val !== false) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
$this->memcache->set($key, $val, false, $time);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$this->memcache->delete($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
|
||||
class MemcachedCache implements CacheEngine {
|
||||
/** @var \Memcached|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcached;
|
||||
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
|
||||
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
|
||||
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
|
||||
$this->memcache->addServer($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$val = $this->memcache->get($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $res == Memcached::RES_SUCCESS ? "hit" : "miss";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res == Memcached::RES_SUCCESS) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else if($res == Memcached::RES_NOTFOUND) {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
error_log("Memcached error during get($key): $res");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->set($key, $val, $time);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS) {
|
||||
error_log("Memcached error during set($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->delete($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
|
||||
error_log("Memcached error during delete($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
|
||||
class APCCache implements CacheEngine {
|
||||
public $hits=0, $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
// $args is not used, but is passed in when APC cache is created.
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = apc_fetch($key);
|
||||
if($val) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
apc_store($key, $val, $time);
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
apc_delete($key);
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
|
||||
class RedisCache implements CacheEngine {
|
||||
public $hits=0, $misses=0;
|
||||
private $redis=null;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$this->redis = new Redis();
|
||||
$hp = explode(":", $args);
|
||||
$this->redis->pconnect($hp[0], $hp[1]);
|
||||
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
|
||||
$this->redis->setOption(Redis::OPT_PREFIX, 'shm:');
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = $this->redis->get($key);
|
||||
if($val !== false) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
if($time > 0) {
|
||||
$this->redis->setEx($key, $time, $val);
|
||||
}
|
||||
else {
|
||||
$this->redis->set($key, $val);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$this->redis->delete($key);
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
55
core/captcha.php
Normal file
55
core/captcha.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* CAPTCHA abstraction *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
function captcha_get_html(): string {
|
||||
global $config, $user;
|
||||
|
||||
if(DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) return "";
|
||||
|
||||
$captcha = "";
|
||||
if($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||
$r_publickey = $config->get_string("api_recaptcha_pubkey");
|
||||
if(!empty($r_publickey)) {
|
||||
$captcha = "
|
||||
<div class=\"g-recaptcha\" data-sitekey=\"{$r_publickey}\"></div>
|
||||
<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js\"></script>";
|
||||
} else {
|
||||
session_start();
|
||||
$captcha = Securimage::getCaptchaHtml(['securimage_path' => './vendor/dapphp/securimage/']);
|
||||
}
|
||||
}
|
||||
return $captcha;
|
||||
}
|
||||
|
||||
function captcha_check(): bool {
|
||||
global $config, $user;
|
||||
|
||||
if(DEBUG && ip_in_range($_SERVER['REMOTE_ADDR'], "127.0.0.0/8")) return true;
|
||||
|
||||
if($user->is_anonymous() && $config->get_bool("comment_captcha")) {
|
||||
$r_privatekey = $config->get_string('api_recaptcha_privkey');
|
||||
if(!empty($r_privatekey)) {
|
||||
$recaptcha = new \ReCaptcha\ReCaptcha($r_privatekey);
|
||||
$resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
if(!$resp->isSuccess()) {
|
||||
log_info("core", "Captcha failed (ReCaptcha): " . implode("", $resp->getErrorCodes()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
session_start();
|
||||
$securimg = new Securimage();
|
||||
if($securimg->check($_POST['captcha_code']) === false) {
|
||||
log_info("core", "Captcha failed (Securimage)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1,803 +0,0 @@
|
||||
<?php
|
||||
/** @privatesection */
|
||||
// Querylet {{{
|
||||
class Querylet {
|
||||
/** @var string */
|
||||
public $sql;
|
||||
/** @var array */
|
||||
public $variables;
|
||||
|
||||
public function __construct(string $sql, array $variables=array()) {
|
||||
$this->sql = $sql;
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
public function append(Querylet $querylet) {
|
||||
$this->sql .= $querylet->sql;
|
||||
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||
}
|
||||
|
||||
public function append_sql(string $sql) {
|
||||
$this->sql .= $sql;
|
||||
}
|
||||
|
||||
public function add_variable($var) {
|
||||
$this->variables[] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
class TagQuerylet {
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(string $tag, bool $positive) {
|
||||
$this->tag = $tag;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
|
||||
class ImgQuerylet {
|
||||
/** @var \Querylet */
|
||||
public $qlet;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(Querylet $qlet, bool $positive) {
|
||||
$this->qlet = $qlet;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ db engines
|
||||
class DBEngine {
|
||||
/** @var null|string */
|
||||
public $name = null;
|
||||
|
||||
public function init(PDO $db) {}
|
||||
|
||||
public function scoreql_to_sql(string $scoreql): string {
|
||||
return $scoreql;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
}
|
||||
class MySQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "mysql";
|
||||
|
||||
public function init(PDO $db) {
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$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", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "ENUM('Y', 'N')", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "DATETIME", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
|
||||
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
|
||||
}
|
||||
}
|
||||
class PostgreSQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "pgsql";
|
||||
|
||||
public function init(PDO $db) {
|
||||
if(array_key_exists('REMOTE_ADDR', $_SERVER)) {
|
||||
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
|
||||
}
|
||||
else {
|
||||
$db->exec("SET application_name TO 'shimmie [local]';");
|
||||
}
|
||||
$db->exec("SET statement_timeout TO 10000;");
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$data = str_replace("SCORE_AIPK", "SERIAL PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "INET", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'t'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'f'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "BOOL", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "TIMESTAMP", $data);
|
||||
$data = str_replace("SCORE_NOW", "current_timestamp", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "ILIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
return "CREATE TABLE $name ($data)";
|
||||
}
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date) { return strtotime($date); }
|
||||
function _now() { return date("Y-m-d h:i:s"); }
|
||||
function _floor($a) { return floor($a); }
|
||||
function _log($a, $b=null) {
|
||||
if(is_null($b)) return log($a);
|
||||
else return log($a, $b);
|
||||
}
|
||||
function _isnull($a) { return is_null($a); }
|
||||
function _md5($a) { return md5($a); }
|
||||
function _concat($a, $b) { return $a . $b; }
|
||||
function _lower($a) { return strtolower($a); }
|
||||
function _rand() { return rand(); }
|
||||
function _ln($n) { return log($n); }
|
||||
|
||||
class SQLite extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "sqlite";
|
||||
|
||||
public function init(PDO $db) {
|
||||
ini_set('sqlite.assoc_case', 0);
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
|
||||
$db->sqliteCreateFunction('now', '_now', 0);
|
||||
$db->sqliteCreateFunction('floor', '_floor', 1);
|
||||
$db->sqliteCreateFunction('log', '_log');
|
||||
$db->sqliteCreateFunction('isnull', '_isnull', 1);
|
||||
$db->sqliteCreateFunction('md5', '_md5', 1);
|
||||
$db->sqliteCreateFunction('concat', '_concat', 2);
|
||||
$db->sqliteCreateFunction('lower', '_lower', 1);
|
||||
$db->sqliteCreateFunction('rand', '_rand', 0);
|
||||
$db->sqliteCreateFunction('ln', '_ln', 1);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "VARCHAR(45)", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "CHAR(1)", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$cols = array();
|
||||
$extras = "";
|
||||
foreach(explode(",", $data) as $bit) {
|
||||
$matches = array();
|
||||
if(preg_match("/(UNIQUE)? ?INDEX\s*\((.*)\)/", $bit, $matches)) {
|
||||
$uni = $matches[1];
|
||||
$col = $matches[2];
|
||||
$extras .= "CREATE $uni INDEX {$name}_{$col} ON {$name}({$col});";
|
||||
}
|
||||
else {
|
||||
$cols[] = $bit;
|
||||
}
|
||||
}
|
||||
$cols_redone = implode(", ", $cols);
|
||||
return "CREATE TABLE $name ($cols_redone); $extras";
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ cache engines
|
||||
interface CacheEngine {
|
||||
|
||||
public function get(string $key);
|
||||
public function set(string $key, $val, int $time=0);
|
||||
public function delete(string $key);
|
||||
public function get_hits(): int;
|
||||
public function get_misses(): int;
|
||||
}
|
||||
class NoCache implements CacheEngine {
|
||||
public function get(string $key) {return false;}
|
||||
public function set(string $key, $val, int $time=0) {}
|
||||
public function delete(string $key) {}
|
||||
|
||||
public function get_hits(): int {return 0;}
|
||||
public function get_misses(): int {return 0;}
|
||||
}
|
||||
class MemcacheCache implements CacheEngine {
|
||||
/** @var \Memcache|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcache;
|
||||
@$this->memcache->pconnect($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = $this->memcache->get($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $val === false ? "miss" : "hit";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($val !== false) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
$this->memcache->set($key, $val, false, $time);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$this->memcache->delete($key);
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
class MemcachedCache implements CacheEngine {
|
||||
/** @var \Memcached|null */
|
||||
public $memcache=null;
|
||||
/** @var int */
|
||||
private $hits=0;
|
||||
/** @var int */
|
||||
private $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$hp = explode(":", $args);
|
||||
$this->memcache = new Memcached;
|
||||
#$this->memcache->setOption(Memcached::OPT_COMPRESSION, False);
|
||||
#$this->memcache->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP);
|
||||
#$this->memcache->setOption(Memcached::OPT_PREFIX_KEY, phpversion());
|
||||
$this->memcache->addServer($hp[0], $hp[1]);
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$val = $this->memcache->get($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
$hit = $res == Memcached::RES_SUCCESS ? "hit" : "miss";
|
||||
file_put_contents("data/cache.log", "Cache $hit: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res == Memcached::RES_SUCCESS) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else if($res == Memcached::RES_NOTFOUND) {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
error_log("Memcached error during get($key): $res");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->set($key, $val, $time);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache set: $key ($time)\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS) {
|
||||
error_log("Memcached error during set($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$key = urlencode($key);
|
||||
|
||||
$this->memcache->delete($key);
|
||||
$res = $this->memcache->getResultCode();
|
||||
if((DEBUG_CACHE === true) || (is_null(DEBUG_CACHE) && @$_GET['DEBUG_CACHE'])) {
|
||||
file_put_contents("data/cache.log", "Cache delete: $key\n", FILE_APPEND);
|
||||
}
|
||||
if($res != Memcached::RES_SUCCESS && $res != Memcached::RES_NOTFOUND) {
|
||||
error_log("Memcached error during delete($key): $res");
|
||||
}
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
|
||||
class APCCache implements CacheEngine {
|
||||
public $hits=0, $misses=0;
|
||||
|
||||
public function __construct(string $args) {
|
||||
// $args is not used, but is passed in when APC cache is created.
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = apc_fetch($key);
|
||||
if($val) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
apc_store($key, $val, $time);
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
apc_delete($key);
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
|
||||
class RedisCache implements CacheEngine {
|
||||
public $hits=0, $misses=0;
|
||||
private $redis=null;
|
||||
|
||||
public function __construct(string $args) {
|
||||
$this->redis = new Redis();
|
||||
$hp = explode(":", $args);
|
||||
$this->redis->pconnect($hp[0], $hp[1]);
|
||||
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
|
||||
$this->redis->setOption(Redis::OPT_PREFIX, 'shm:');
|
||||
}
|
||||
|
||||
public function get(string $key) {
|
||||
$val = $this->redis->get($key);
|
||||
if($val !== false) {
|
||||
$this->hits++;
|
||||
return $val;
|
||||
}
|
||||
else {
|
||||
$this->misses++;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, $val, int $time=0) {
|
||||
if($time > 0) {
|
||||
$this->redis->setEx($key, $time, $val);
|
||||
}
|
||||
else {
|
||||
$this->redis->set($key, $val);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $key) {
|
||||
$this->redis->delete($key);
|
||||
}
|
||||
|
||||
public function get_hits(): int {return $this->hits;}
|
||||
public function get_misses(): int {return $this->misses;}
|
||||
}
|
||||
// }}}
|
||||
/** @publicsection */
|
||||
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*/
|
||||
class Database {
|
||||
/**
|
||||
* The PDO database connection object, for anyone who wants direct access.
|
||||
* @var null|PDO
|
||||
*/
|
||||
private $db = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
public $dbtime = 0.0;
|
||||
|
||||
/**
|
||||
* Meta info about the database engine.
|
||||
* @var DBEngine|null
|
||||
*/
|
||||
private $engine = null;
|
||||
|
||||
/**
|
||||
* The currently active cache engine.
|
||||
* @var CacheEngine|null
|
||||
*/
|
||||
public $cache = 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 $query_count = 0;
|
||||
|
||||
/**
|
||||
* For now, only connect to the cache, as we will pretty much certainly
|
||||
* need it. There are some pages where all the data is in cache, so the
|
||||
* DB connection is on-demand.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->connect_cache();
|
||||
}
|
||||
|
||||
private function connect_cache() {
|
||||
$matches = array();
|
||||
if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(.*)://(.*)#", CACHE_DSN, $matches)) {
|
||||
if($matches[1] == "memcache") {
|
||||
$this->cache = new MemcacheCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "memcached") {
|
||||
$this->cache = new MemcachedCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "apc") {
|
||||
$this->cache = new APCCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "redis") {
|
||||
$this->cache = new RedisCache($matches[2]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->cache = new NoCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function connect_db() {
|
||||
# FIXME: detect ADODB URI, automatically translate PDO DSN
|
||||
|
||||
/*
|
||||
* Why does the abstraction layer act differently depending on the
|
||||
* back-end? Because PHP is deliberately retarded.
|
||||
*
|
||||
* http://stackoverflow.com/questions/237367
|
||||
*/
|
||||
$matches = array(); $db_user=null; $db_pass=null;
|
||||
if(preg_match("/user=([^;]*)/", DATABASE_DSN, $matches)) $db_user=$matches[1];
|
||||
if(preg_match("/password=([^;]*)/", DATABASE_DSN, $matches)) $db_pass=$matches[1];
|
||||
|
||||
// https://bugs.php.net/bug.php?id=70221
|
||||
$ka = DATABASE_KA;
|
||||
if(version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == "sqlite") {
|
||||
$ka = false;
|
||||
}
|
||||
|
||||
$db_params = array(
|
||||
PDO::ATTR_PERSISTENT => $ka,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
);
|
||||
$this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params);
|
||||
|
||||
$this->connect_engine();
|
||||
$this->engine->init($this->db);
|
||||
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
private function connect_engine() {
|
||||
if(preg_match("/^([^:]*)/", DATABASE_DSN, $matches)) $db_proto=$matches[1];
|
||||
else throw new SCoreException("Can't figure out database engine");
|
||||
|
||||
if($db_proto === "mysql") {
|
||||
$this->engine = new MySQL();
|
||||
}
|
||||
else if($db_proto === "pgsql") {
|
||||
$this->engine = new PostgreSQL();
|
||||
}
|
||||
else if($db_proto === "sqlite") {
|
||||
$this->engine = new SQLite();
|
||||
}
|
||||
else {
|
||||
die('Unknown PDO driver: '.$db_proto);
|
||||
}
|
||||
}
|
||||
|
||||
public function beginTransaction() {
|
||||
if ($this->transaction === false) {
|
||||
$this->db->beginTransaction();
|
||||
$this->transaction = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): bool {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->commit();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no connection currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function rollback(): bool {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->rollback();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no connection currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function escape(string $input): string {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
return $this->db->Quote($input);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $input): string {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->scoreql_to_sql($input);
|
||||
}
|
||||
|
||||
public function get_driver_name(): string {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->name;
|
||||
}
|
||||
|
||||
private function count_execs(string $sql, array $inputarray) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
|
||||
if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
|
||||
$text = $sql." -- ".join(", ", $inputarray)."\n";
|
||||
}
|
||||
else {
|
||||
$text = $sql."\n";
|
||||
}
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
if(!is_array($inputarray)) $this->query_count++;
|
||||
# handle 2-dimensional input arrays
|
||||
else if(is_array(reset($inputarray))) $this->query_count += sizeof($inputarray);
|
||||
else $this->query_count++;
|
||||
}
|
||||
|
||||
private function count_time(string $method, float $start) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$text = $method.":".(microtime(true) - $start)."\n";
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
$this->dbtime += microtime(true) - $start;
|
||||
}
|
||||
|
||||
public function execute(string $query, array $args=array()): PDOStatement {
|
||||
try {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
$this->count_execs($query, $args);
|
||||
$stmt = $this->db->prepare(
|
||||
"-- " . str_replace("%2F", "/", urlencode(@$_GET['q'])). "\n" .
|
||||
$query
|
||||
);
|
||||
// $stmt = $this->db->prepare($query);
|
||||
if (!array_key_exists(0, $args)) {
|
||||
foreach($args as $name=>$value) {
|
||||
if(is_numeric($value)) {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_INT);
|
||||
}
|
||||
else {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_STR);
|
||||
}
|
||||
}
|
||||
$stmt->execute();
|
||||
}
|
||||
else {
|
||||
$stmt->execute($args);
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
catch(PDOException $pdoe) {
|
||||
throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$data = $this->execute($query, $args)->fetchAll();
|
||||
$this->count_time("get_all", $_start);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_row(string $query, array $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_row", $_start);
|
||||
return $row ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_col(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[] = $row[0];
|
||||
}
|
||||
$this->count_time("get_col", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first row => the second row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_pairs(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[$row[0]] = $row[1];
|
||||
}
|
||||
$this->count_time("get_pairs", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get_one(string $query, array $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_one", $_start);
|
||||
return $row[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the last inserted row.
|
||||
*
|
||||
* @param string|null $seq
|
||||
* @return int
|
||||
*/
|
||||
public function get_last_insert_id(string $seq): int {
|
||||
if($this->engine->name == "pgsql") {
|
||||
return $this->db->lastInsertId($seq);
|
||||
}
|
||||
else {
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table from pseudo-SQL.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
*/
|
||||
public function create_table(string $name, string $data) {
|
||||
if(is_null($this->engine)) { $this->connect_engine(); }
|
||||
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
|
||||
$this->execute($this->engine->create_table_sql($name, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of tables present in the current database.
|
||||
*
|
||||
* @return int
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function count_tables(): int {
|
||||
if(is_null($this->db) || is_null($this->engine)) $this->connect_db();
|
||||
|
||||
if($this->engine->name === "mysql") {
|
||||
return count(
|
||||
$this->get_all("SHOW TABLES")
|
||||
);
|
||||
} else if ($this->engine->name === "pgsql") {
|
||||
return count(
|
||||
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||
);
|
||||
} else if ($this->engine->name === "sqlite") {
|
||||
return count(
|
||||
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
);
|
||||
} else {
|
||||
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockDatabase extends Database {
|
||||
/** @var int */
|
||||
private $query_id = 0;
|
||||
/** @var array */
|
||||
private $responses = array();
|
||||
/** @var \NoCache|null */
|
||||
public $cache = null;
|
||||
|
||||
public function __construct(array $responses = array()) {
|
||||
$this->cache = new NoCache();
|
||||
$this->responses = $responses;
|
||||
}
|
||||
|
||||
public function execute(string $query, array $params=array()): PDOStatement {
|
||||
log_debug("mock-database",
|
||||
"QUERY: " . $query .
|
||||
"\nARGS: " . var_export($params, true) .
|
||||
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
|
||||
);
|
||||
return $this->responses[$this->query_id++];
|
||||
}
|
||||
public function _execute(string $query, array $params=array()) {
|
||||
log_debug("mock-database",
|
||||
"QUERY: " . $query .
|
||||
"\nARGS: " . var_export($params, true) .
|
||||
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
|
||||
);
|
||||
return $this->responses[$this->query_id++];
|
||||
}
|
||||
|
||||
public function get_all(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_row(string $query, array $args=array()) {return $this->_execute($query, $args);}
|
||||
public function get_col(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_pairs(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_one(string $query, array $args=array()) {return $this->_execute($query, $args);}
|
||||
|
||||
public function get_last_insert_id(string $seq): int {return $this->query_id;}
|
||||
|
||||
public function scoreql_to_sql(string $sql): string {return $sql;}
|
||||
public function create_table(string $name, string $def) {}
|
||||
public function connect_engine() {}
|
||||
}
|
||||
|
402
core/database.php
Normal file
402
core/database.php
Normal file
@ -0,0 +1,402 @@
|
||||
<?php
|
||||
/**
|
||||
* A class for controlled database access
|
||||
*/
|
||||
class Database {
|
||||
/**
|
||||
* The PDO database connection object, for anyone who wants direct access.
|
||||
* @var null|PDO
|
||||
*/
|
||||
private $db = null;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
public $dbtime = 0.0;
|
||||
|
||||
/**
|
||||
* Meta info about the database engine.
|
||||
* @var DBEngine|null
|
||||
*/
|
||||
private $engine = null;
|
||||
|
||||
/**
|
||||
* The currently active cache engine.
|
||||
* @var CacheEngine|null
|
||||
*/
|
||||
public $cache = 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 $query_count = 0;
|
||||
|
||||
/**
|
||||
* For now, only connect to the cache, as we will pretty much certainly
|
||||
* need it. There are some pages where all the data is in cache, so the
|
||||
* DB connection is on-demand.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->connect_cache();
|
||||
}
|
||||
|
||||
private function connect_cache() {
|
||||
$matches = array();
|
||||
if(defined("CACHE_DSN") && CACHE_DSN && preg_match("#(.*)://(.*)#", CACHE_DSN, $matches)) {
|
||||
if($matches[1] == "memcache") {
|
||||
$this->cache = new MemcacheCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "memcached") {
|
||||
$this->cache = new MemcachedCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "apc") {
|
||||
$this->cache = new APCCache($matches[2]);
|
||||
}
|
||||
else if($matches[1] == "redis") {
|
||||
$this->cache = new RedisCache($matches[2]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->cache = new NoCache();
|
||||
}
|
||||
}
|
||||
|
||||
private function connect_db() {
|
||||
# FIXME: detect ADODB URI, automatically translate PDO DSN
|
||||
|
||||
/*
|
||||
* Why does the abstraction layer act differently depending on the
|
||||
* back-end? Because PHP is deliberately retarded.
|
||||
*
|
||||
* http://stackoverflow.com/questions/237367
|
||||
*/
|
||||
$matches = array(); $db_user=null; $db_pass=null;
|
||||
if(preg_match("/user=([^;]*)/", DATABASE_DSN, $matches)) $db_user=$matches[1];
|
||||
if(preg_match("/password=([^;]*)/", DATABASE_DSN, $matches)) $db_pass=$matches[1];
|
||||
|
||||
// https://bugs.php.net/bug.php?id=70221
|
||||
$ka = DATABASE_KA;
|
||||
if(version_compare(PHP_VERSION, "6.9.9") == 1 && $this->get_driver_name() == "sqlite") {
|
||||
$ka = false;
|
||||
}
|
||||
|
||||
$db_params = array(
|
||||
PDO::ATTR_PERSISTENT => $ka,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
);
|
||||
$this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params);
|
||||
|
||||
$this->connect_engine();
|
||||
$this->engine->init($this->db);
|
||||
|
||||
$this->beginTransaction();
|
||||
}
|
||||
|
||||
private function connect_engine() {
|
||||
if(preg_match("/^([^:]*)/", DATABASE_DSN, $matches)) $db_proto=$matches[1];
|
||||
else throw new SCoreException("Can't figure out database engine");
|
||||
|
||||
if($db_proto === "mysql") {
|
||||
$this->engine = new MySQL();
|
||||
}
|
||||
else if($db_proto === "pgsql") {
|
||||
$this->engine = new PostgreSQL();
|
||||
}
|
||||
else if($db_proto === "sqlite") {
|
||||
$this->engine = new SQLite();
|
||||
}
|
||||
else {
|
||||
die('Unknown PDO driver: '.$db_proto);
|
||||
}
|
||||
}
|
||||
|
||||
public function beginTransaction() {
|
||||
if ($this->transaction === false) {
|
||||
$this->db->beginTransaction();
|
||||
$this->transaction = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function commit(): bool {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->commit();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call commit() as there is no connection currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function rollback(): bool {
|
||||
if(!is_null($this->db)) {
|
||||
if ($this->transaction === true) {
|
||||
$this->transaction = false;
|
||||
return $this->db->rollback();
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no transaction currently open.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SCoreException("<p><b>Database Transaction Error:</b> Unable to call rollback() as there is no connection currently open.");
|
||||
}
|
||||
}
|
||||
|
||||
public function escape(string $input): string {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
return $this->db->Quote($input);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $input): string {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->scoreql_to_sql($input);
|
||||
}
|
||||
|
||||
public function get_driver_name(): string {
|
||||
if(is_null($this->engine)) $this->connect_engine();
|
||||
return $this->engine->name;
|
||||
}
|
||||
|
||||
private function count_execs(string $sql, array $inputarray) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$sql = trim(preg_replace('/\s+/msi', ' ', $sql));
|
||||
if(isset($inputarray) && is_array($inputarray) && !empty($inputarray)) {
|
||||
$text = $sql." -- ".join(", ", $inputarray)."\n";
|
||||
}
|
||||
else {
|
||||
$text = $sql."\n";
|
||||
}
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
if(!is_array($inputarray)) $this->query_count++;
|
||||
# handle 2-dimensional input arrays
|
||||
else if(is_array(reset($inputarray))) $this->query_count += sizeof($inputarray);
|
||||
else $this->query_count++;
|
||||
}
|
||||
|
||||
private function count_time(string $method, float $start) {
|
||||
if((DEBUG_SQL === true) || (is_null(DEBUG_SQL) && @$_GET['DEBUG_SQL'])) {
|
||||
$text = $method.":".(microtime(true) - $start)."\n";
|
||||
file_put_contents("data/sql.log", $text, FILE_APPEND);
|
||||
}
|
||||
$this->dbtime += microtime(true) - $start;
|
||||
}
|
||||
|
||||
public function execute(string $query, array $args=array()): PDOStatement {
|
||||
try {
|
||||
if(is_null($this->db)) $this->connect_db();
|
||||
$this->count_execs($query, $args);
|
||||
$stmt = $this->db->prepare(
|
||||
"-- " . str_replace("%2F", "/", urlencode(@$_GET['q'])). "\n" .
|
||||
$query
|
||||
);
|
||||
// $stmt = $this->db->prepare($query);
|
||||
if (!array_key_exists(0, $args)) {
|
||||
foreach($args as $name=>$value) {
|
||||
if(is_numeric($value)) {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_INT);
|
||||
}
|
||||
else {
|
||||
$stmt->bindValue(':'.$name, $value, PDO::PARAM_STR);
|
||||
}
|
||||
}
|
||||
$stmt->execute();
|
||||
}
|
||||
else {
|
||||
$stmt->execute($args);
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
catch(PDOException $pdoe) {
|
||||
throw new SCoreException($pdoe->getMessage()."<p><b>Query:</b> ".$query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a 2D array.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$data = $this->execute($query, $args)->fetchAll();
|
||||
$this->count_time("get_all", $_start);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_row(string $query, array $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_row", $_start);
|
||||
return $row ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the first column of each row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_col(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[] = $row[0];
|
||||
}
|
||||
$this->count_time("get_col", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return the the first row => the second row.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public function get_pairs(string $query, array $args=array()): array {
|
||||
$_start = microtime(true);
|
||||
$stmt = $this->execute($query, $args);
|
||||
$res = array();
|
||||
foreach($stmt as $row) {
|
||||
$res[$row[0]] = $row[1];
|
||||
}
|
||||
$this->count_time("get_pairs", $_start);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL query and return a single value.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $args
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get_one(string $query, array $args=array()) {
|
||||
$_start = microtime(true);
|
||||
$row = $this->execute($query, $args)->fetch();
|
||||
$this->count_time("get_one", $_start);
|
||||
return $row[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the last inserted row.
|
||||
*
|
||||
* @param string|null $seq
|
||||
* @return int
|
||||
*/
|
||||
public function get_last_insert_id(string $seq): int {
|
||||
if($this->engine->name == "pgsql") {
|
||||
return $this->db->lastInsertId($seq);
|
||||
}
|
||||
else {
|
||||
return $this->db->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table from pseudo-SQL.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $data
|
||||
*/
|
||||
public function create_table(string $name, string $data) {
|
||||
if(is_null($this->engine)) { $this->connect_engine(); }
|
||||
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
|
||||
$this->execute($this->engine->create_table_sql($name, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of tables present in the current database.
|
||||
*
|
||||
* @return int
|
||||
* @throws SCoreException
|
||||
*/
|
||||
public function count_tables(): int {
|
||||
if(is_null($this->db) || is_null($this->engine)) $this->connect_db();
|
||||
|
||||
if($this->engine->name === "mysql") {
|
||||
return count(
|
||||
$this->get_all("SHOW TABLES")
|
||||
);
|
||||
} else if ($this->engine->name === "pgsql") {
|
||||
return count(
|
||||
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
|
||||
);
|
||||
} else if ($this->engine->name === "sqlite") {
|
||||
return count(
|
||||
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
);
|
||||
} else {
|
||||
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockDatabase extends Database {
|
||||
/** @var int */
|
||||
private $query_id = 0;
|
||||
/** @var array */
|
||||
private $responses = array();
|
||||
/** @var \NoCache|null */
|
||||
public $cache = null;
|
||||
|
||||
public function __construct(array $responses = array()) {
|
||||
$this->cache = new NoCache();
|
||||
$this->responses = $responses;
|
||||
}
|
||||
|
||||
public function execute(string $query, array $params=array()): PDOStatement {
|
||||
log_debug("mock-database",
|
||||
"QUERY: " . $query .
|
||||
"\nARGS: " . var_export($params, true) .
|
||||
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
|
||||
);
|
||||
return $this->responses[$this->query_id++];
|
||||
}
|
||||
public function _execute(string $query, array $params=array()) {
|
||||
log_debug("mock-database",
|
||||
"QUERY: " . $query .
|
||||
"\nARGS: " . var_export($params, true) .
|
||||
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
|
||||
);
|
||||
return $this->responses[$this->query_id++];
|
||||
}
|
||||
|
||||
public function get_all(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_row(string $query, array $args=array()) {return $this->_execute($query, $args);}
|
||||
public function get_col(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_pairs(string $query, array $args=array()): array {return $this->_execute($query, $args);}
|
||||
public function get_one(string $query, array $args=array()) {return $this->_execute($query, $args);}
|
||||
|
||||
public function get_last_insert_id(string $seq): int {return $this->query_id;}
|
||||
|
||||
public function scoreql_to_sql(string $sql): string {return $sql;}
|
||||
public function create_table(string $name, string $def) {}
|
||||
public function connect_engine() {}
|
||||
}
|
||||
|
142
core/dbengine.php
Normal file
142
core/dbengine.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
class DBEngine {
|
||||
/** @var null|string */
|
||||
public $name = null;
|
||||
|
||||
public function init(PDO $db) {}
|
||||
|
||||
public function scoreql_to_sql(string $scoreql): string {
|
||||
return $scoreql;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
return 'CREATE TABLE '.$name.' ('.$data.')';
|
||||
}
|
||||
}
|
||||
|
||||
class MySQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "mysql";
|
||||
|
||||
public function init(PDO $db) {
|
||||
$db->exec("SET NAMES utf8;");
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$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", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "ENUM('Y', 'N')", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "DATETIME", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$ctes = "ENGINE=InnoDB DEFAULT CHARSET='utf8'";
|
||||
return 'CREATE TABLE '.$name.' ('.$data.') '.$ctes;
|
||||
}
|
||||
}
|
||||
|
||||
class PostgreSQL extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "pgsql";
|
||||
|
||||
public function init(PDO $db) {
|
||||
if(array_key_exists('REMOTE_ADDR', $_SERVER)) {
|
||||
$db->exec("SET application_name TO 'shimmie [{$_SERVER['REMOTE_ADDR']}]';");
|
||||
}
|
||||
else {
|
||||
$db->exec("SET application_name TO 'shimmie [local]';");
|
||||
}
|
||||
$db->exec("SET statement_timeout TO 10000;");
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$data = str_replace("SCORE_AIPK", "SERIAL PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "INET", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'t'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'f'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "BOOL", $data);
|
||||
$data = str_replace("SCORE_DATETIME", "TIMESTAMP", $data);
|
||||
$data = str_replace("SCORE_NOW", "current_timestamp", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "ILIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
return "CREATE TABLE $name ($data)";
|
||||
}
|
||||
}
|
||||
|
||||
// shimmie functions for export to sqlite
|
||||
function _unix_timestamp($date) { return strtotime($date); }
|
||||
function _now() { return date("Y-m-d h:i:s"); }
|
||||
function _floor($a) { return floor($a); }
|
||||
function _log($a, $b=null) {
|
||||
if(is_null($b)) return log($a);
|
||||
else return log($a, $b);
|
||||
}
|
||||
function _isnull($a) { return is_null($a); }
|
||||
function _md5($a) { return md5($a); }
|
||||
function _concat($a, $b) { return $a . $b; }
|
||||
function _lower($a) { return strtolower($a); }
|
||||
function _rand() { return rand(); }
|
||||
function _ln($n) { return log($n); }
|
||||
|
||||
class SQLite extends DBEngine {
|
||||
/** @var string */
|
||||
public $name = "sqlite";
|
||||
|
||||
public function init(PDO $db) {
|
||||
ini_set('sqlite.assoc_case', 0);
|
||||
$db->exec("PRAGMA foreign_keys = ON;");
|
||||
$db->sqliteCreateFunction('UNIX_TIMESTAMP', '_unix_timestamp', 1);
|
||||
$db->sqliteCreateFunction('now', '_now', 0);
|
||||
$db->sqliteCreateFunction('floor', '_floor', 1);
|
||||
$db->sqliteCreateFunction('log', '_log');
|
||||
$db->sqliteCreateFunction('isnull', '_isnull', 1);
|
||||
$db->sqliteCreateFunction('md5', '_md5', 1);
|
||||
$db->sqliteCreateFunction('concat', '_concat', 2);
|
||||
$db->sqliteCreateFunction('lower', '_lower', 1);
|
||||
$db->sqliteCreateFunction('rand', '_rand', 0);
|
||||
$db->sqliteCreateFunction('ln', '_ln', 1);
|
||||
}
|
||||
|
||||
public function scoreql_to_sql(string $data): string {
|
||||
$data = str_replace("SCORE_AIPK", "INTEGER PRIMARY KEY", $data);
|
||||
$data = str_replace("SCORE_INET", "VARCHAR(45)", $data);
|
||||
$data = str_replace("SCORE_BOOL_Y", "'Y'", $data);
|
||||
$data = str_replace("SCORE_BOOL_N", "'N'", $data);
|
||||
$data = str_replace("SCORE_BOOL", "CHAR(1)", $data);
|
||||
$data = str_replace("SCORE_NOW", "\"1970-01-01\"", $data);
|
||||
$data = str_replace("SCORE_STRNORM", "lower", $data);
|
||||
$data = str_replace("SCORE_ILIKE", "LIKE", $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function create_table_sql(string $name, string $data): string {
|
||||
$data = $this->scoreql_to_sql($data);
|
||||
$cols = array();
|
||||
$extras = "";
|
||||
foreach(explode(",", $data) as $bit) {
|
||||
$matches = array();
|
||||
if(preg_match("/(UNIQUE)? ?INDEX\s*\((.*)\)/", $bit, $matches)) {
|
||||
$uni = $matches[1];
|
||||
$col = $matches[2];
|
||||
$extras .= "CREATE $uni INDEX {$name}_{$col} ON {$name}({$col});";
|
||||
}
|
||||
else {
|
||||
$cols[] = $bit;
|
||||
}
|
||||
}
|
||||
$cols_redone = implode(", ", $cols);
|
||||
return "CREATE TABLE $name ($cols_redone); $extras";
|
||||
}
|
||||
}
|
@ -1,28 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* All the imageboard-specific bits of code should be in this file, everything
|
||||
* else in /core should be standard SCore bits.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \page search Shimmie2: Searching
|
||||
*
|
||||
* The current search system is built of several search item -> image ID list
|
||||
* translators, eg:
|
||||
*
|
||||
* \li the item "fred" will search the image_tags table to find image IDs with the fred tag
|
||||
* \li the item "size=640x480" will search the images table to find image IDs of 640x480 images
|
||||
*
|
||||
* So the search "fred size=640x480" will calculate two lists and take the
|
||||
* intersection. (There are some optimisations in there making it more
|
||||
* complicated behind the scenes, but as long as you can turn a single word
|
||||
* into a list of image IDs, making a search plugin should be simple)
|
||||
*/
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Classes *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Class Image
|
||||
*
|
||||
@ -1045,215 +1021,3 @@ class Image {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Tag
|
||||
*
|
||||
* A class for organising the tag related functions.
|
||||
*
|
||||
* All the methods are static, one should never actually use a tag object.
|
||||
*
|
||||
*/
|
||||
class Tag {
|
||||
public static function implode(array $tags): string {
|
||||
sort($tags);
|
||||
$tags = implode(' ', $tags);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human-supplied string into a valid tag array.
|
||||
*
|
||||
* @param string $tags
|
||||
* @param bool $tagme add "tagme" if the string is empty
|
||||
* @return string[]
|
||||
*/
|
||||
public static function explode(string $tags, bool $tagme=true): array {
|
||||
global $database;
|
||||
|
||||
$tags = explode(' ', trim($tags));
|
||||
|
||||
/* sanitise by removing invisible / dodgy characters */
|
||||
$tag_array = array();
|
||||
foreach($tags as $tag) {
|
||||
$tag = preg_replace("/\s/", "", $tag); # whitespace
|
||||
$tag = preg_replace('/\x20(\x0e|\x0f)/', '', $tag); # unicode RTL
|
||||
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
|
||||
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
|
||||
$tag = trim($tag, ", \t\n\r\0\x0B");
|
||||
|
||||
if(mb_strlen($tag, 'UTF-8') > 255){
|
||||
flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!empty($tag)) {
|
||||
$tag_array[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/* if user supplied a blank string, add "tagme" */
|
||||
if(count($tag_array) === 0 && $tagme) {
|
||||
$tag_array = array("tagme");
|
||||
}
|
||||
|
||||
/* resolve aliases */
|
||||
$new = array();
|
||||
$i = 0;
|
||||
$tag_count = count($tag_array);
|
||||
while($i<$tag_count) {
|
||||
$tag = $tag_array[$i];
|
||||
$negative = '';
|
||||
if(!empty($tag) && ($tag[0] == '-')) {
|
||||
$negative = '-';
|
||||
$tag = substr($tag, 1);
|
||||
}
|
||||
|
||||
$newtags = $database->get_one(
|
||||
$database->scoreql_to_sql("
|
||||
SELECT newtag
|
||||
FROM aliases
|
||||
WHERE SCORE_STRNORM(oldtag)=SCORE_STRNORM(:tag)
|
||||
"),
|
||||
array("tag"=>$tag)
|
||||
);
|
||||
if(empty($newtags)) {
|
||||
//tag has no alias, use old tag
|
||||
$aliases = array($tag);
|
||||
}
|
||||
else {
|
||||
$aliases = explode(" ", $newtags); // Tag::explode($newtags); - recursion can be infinite
|
||||
}
|
||||
|
||||
foreach($aliases as $alias) {
|
||||
if(!in_array($alias, $new)) {
|
||||
if($tag == $alias) {
|
||||
$new[] = $negative.$alias;
|
||||
}
|
||||
elseif(!in_array($alias, $tag_array)) {
|
||||
$tag_array[] = $negative.$alias;
|
||||
$tag_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
/* remove any duplicate tags */
|
||||
$tag_array = array_iunique($new);
|
||||
|
||||
/* tidy up */
|
||||
sort($tag_array);
|
||||
|
||||
return $tag_array;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc functions *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Move a file from PHP's temporary area into shimmie's image storage
|
||||
* hierarchy, or throw an exception trying.
|
||||
*
|
||||
* @param DataUploadEvent $event
|
||||
* @throws UploadException
|
||||
*/
|
||||
function move_upload_to_archive(DataUploadEvent $event) {
|
||||
$target = warehouse_path("images", $event->hash);
|
||||
if(!@copy($event->tmpname, $target)) {
|
||||
$errors = error_get_last();
|
||||
throw new UploadException(
|
||||
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
|
||||
"{$errors['type']} / {$errors['message']}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a directory full of images
|
||||
*
|
||||
* @param $base string
|
||||
* @return array|string[]
|
||||
*/
|
||||
function add_dir($base) {
|
||||
$results = array();
|
||||
|
||||
foreach(list_files($base) as $full_path) {
|
||||
$short_path = str_replace($base, "", $full_path);
|
||||
$filename = basename($full_path);
|
||||
|
||||
$tags = path_to_tags($short_path);
|
||||
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
||||
try {
|
||||
add_image($full_path, $filename, $tags);
|
||||
$result .= "ok";
|
||||
}
|
||||
catch(UploadException $ex) {
|
||||
$result .= "failed: ".$ex->getMessage();
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tmpname
|
||||
* @param string $filename
|
||||
* @param string $tags
|
||||
* @throws UploadException
|
||||
*/
|
||||
function add_image($tmpname, $filename, $tags) {
|
||||
assert(file_exists($tmpname));
|
||||
|
||||
$pathinfo = pathinfo($filename);
|
||||
if(!array_key_exists('extension', $pathinfo)) {
|
||||
throw new UploadException("File has no extension");
|
||||
}
|
||||
$metadata = array();
|
||||
$metadata['filename'] = $pathinfo['basename'];
|
||||
$metadata['extension'] = $pathinfo['extension'];
|
||||
$metadata['tags'] = Tag::explode($tags);
|
||||
$metadata['source'] = null;
|
||||
$event = new DataUploadEvent($tmpname, $metadata);
|
||||
send_event($event);
|
||||
if($event->image_id == -1) {
|
||||
throw new UploadException("File type not recognised");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a full size pair of dimensions, return a pair scaled down to fit
|
||||
* into the configured thumbnail square, with ratio intact
|
||||
*
|
||||
* @param int $orig_width
|
||||
* @param int $orig_height
|
||||
* @return integer[]
|
||||
*/
|
||||
function get_thumbnail_size(int $orig_width, int $orig_height) {
|
||||
global $config;
|
||||
|
||||
if($orig_width === 0) $orig_width = 192;
|
||||
if($orig_height === 0) $orig_height = 192;
|
||||
|
||||
if($orig_width > $orig_height * 5) $orig_width = $orig_height * 5;
|
||||
if($orig_height > $orig_width * 5) $orig_height = $orig_width * 5;
|
||||
|
||||
$max_width = $config->get_int('thumb_width');
|
||||
$max_height = $config->get_int('thumb_height');
|
||||
|
||||
$xscale = ($max_height / $orig_height);
|
||||
$yscale = ($max_width / $orig_width);
|
||||
$scale = ($xscale < $yscale) ? $xscale : $yscale;
|
||||
|
||||
if($scale > 1 && $config->get_bool('thumb_upscale')) {
|
||||
return array((int)$orig_width, (int)$orig_height);
|
||||
}
|
||||
else {
|
||||
return array((int)($orig_width*$scale), (int)($orig_height*$scale));
|
||||
}
|
||||
}
|
||||
|
107
core/imageboard/misc.php
Normal file
107
core/imageboard/misc.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc functions *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Move a file from PHP's temporary area into shimmie's image storage
|
||||
* hierarchy, or throw an exception trying.
|
||||
*
|
||||
* @param DataUploadEvent $event
|
||||
* @throws UploadException
|
||||
*/
|
||||
function move_upload_to_archive(DataUploadEvent $event) {
|
||||
$target = warehouse_path("images", $event->hash);
|
||||
if(!@copy($event->tmpname, $target)) {
|
||||
$errors = error_get_last();
|
||||
throw new UploadException(
|
||||
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
|
||||
"{$errors['type']} / {$errors['message']}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a directory full of images
|
||||
*
|
||||
* @param $base string
|
||||
* @return array|string[]
|
||||
*/
|
||||
function add_dir($base) {
|
||||
$results = array();
|
||||
|
||||
foreach(list_files($base) as $full_path) {
|
||||
$short_path = str_replace($base, "", $full_path);
|
||||
$filename = basename($full_path);
|
||||
|
||||
$tags = path_to_tags($short_path);
|
||||
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
||||
try {
|
||||
add_image($full_path, $filename, $tags);
|
||||
$result .= "ok";
|
||||
}
|
||||
catch(UploadException $ex) {
|
||||
$result .= "failed: ".$ex->getMessage();
|
||||
}
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tmpname
|
||||
* @param string $filename
|
||||
* @param string $tags
|
||||
* @throws UploadException
|
||||
*/
|
||||
function add_image($tmpname, $filename, $tags) {
|
||||
assert(file_exists($tmpname));
|
||||
|
||||
$pathinfo = pathinfo($filename);
|
||||
if(!array_key_exists('extension', $pathinfo)) {
|
||||
throw new UploadException("File has no extension");
|
||||
}
|
||||
$metadata = array();
|
||||
$metadata['filename'] = $pathinfo['basename'];
|
||||
$metadata['extension'] = $pathinfo['extension'];
|
||||
$metadata['tags'] = Tag::explode($tags);
|
||||
$metadata['source'] = null;
|
||||
$event = new DataUploadEvent($tmpname, $metadata);
|
||||
send_event($event);
|
||||
if($event->image_id == -1) {
|
||||
throw new UploadException("File type not recognised");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a full size pair of dimensions, return a pair scaled down to fit
|
||||
* into the configured thumbnail square, with ratio intact
|
||||
*
|
||||
* @param int $orig_width
|
||||
* @param int $orig_height
|
||||
* @return integer[]
|
||||
*/
|
||||
function get_thumbnail_size(int $orig_width, int $orig_height) {
|
||||
global $config;
|
||||
|
||||
if($orig_width === 0) $orig_width = 192;
|
||||
if($orig_height === 0) $orig_height = 192;
|
||||
|
||||
if($orig_width > $orig_height * 5) $orig_width = $orig_height * 5;
|
||||
if($orig_height > $orig_width * 5) $orig_height = $orig_width * 5;
|
||||
|
||||
$max_width = $config->get_int('thumb_width');
|
||||
$max_height = $config->get_int('thumb_height');
|
||||
|
||||
$xscale = ($max_height / $orig_height);
|
||||
$yscale = ($max_width / $orig_width);
|
||||
$scale = ($xscale < $yscale) ? $xscale : $yscale;
|
||||
|
||||
if($scale > 1 && $config->get_bool('thumb_upscale')) {
|
||||
return array((int)$orig_width, (int)$orig_height);
|
||||
}
|
||||
else {
|
||||
return array((int)($orig_width*$scale), (int)($orig_height*$scale));
|
||||
}
|
||||
}
|
49
core/imageboard/search.php
Normal file
49
core/imageboard/search.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
class Querylet {
|
||||
/** @var string */
|
||||
public $sql;
|
||||
/** @var array */
|
||||
public $variables;
|
||||
|
||||
public function __construct(string $sql, array $variables=array()) {
|
||||
$this->sql = $sql;
|
||||
$this->variables = $variables;
|
||||
}
|
||||
|
||||
public function append(Querylet $querylet) {
|
||||
$this->sql .= $querylet->sql;
|
||||
$this->variables = array_merge($this->variables, $querylet->variables);
|
||||
}
|
||||
|
||||
public function append_sql(string $sql) {
|
||||
$this->sql .= $sql;
|
||||
}
|
||||
|
||||
public function add_variable($var) {
|
||||
$this->variables[] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
class TagQuerylet {
|
||||
/** @var string */
|
||||
public $tag;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(string $tag, bool $positive) {
|
||||
$this->tag = $tag;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
||||
|
||||
class ImgQuerylet {
|
||||
/** @var \Querylet */
|
||||
public $qlet;
|
||||
/** @var bool */
|
||||
public $positive;
|
||||
|
||||
public function __construct(Querylet $qlet, bool $positive) {
|
||||
$this->qlet = $qlet;
|
||||
$this->positive = $positive;
|
||||
}
|
||||
}
|
104
core/imageboard/tag.php
Normal file
104
core/imageboard/tag.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Tag
|
||||
*
|
||||
* A class for organising the tag related functions.
|
||||
*
|
||||
* All the methods are static, one should never actually use a tag object.
|
||||
*
|
||||
*/
|
||||
class Tag {
|
||||
public static function implode(array $tags): string {
|
||||
sort($tags);
|
||||
$tags = implode(' ', $tags);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human-supplied string into a valid tag array.
|
||||
*
|
||||
* @param string $tags
|
||||
* @param bool $tagme add "tagme" if the string is empty
|
||||
* @return string[]
|
||||
*/
|
||||
public static function explode(string $tags, bool $tagme=true): array {
|
||||
global $database;
|
||||
|
||||
$tags = explode(' ', trim($tags));
|
||||
|
||||
/* sanitise by removing invisible / dodgy characters */
|
||||
$tag_array = array();
|
||||
foreach($tags as $tag) {
|
||||
$tag = preg_replace("/\s/", "", $tag); # whitespace
|
||||
$tag = preg_replace('/\x20(\x0e|\x0f)/', '', $tag); # unicode RTL
|
||||
$tag = preg_replace("/\.+/", ".", $tag); # strings of dots?
|
||||
$tag = preg_replace("/^(\.+[\/\\\\])+/", "", $tag); # trailing slashes?
|
||||
$tag = trim($tag, ", \t\n\r\0\x0B");
|
||||
|
||||
if(mb_strlen($tag, 'UTF-8') > 255){
|
||||
flash_message("The tag below is longer than 255 characters, please use a shorter tag.\n$tag\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!empty($tag)) {
|
||||
$tag_array[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/* if user supplied a blank string, add "tagme" */
|
||||
if(count($tag_array) === 0 && $tagme) {
|
||||
$tag_array = array("tagme");
|
||||
}
|
||||
|
||||
/* resolve aliases */
|
||||
$new = array();
|
||||
$i = 0;
|
||||
$tag_count = count($tag_array);
|
||||
while($i<$tag_count) {
|
||||
$tag = $tag_array[$i];
|
||||
$negative = '';
|
||||
if(!empty($tag) && ($tag[0] == '-')) {
|
||||
$negative = '-';
|
||||
$tag = substr($tag, 1);
|
||||
}
|
||||
|
||||
$newtags = $database->get_one(
|
||||
$database->scoreql_to_sql("
|
||||
SELECT newtag
|
||||
FROM aliases
|
||||
WHERE SCORE_STRNORM(oldtag)=SCORE_STRNORM(:tag)
|
||||
"),
|
||||
array("tag"=>$tag)
|
||||
);
|
||||
if(empty($newtags)) {
|
||||
//tag has no alias, use old tag
|
||||
$aliases = array($tag);
|
||||
}
|
||||
else {
|
||||
$aliases = explode(" ", $newtags); // Tag::explode($newtags); - recursion can be infinite
|
||||
}
|
||||
|
||||
foreach($aliases as $alias) {
|
||||
if(!in_array($alias, $new)) {
|
||||
if($tag == $alias) {
|
||||
$new[] = $negative.$alias;
|
||||
}
|
||||
elseif(!in_array($alias, $tag_array)) {
|
||||
$tag_array[] = $negative.$alias;
|
||||
$tag_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
/* remove any duplicate tags */
|
||||
$tag_array = array_iunique($new);
|
||||
|
||||
/* tidy up */
|
||||
sort($tag_array);
|
||||
|
||||
return $tag_array;
|
||||
}
|
||||
}
|
100
core/logging.php
Normal file
100
core/logging.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Logging convenience *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
define("SCORE_LOG_CRITICAL", 50);
|
||||
define("SCORE_LOG_ERROR", 40);
|
||||
define("SCORE_LOG_WARNING", 30);
|
||||
define("SCORE_LOG_INFO", 20);
|
||||
define("SCORE_LOG_DEBUG", 10);
|
||||
define("SCORE_LOG_NOTSET", 0);
|
||||
|
||||
/**
|
||||
* A shorthand way to send a LogEvent
|
||||
*
|
||||
* When parsing a user request, a flash message should give info to the user
|
||||
* When taking action, a log event should be stored by the server
|
||||
* Quite often, both of these happen at once, hence log_*() having $flash
|
||||
*
|
||||
* $flash = null (default) - log to server only, no flash message
|
||||
* $flash = true - show the message to the user as well
|
||||
* $flash = "some string" - log the message, flash the string
|
||||
*
|
||||
* @param string $section
|
||||
* @param int $priority
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_msg(string $section, int $priority, string $message, $flash=false, $args=array()) {
|
||||
send_event(new LogEvent($section, $priority, $message, $args));
|
||||
$threshold = defined("CLI_LOG_LEVEL") ? CLI_LOG_LEVEL : 0;
|
||||
|
||||
if((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ($priority >= $threshold)) {
|
||||
print date("c")." $section: $message\n";
|
||||
}
|
||||
if($flash === true) {
|
||||
flash_message($message);
|
||||
}
|
||||
else if(is_string($flash)) {
|
||||
flash_message($flash);
|
||||
}
|
||||
}
|
||||
|
||||
// More shorthand ways of logging
|
||||
/**
|
||||
* @param string $section
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_debug( string $section, string $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_DEBUG, $message, $flash, $args);}
|
||||
/**
|
||||
* @param string $section
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_info( string $section, string $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_INFO, $message, $flash, $args);}
|
||||
/**
|
||||
* @param string $section
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_warning( string $section, string $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_WARNING, $message, $flash, $args);}
|
||||
/**
|
||||
* @param string $section
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_error( string $section, string $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_ERROR, $message, $flash, $args);}
|
||||
/**
|
||||
* @param string $section
|
||||
* @param string $message
|
||||
* @param bool|string $flash
|
||||
* @param array $args
|
||||
*/
|
||||
function log_critical(string $section, string $message, $flash=false, $args=array()) {log_msg($section, SCORE_LOG_CRITICAL, $message, $flash, $args);}
|
||||
|
||||
|
||||
/**
|
||||
* Get a unique ID for this request, useful for grouping log messages.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_request_id(): string {
|
||||
static $request_id = null;
|
||||
if(!$request_id) {
|
||||
// not completely trustworthy, as a user can spoof this
|
||||
if(@$_SERVER['HTTP_X_VARNISH']) {
|
||||
$request_id = $_SERVER['HTTP_X_VARNISH'];
|
||||
}
|
||||
else {
|
||||
$request_id = "P" . uniqid();
|
||||
}
|
||||
}
|
||||
return $request_id;
|
||||
}
|
@ -333,33 +333,17 @@ class Page {
|
||||
}
|
||||
|
||||
/*** Generate CSS cache files ***/
|
||||
$css_lib_latest = $config_latest;
|
||||
$css_lib_files = zglob("lib/vendor/css/*.css");
|
||||
foreach($css_lib_files as $css) {
|
||||
$css_lib_latest = max($css_lib_latest, filemtime($css));
|
||||
}
|
||||
$css_lib_md5 = md5(serialize($css_lib_files));
|
||||
$css_lib_cache_file = data_path("cache/style.lib.{$theme_name}.{$css_lib_latest}.{$css_lib_md5}.css");
|
||||
if(!file_exists($css_lib_cache_file)) {
|
||||
$css_lib_data = "";
|
||||
foreach($css_lib_files as $file) {
|
||||
$file_data = file_get_contents($file);
|
||||
$pattern = '/url[\s]*\([\s]*["\']?([^"\'\)]+)["\']?[\s]*\)/';
|
||||
$replace = 'url("../../'.dirname($file).'/$1")';
|
||||
$file_data = preg_replace($pattern, $replace, $file_data);
|
||||
$css_lib_data .= $file_data . "\n";
|
||||
}
|
||||
file_put_contents($css_lib_cache_file, $css_lib_data);
|
||||
}
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_lib_cache_file' type='text/css'>", 43);
|
||||
|
||||
$css_latest = $config_latest;
|
||||
$css_files = array_merge(zglob("lib/shimmie.css"), zglob("ext/{".ENABLED_EXTS."}/style.css"), zglob("themes/$theme_name/style.css"));
|
||||
$css_files = array_merge(
|
||||
zglob("lib/shimmie.css"),
|
||||
zglob("ext/{".ENABLED_EXTS."}/style.css"),
|
||||
zglob("themes/$theme_name/style.css"),
|
||||
);
|
||||
foreach($css_files as $css) {
|
||||
$css_latest = max($css_latest, filemtime($css));
|
||||
}
|
||||
$css_md5 = md5(serialize($css_files));
|
||||
$css_cache_file = data_path("cache/style.main.{$theme_name}.{$css_latest}.{$css_md5}.css");
|
||||
$css_cache_file = data_path("cache/style.{$theme_name}.{$css_latest}.{$css_md5}.css");
|
||||
if(!file_exists($css_cache_file)) {
|
||||
$css_data = "";
|
||||
foreach($css_files as $file) {
|
||||
@ -374,29 +358,24 @@ class Page {
|
||||
$this->add_html_header("<link rel='stylesheet' href='$data_href/$css_cache_file' type='text/css'>", 100);
|
||||
|
||||
/*** Generate JS cache files ***/
|
||||
$js_lib_latest = $config_latest;
|
||||
$js_lib_files = zglob("lib/vendor/js/*.js");
|
||||
foreach($js_lib_files as $js) {
|
||||
$js_lib_latest = max($js_lib_latest, filemtime($js));
|
||||
}
|
||||
$js_lib_md5 = md5(serialize($js_lib_files));
|
||||
$js_lib_cache_file = data_path("cache/script.lib.{$theme_name}.{$js_lib_latest}.{$js_lib_md5}.js");
|
||||
if(!file_exists($js_lib_cache_file)) {
|
||||
$js_data = "";
|
||||
foreach($js_lib_files as $file) {
|
||||
$js_data .= file_get_contents($file) . "\n";
|
||||
}
|
||||
file_put_contents($js_lib_cache_file, $js_data);
|
||||
}
|
||||
$this->add_html_header("<script src='$data_href/$js_lib_cache_file' type='text/javascript'></script>", 45);
|
||||
|
||||
$js_latest = $config_latest;
|
||||
$js_files = array_merge(zglob("lib/shimmie.js"), zglob("ext/{".ENABLED_EXTS."}/script.js"), zglob("themes/$theme_name/script.js"));
|
||||
$js_files = array_merge(
|
||||
[
|
||||
"vendor/bower-asset/jquery/dist/jquery.min.js",
|
||||
"vendor/bower-asset/jquery-timeago/jquery.timeago.js",
|
||||
"vendor/bower-asset/tablesorter/jquery.tablesorter.min.js",
|
||||
"vendor/bower-asset/js-cookie/src/js.cookie.js",
|
||||
"lib/modernizr-3.3.1.custom.js",
|
||||
],
|
||||
zglob("lib/shimmie.js"),
|
||||
zglob("ext/{".ENABLED_EXTS."}/script.js"),
|
||||
zglob("themes/$theme_name/script.js")
|
||||
);
|
||||
foreach($js_files as $js) {
|
||||
$js_latest = max($js_latest, filemtime($js));
|
||||
}
|
||||
$js_md5 = md5(serialize($js_files));
|
||||
$js_cache_file = data_path("cache/script.main.{$theme_name}.{$js_latest}.{$js_md5}.js");
|
||||
$js_cache_file = data_path("cache/script.{$theme_name}.{$js_latest}.{$js_md5}.js");
|
||||
if(!file_exists($js_cache_file)) {
|
||||
$js_data = "";
|
||||
foreach($js_files as $file) {
|
||||
@ -407,6 +386,3 @@ class Page {
|
||||
$this->add_html_header("<script src='$data_href/$js_cache_file' type='text/javascript'></script>", 100);
|
||||
}
|
||||
}
|
||||
|
||||
class MockPage extends Page {
|
||||
}
|
779
core/polyfills.php
Normal file
779
core/polyfills.php
Normal file
@ -0,0 +1,779 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Things which should be in the core API *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Remove an item from an array
|
||||
*
|
||||
* @param array $array
|
||||
* @param mixed $to_remove
|
||||
* @return array
|
||||
*/
|
||||
function array_remove(array $array, $to_remove): array {
|
||||
$array = array_unique($array);
|
||||
$a2 = array();
|
||||
foreach($array as $existing) {
|
||||
if($existing != $to_remove) {
|
||||
$a2[] = $existing;
|
||||
}
|
||||
}
|
||||
return $a2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to an array.
|
||||
*
|
||||
* Also removes duplicate values from the array.
|
||||
*
|
||||
* @param array $array
|
||||
* @param mixed $element
|
||||
* @return array
|
||||
*/
|
||||
function array_add(array $array, $element): array {
|
||||
// Could we just use array_push() ?
|
||||
// http://www.php.net/manual/en/function.array-push.php
|
||||
$array[] = $element;
|
||||
$array = array_unique($array);
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the unique elements of an array, case insensitively
|
||||
*
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
function array_iunique(array $array): array {
|
||||
$ok = array();
|
||||
foreach($array as $element) {
|
||||
$found = false;
|
||||
foreach($ok as $existing) {
|
||||
if(strtolower($element) == strtolower($existing)) {
|
||||
$found = true; break;
|
||||
}
|
||||
}
|
||||
if(!$found) {
|
||||
$ok[] = $element;
|
||||
}
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out if an IP is in a specified range
|
||||
*
|
||||
* from http://uk.php.net/network
|
||||
*
|
||||
* @param string $IP
|
||||
* @param string $CIDR
|
||||
* @return bool
|
||||
*/
|
||||
function ip_in_range(string $IP, string $CIDR): bool {
|
||||
list ($net, $mask) = explode("/", $CIDR);
|
||||
|
||||
$ip_net = ip2long ($net);
|
||||
$ip_mask = ~((1 << (32 - $mask)) - 1);
|
||||
|
||||
$ip_ip = ip2long ($IP);
|
||||
|
||||
$ip_ip_net = $ip_ip & $ip_mask;
|
||||
|
||||
return ($ip_ip_net == $ip_net);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entire file heirachy
|
||||
*
|
||||
* from a patch by Christian Walde; only intended for use in the
|
||||
* "extension manager" extension, but it seems to fit better here
|
||||
*
|
||||
* @param string $f
|
||||
*/
|
||||
function deltree(string $f) {
|
||||
//Because Windows (I know, bad excuse)
|
||||
if(PHP_OS === 'WINNT') {
|
||||
$real = realpath($f);
|
||||
$path = realpath('./').'\\'.str_replace('/', '\\', $f);
|
||||
if($path != $real) {
|
||||
rmdir($path);
|
||||
}
|
||||
else {
|
||||
foreach(glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
}
|
||||
else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (is_link($f)) {
|
||||
unlink($f);
|
||||
}
|
||||
else if(is_dir($f)) {
|
||||
foreach(glob($f.'/*') as $sf) {
|
||||
if (is_dir($sf) && !is_link($sf)) {
|
||||
deltree($sf);
|
||||
}
|
||||
else {
|
||||
unlink($sf);
|
||||
}
|
||||
}
|
||||
rmdir($f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an entire file hierarchy
|
||||
*
|
||||
* from a comment on http://uk.php.net/copy
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $target
|
||||
*/
|
||||
function full_copy(string $source, string $target) {
|
||||
if(is_dir($source)) {
|
||||
@mkdir($target);
|
||||
|
||||
$d = dir($source);
|
||||
|
||||
while(FALSE !== ($entry = $d->read())) {
|
||||
if($entry == '.' || $entry == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$Entry = $source . '/' . $entry;
|
||||
if(is_dir($Entry)) {
|
||||
full_copy($Entry, $target . '/' . $entry);
|
||||
continue;
|
||||
}
|
||||
copy($Entry, $target . '/' . $entry);
|
||||
}
|
||||
$d->close();
|
||||
}
|
||||
else {
|
||||
copy($source, $target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all the regular files in a directory and subdirectories
|
||||
*
|
||||
* @param string $base
|
||||
* @param string $_sub_dir
|
||||
* @return array file list
|
||||
*/
|
||||
function list_files(string $base, string $_sub_dir=""): array {
|
||||
assert(is_dir($base));
|
||||
|
||||
$file_list = array();
|
||||
|
||||
$files = array();
|
||||
$dir = opendir("$base/$_sub_dir");
|
||||
while($f = readdir($dir)) {
|
||||
$files[] = $f;
|
||||
}
|
||||
closedir($dir);
|
||||
sort($files);
|
||||
|
||||
foreach($files as $filename) {
|
||||
$full_path = "$base/$_sub_dir/$filename";
|
||||
|
||||
if(is_link($full_path)) {
|
||||
// ignore
|
||||
}
|
||||
else if(is_dir($full_path)) {
|
||||
if(!($filename == "." || $filename == "..")) {
|
||||
//subdirectory found
|
||||
$file_list = array_merge(
|
||||
$file_list,
|
||||
list_files($base, "$_sub_dir/$filename")
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$full_path = str_replace("//", "/", $full_path);
|
||||
$file_list[] = $full_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $file_list;
|
||||
}
|
||||
|
||||
if (!function_exists('http_parse_headers')) { #http://www.php.net/manual/en/function.http-parse-headers.php#112917
|
||||
|
||||
/**
|
||||
* @param string $raw_headers
|
||||
* @return string[]
|
||||
*/
|
||||
function http_parse_headers ($raw_headers){
|
||||
$headers = array(); // $headers = [];
|
||||
|
||||
foreach (explode("\n", $raw_headers) as $i => $h) {
|
||||
$h = explode(':', $h, 2);
|
||||
|
||||
if (isset($h[1])){
|
||||
if(!isset($headers[$h[0]])){
|
||||
$headers[$h[0]] = trim($h[1]);
|
||||
}else if(is_array($headers[$h[0]])){
|
||||
$tmp = array_merge($headers[$h[0]],array(trim($h[1])));
|
||||
$headers[$h[0]] = $tmp;
|
||||
}else{
|
||||
$tmp = array_merge(array($headers[$h[0]]),array(trim($h[1])));
|
||||
$headers[$h[0]] = $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Headers can sometimes be lowercase which will cause issues.
|
||||
* In cases like these, we need to make sure to check for them if the camelcase version does not exist.
|
||||
*
|
||||
* @param array $headers
|
||||
* @param string $name
|
||||
* @return string|bool
|
||||
*/
|
||||
function findHeader(array $headers, string $name) {
|
||||
if (!is_array($headers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$header = false;
|
||||
|
||||
if(array_key_exists($name, $headers)) {
|
||||
$header = $headers[$name];
|
||||
} else {
|
||||
$headers = array_change_key_case($headers); // convert all to lower case.
|
||||
$lc_name = strtolower($name);
|
||||
|
||||
if(array_key_exists($lc_name, $headers)) {
|
||||
$header = $headers[$lc_name];
|
||||
}
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
if (!function_exists('mb_strlen')) {
|
||||
// TODO: we should warn the admin that they are missing multibyte support
|
||||
function mb_strlen($str, $encoding) {
|
||||
return strlen($str);
|
||||
}
|
||||
function mb_internal_encoding($encoding) {}
|
||||
function mb_strtolower($str) {
|
||||
return strtolower($str);
|
||||
}
|
||||
}
|
||||
|
||||
const MIME_TYPE_MAP = [
|
||||
'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png',
|
||||
'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon',
|
||||
'swf' => 'application/x-shockwave-flash', 'video/x-flv' => 'flv',
|
||||
'svg' => 'image/svg+xml', 'pdf' => 'application/pdf',
|
||||
'zip' => 'application/zip', 'gz' => 'application/x-gzip',
|
||||
'tar' => 'application/x-tar', 'bz' => 'application/x-bzip',
|
||||
'bz2' => 'application/x-bzip2', 'txt' => 'text/plain',
|
||||
'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html',
|
||||
'css' => 'text/css', 'js' => 'text/javascript',
|
||||
'xml' => 'text/xml', 'xsl' => 'application/xsl+xml',
|
||||
'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
|
||||
'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
|
||||
'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php',
|
||||
'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get MIME type for file
|
||||
*
|
||||
* The contents of this function are taken from the __getMimeType() function
|
||||
* from the "Amazon S3 PHP class" which is Copyright (c) 2008, Donovan Schönknecht
|
||||
* and released under the 'Simplified BSD License'.
|
||||
*
|
||||
* @param string $file File path
|
||||
* @param string $ext
|
||||
* @return string
|
||||
*/
|
||||
function getMimeType(string $file, string $ext=""): string {
|
||||
// Static extension lookup
|
||||
$ext = strtolower($ext);
|
||||
|
||||
if (array_key_exists($ext, MIME_TYPE_MAP)) { return MIME_TYPE_MAP[$ext]; }
|
||||
|
||||
$type = false;
|
||||
// Fileinfo documentation says fileinfo_open() will use the
|
||||
// MAGIC env var for the magic file
|
||||
if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) &&
|
||||
($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false)
|
||||
{
|
||||
if (($type = finfo_file($finfo, $file)) !== false)
|
||||
{
|
||||
// Remove the charset and grab the last content-type
|
||||
$type = explode(' ', str_replace('; charset=', ';charset=', $type));
|
||||
$type = array_pop($type);
|
||||
$type = explode(';', $type);
|
||||
$type = trim(array_shift($type));
|
||||
}
|
||||
finfo_close($finfo);
|
||||
|
||||
// If anyone is still using mime_content_type()
|
||||
} elseif (function_exists('mime_content_type'))
|
||||
$type = trim(mime_content_type($file));
|
||||
|
||||
if ($type !== false && strlen($type) > 0) return $type;
|
||||
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $mime_type
|
||||
* @return bool|string
|
||||
*/
|
||||
function getExtension(string $mime_type) {
|
||||
if(empty($mime_type)){
|
||||
return false;
|
||||
}
|
||||
|
||||
$ext = array_search($mime_type, MIME_TYPE_MAP);
|
||||
return ($ext ? $ext : false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like glob, with support for matching very long patterns with braces.
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return array
|
||||
*/
|
||||
function zglob(string $pattern): array {
|
||||
$results = array();
|
||||
if(preg_match('/(.*)\{(.*)\}(.*)/', $pattern, $matches)) {
|
||||
$braced = explode(",", $matches[2]);
|
||||
foreach($braced as $b) {
|
||||
$sub_pattern = $matches[1].$b.$matches[3];
|
||||
$results = array_merge($results, zglob($sub_pattern));
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
else {
|
||||
$r = glob($pattern);
|
||||
if($r) return $r;
|
||||
else return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out the path to the shimmie install directory.
|
||||
*
|
||||
* eg if shimmie is visible at http://foo.com/gallery, this
|
||||
* function should return /gallery
|
||||
*
|
||||
* PHP really, really sucks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_base_href(): string {
|
||||
if(defined("BASE_HREF")) return BASE_HREF;
|
||||
$possible_vars = array('SCRIPT_NAME', 'PHP_SELF', 'PATH_INFO', 'ORIG_PATH_INFO');
|
||||
$ok_var = null;
|
||||
foreach($possible_vars as $var) {
|
||||
if(isset($_SERVER[$var]) && substr($_SERVER[$var], -4) === '.php') {
|
||||
$ok_var = $_SERVER[$var];
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(!empty($ok_var));
|
||||
$dir = dirname($ok_var);
|
||||
$dir = str_replace("\\", "/", $dir);
|
||||
$dir = str_replace("//", "/", $dir);
|
||||
$dir = rtrim($dir, "/");
|
||||
return $dir;
|
||||
}
|
||||
|
||||
function startsWith(string $haystack, string $needle): bool {
|
||||
$length = strlen($needle);
|
||||
return (substr($haystack, 0, $length) === $needle);
|
||||
}
|
||||
|
||||
function endsWith(string $haystack, string $needle): bool {
|
||||
$length = strlen($needle);
|
||||
$start = $length * -1; //negative
|
||||
return (substr($haystack, $start) === $needle);
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Input / Output Sanitising *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Make some data safe for printing into HTML
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
function html_escape($input): string {
|
||||
return htmlentities($input, ENT_QUOTES, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape data that was made safe for printing into HTML
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
function html_unescape($input): string {
|
||||
return html_entity_decode($input, ENT_QUOTES, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure some data is safe to be used in integer context
|
||||
*
|
||||
* @param string $input
|
||||
* @return int
|
||||
*/
|
||||
function int_escape($input): int {
|
||||
/*
|
||||
Side note, Casting to an integer is FASTER than using intval.
|
||||
http://hakre.wordpress.com/2010/05/13/php-casting-vs-intval/
|
||||
*/
|
||||
return (int)$input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure some data is safe to be used in URL context
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
function url_escape($input): string {
|
||||
/*
|
||||
Shish: I have a feeling that these three lines are important, possibly for searching for tags with slashes in them like fate/stay_night
|
||||
green-ponies: indeed~
|
||||
|
||||
$input = str_replace('^', '^^', $input);
|
||||
$input = str_replace('/', '^s', $input);
|
||||
$input = str_replace('\\', '^b', $input);
|
||||
|
||||
/* The function idn_to_ascii is used to support Unicode domains / URLs as well.
|
||||
See here for more: http://php.net/manual/en/function.filter-var.php
|
||||
However, it is only supported by PHP version 5.3 and up
|
||||
|
||||
if (function_exists('idn_to_ascii')) {
|
||||
return filter_var(idn_to_ascii($input), FILTER_SANITIZE_URL);
|
||||
} else {
|
||||
return filter_var($input, FILTER_SANITIZE_URL);
|
||||
}
|
||||
*/
|
||||
if(is_null($input)) {
|
||||
return "";
|
||||
}
|
||||
$input = str_replace('^', '^^', $input);
|
||||
$input = str_replace('/', '^s', $input);
|
||||
$input = str_replace('\\', '^b', $input);
|
||||
$input = rawurlencode($input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure some data is safe to be used in SQL context
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
function sql_escape($input): string {
|
||||
global $database;
|
||||
return $database->escape($input);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Turn all manner of HTML / INI / JS / DB booleans into a PHP one
|
||||
*
|
||||
* @param mixed $input
|
||||
* @return boolean
|
||||
*/
|
||||
function bool_escape($input): bool {
|
||||
/*
|
||||
Sometimes, I don't like PHP -- this, is one of those times...
|
||||
"a boolean FALSE is not considered a valid boolean value by this function."
|
||||
Yay for Got'chas!
|
||||
http://php.net/manual/en/filter.filters.validate.php
|
||||
*/
|
||||
if (is_bool($input)) {
|
||||
return $input;
|
||||
} else if (is_numeric($input)) {
|
||||
return ($input === 1);
|
||||
} else {
|
||||
$value = filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if (!is_null($value)) {
|
||||
return $value;
|
||||
} else {
|
||||
$input = strtolower( trim($input) );
|
||||
return (
|
||||
$input === "y" ||
|
||||
$input === "yes" ||
|
||||
$input === "t" ||
|
||||
$input === "true" ||
|
||||
$input === "on" ||
|
||||
$input === "1"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some functions require a callback function for escaping,
|
||||
* but we might not want to alter the data
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
function no_escape($input) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
function clamp(int $val, int $min=null, int $max=null): int {
|
||||
if(!is_numeric($val) || (!is_null($min) && $val < $min)) {
|
||||
$val = $min;
|
||||
}
|
||||
if(!is_null($max) && $val > $max) {
|
||||
$val = $max;
|
||||
}
|
||||
if(!is_null($min) && !is_null($max)) {
|
||||
assert('$val >= $min && $val <= $max', "$min <= $val <= $max");
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
function xml_tag(string $name, array $attrs=array(), array $children=array()): string {
|
||||
$xml = "<$name ";
|
||||
foreach($attrs as $k => $v) {
|
||||
$xv = str_replace(''', ''', htmlspecialchars($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.
|
||||
*
|
||||
* @param string $string input data
|
||||
* @param int $limit how long the string should be
|
||||
* @param string $break where to break the string
|
||||
* @param string $pad what to add to the end of the string after truncating
|
||||
* @return string
|
||||
*/
|
||||
function truncate($string, $limit, $break=" ", $pad="...") {
|
||||
// return with no change if string is shorter than $limit
|
||||
if(strlen($string) <= $limit) return $string;
|
||||
|
||||
// is $break present between $limit and the end of the string?
|
||||
if(false !== ($breakpoint = strpos($string, $break, $limit))) {
|
||||
if($breakpoint < strlen($string) - 1) {
|
||||
$string = substr($string, 0, $breakpoint) . $pad;
|
||||
}
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a human readable filesize into an integer, eg 1KB -> 1024
|
||||
*
|
||||
* @param string $limit
|
||||
* @return int
|
||||
*/
|
||||
function parse_shorthand_int(string $limit): int {
|
||||
if(preg_match('/^([\d\.]+)([gmk])?b?$/i', (string)$limit, $m)) {
|
||||
$value = $m[1];
|
||||
if (isset($m[2])) {
|
||||
switch(strtolower($m[2])) {
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'g': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'm': $value *= 1024; // fall through
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case 'k': $value *= 1024; break;
|
||||
default: $value = -1;
|
||||
}
|
||||
}
|
||||
return (int)$value;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an integer into a human readable filesize, eg 1024 -> 1KB
|
||||
*
|
||||
* @param integer $int
|
||||
* @return string
|
||||
*/
|
||||
function to_shorthand_int(int $int): string {
|
||||
assert($int >= 0);
|
||||
|
||||
if($int >= pow(1024, 3)) {
|
||||
return sprintf("%.1fGB", $int / pow(1024, 3));
|
||||
}
|
||||
else if($int >= pow(1024, 2)) {
|
||||
return sprintf("%.1fMB", $int / pow(1024, 2));
|
||||
}
|
||||
else if($int >= 1024) {
|
||||
return sprintf("%.1fKB", $int / 1024);
|
||||
}
|
||||
else {
|
||||
return (string)$int;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Turn a date into a time, a date, an "X minutes ago...", etc
|
||||
*
|
||||
* @param string $date
|
||||
* @param bool $html
|
||||
* @return string
|
||||
*/
|
||||
function autodate(string $date, bool $html=true): string {
|
||||
$cpu = date('c', strtotime($date));
|
||||
$hum = date('F j, Y; H:i', strtotime($date));
|
||||
return ($html ? "<time datetime='$cpu'>$hum</time>" : $hum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given string is a valid date-time. ( Format: yyyy-mm-dd hh:mm:ss )
|
||||
*
|
||||
* @param string $dateTime
|
||||
* @return bool
|
||||
*/
|
||||
function isValidDateTime(string $dateTime): bool {
|
||||
if (preg_match("/^(\d{4})-(\d{2})-(\d{2}) ([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/", $dateTime, $matches)) {
|
||||
if (checkdate($matches[2], $matches[3], $matches[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given string is a valid date. ( Format: yyyy-mm-dd )
|
||||
*
|
||||
* @param string $date
|
||||
* @return bool
|
||||
*/
|
||||
function isValidDate(string $date): bool {
|
||||
if (preg_match("/^(\d{4})-(\d{2})-(\d{2})$/", $date, $matches)) {
|
||||
// checkdate wants (month, day, year)
|
||||
if (checkdate($matches[2], $matches[3], $matches[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function validate_input(array $inputs): array {
|
||||
$outputs = array();
|
||||
|
||||
foreach($inputs as $key => $validations) {
|
||||
$flags = explode(',', $validations);
|
||||
|
||||
if(in_array('bool', $flags) && !isset($_POST[$key])) {
|
||||
$_POST[$key] = 'off';
|
||||
}
|
||||
|
||||
if(in_array('optional', $flags)) {
|
||||
if(!isset($_POST[$key]) || trim($_POST[$key]) == "") {
|
||||
$outputs[$key] = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(!isset($_POST[$key]) || trim($_POST[$key]) == "") {
|
||||
throw new InvalidInput("Input '$key' not set");
|
||||
}
|
||||
|
||||
$value = trim($_POST[$key]);
|
||||
|
||||
if(in_array('user_id', $flags)) {
|
||||
$id = int_escape($value);
|
||||
if(in_array('exists', $flags)) {
|
||||
if(is_null(User::by_id($id))) {
|
||||
throw new InvalidInput("User #$id does not exist");
|
||||
}
|
||||
}
|
||||
$outputs[$key] = $id;
|
||||
}
|
||||
else if(in_array('user_name', $flags)) {
|
||||
if(strlen($value) < 1) {
|
||||
throw new InvalidInput("Username must be at least 1 character");
|
||||
}
|
||||
else if(!preg_match('/^[a-zA-Z0-9-_]+$/', $value)) {
|
||||
throw new InvalidInput(
|
||||
"Username contains invalid characters. Allowed characters are ".
|
||||
"letters, numbers, dash, and underscore");
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
}
|
||||
else if(in_array('user_class', $flags)) {
|
||||
global $_shm_user_classes;
|
||||
if(!array_key_exists($value, $_shm_user_classes)) {
|
||||
throw new InvalidInput("Invalid user class: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
}
|
||||
else if(in_array('email', $flags)) {
|
||||
$outputs[$key] = trim($value);
|
||||
}
|
||||
else if(in_array('password', $flags)) {
|
||||
$outputs[$key] = $value;
|
||||
}
|
||||
else if(in_array('int', $flags)) {
|
||||
$value = trim($value);
|
||||
if(empty($value) || !is_numeric($value)) {
|
||||
throw new InvalidInput("Invalid int: ".html_escape($value));
|
||||
}
|
||||
$outputs[$key] = (int)$value;
|
||||
}
|
||||
else if(in_array('bool', $flags)) {
|
||||
$outputs[$key] = bool_escape($value);
|
||||
}
|
||||
else if(in_array('string', $flags)) {
|
||||
if(in_array('trim', $flags)) {
|
||||
$value = trim($value);
|
||||
}
|
||||
if(in_array('lower', $flags)) {
|
||||
$value = strtolower($value);
|
||||
}
|
||||
if(in_array('not-empty', $flags)) {
|
||||
throw new InvalidInput("$key must not be blank");
|
||||
}
|
||||
if(in_array('nullify', $flags)) {
|
||||
if(empty($value)) $value = null;
|
||||
}
|
||||
$outputs[$key] = $value;
|
||||
}
|
||||
else {
|
||||
throw new InvalidInput("Unknown validation '$validations'");
|
||||
}
|
||||
}
|
||||
|
||||
return $outputs;
|
||||
}
|
134
core/send_event.php
Normal file
134
core/send_event.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Event API *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/** @private */
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = array();
|
||||
|
||||
function _load_event_listeners() {
|
||||
global $_shm_event_listeners, $_shm_ctx;
|
||||
|
||||
$_shm_ctx->log_start("Loading extensions");
|
||||
|
||||
$cache_path = data_path("cache/shm_event_listeners.php");
|
||||
if(COMPILE_ELS && file_exists($cache_path)) {
|
||||
require_once($cache_path);
|
||||
}
|
||||
else {
|
||||
_set_event_listeners();
|
||||
|
||||
if(COMPILE_ELS) {
|
||||
_dump_event_listeners($_shm_event_listeners, $cache_path);
|
||||
}
|
||||
}
|
||||
|
||||
$_shm_ctx->log_endok();
|
||||
}
|
||||
|
||||
function _set_event_listeners() {
|
||||
global $_shm_event_listeners;
|
||||
$_shm_event_listeners = array();
|
||||
|
||||
foreach(get_declared_classes() as $class) {
|
||||
$rclass = new ReflectionClass($class);
|
||||
if($rclass->isAbstract()) {
|
||||
// don't do anything
|
||||
}
|
||||
elseif(is_subclass_of($class, "Extension")) {
|
||||
/** @var Extension $extension */
|
||||
$extension = new $class();
|
||||
|
||||
// skip extensions which don't support our current database
|
||||
if(!$extension->is_live()) continue;
|
||||
|
||||
foreach(get_class_methods($extension) as $method) {
|
||||
if(substr($method, 0, 2) == "on") {
|
||||
$event = substr($method, 2) . "Event";
|
||||
$pos = $extension->get_priority() * 100;
|
||||
while(isset($_shm_event_listeners[$event][$pos])) {
|
||||
$pos += 1;
|
||||
}
|
||||
$_shm_event_listeners[$event][$pos] = $extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $event_listeners
|
||||
* @param string $path
|
||||
*/
|
||||
function _dump_event_listeners($event_listeners, $path) {
|
||||
$p = "<"."?php\n";
|
||||
|
||||
foreach(get_declared_classes() as $class) {
|
||||
$rclass = new ReflectionClass($class);
|
||||
if($rclass->isAbstract()) {}
|
||||
elseif(is_subclass_of($class, "Extension")) {
|
||||
$p .= "\$$class = new $class(); ";
|
||||
}
|
||||
}
|
||||
|
||||
$p .= "\$_shm_event_listeners = array(\n";
|
||||
foreach($event_listeners as $event => $listeners) {
|
||||
$p .= "\t'$event' => array(\n";
|
||||
foreach($listeners as $id => $listener) {
|
||||
$p .= "\t\t$id => \$".get_class($listener).",\n";
|
||||
}
|
||||
$p .= "\t),\n";
|
||||
}
|
||||
$p .= ");\n";
|
||||
|
||||
$p .= "?".">";
|
||||
file_put_contents($path, $p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ext_name Main class name (eg ImageIO as opposed to ImageIOTheme or ImageIOTest)
|
||||
* @return bool
|
||||
*/
|
||||
function ext_is_live(string $ext_name): bool {
|
||||
if (class_exists($ext_name)) {
|
||||
/** @var Extension $ext */
|
||||
$ext = new $ext_name();
|
||||
return $ext->is_live();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** @private */
|
||||
global $_shm_event_count;
|
||||
$_shm_event_count = 0;
|
||||
|
||||
/**
|
||||
* Send an event to all registered Extensions.
|
||||
*
|
||||
* @param Event $event
|
||||
*/
|
||||
function send_event(Event $event) {
|
||||
global $_shm_event_listeners, $_shm_event_count, $_shm_ctx;
|
||||
if(!isset($_shm_event_listeners[get_class($event)])) return;
|
||||
$method_name = "on".str_replace("Event", "", get_class($event));
|
||||
|
||||
// send_event() is performance sensitive, and with the number
|
||||
// of times context gets called the time starts to add up
|
||||
$ctx_enabled = constant('CONTEXT');
|
||||
|
||||
if($ctx_enabled) $_shm_ctx->log_start(get_class($event));
|
||||
// SHIT: http://bugs.php.net/bug.php?id=35106
|
||||
$my_event_listeners = $_shm_event_listeners[get_class($event)];
|
||||
ksort($my_event_listeners);
|
||||
foreach($my_event_listeners as $listener) {
|
||||
if($ctx_enabled) $_shm_ctx->log_start(get_class($listener));
|
||||
if(method_exists($listener, $method_name)) {
|
||||
$listener->$method_name($event);
|
||||
}
|
||||
if($ctx_enabled) $_shm_ctx->log_endok();
|
||||
}
|
||||
$_shm_event_count++;
|
||||
if($ctx_enabled) $_shm_ctx->log_endok();
|
||||
}
|
@ -40,7 +40,8 @@ _d("TIMEZONE", null); // string timezone
|
||||
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable
|
||||
_d("EXTRA_EXTS", ""); // string optional extra extensions
|
||||
_d("BASE_URL", null); // string force a specific base URL (default is auto-detect)
|
||||
_d("MIN_PHP_VERSION", '7.0');// string minium supported PHP version
|
||||
_d("MIN_PHP_VERSION", '7.1');// string minimum supported PHP version
|
||||
_d("ENABLED_MODS", "imageboard");
|
||||
|
||||
/*
|
||||
* Calculated settings - you should never need to change these
|
43
core/tests/polyfills.test.php
Normal file
43
core/tests/polyfills.test.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
require_once "core/polyfills.php";
|
||||
|
||||
class PolyfillsTest extends \PHPUnit\Framework\TestCase {
|
||||
public function test_html_escape() {
|
||||
$this->assertEquals(
|
||||
html_escape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
html_unescape("Foo & <waffles>"),
|
||||
"Foo & <waffles>"
|
||||
);
|
||||
|
||||
$x = "Foo & <waffles>";
|
||||
$this->assertEquals(html_escape(html_unescape($x)), $x);
|
||||
}
|
||||
|
||||
public function test_int_escape() {
|
||||
$this->assertEquals(int_escape(""), 0);
|
||||
$this->assertEquals(int_escape("1"), 1);
|
||||
$this->assertEquals(int_escape("-1"), -1);
|
||||
$this->assertEquals(int_escape("-1.5"), -1);
|
||||
}
|
||||
|
||||
public function test_clamp() {
|
||||
$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_shorthand_int() {
|
||||
$this->assertEquals(to_shorthand_int(1231231231), "1.1GB");
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
110
core/urls.php
Normal file
110
core/urls.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Figure out the correct way to link to a page, taking into account
|
||||
* things like the nice URLs setting.
|
||||
*
|
||||
* eg make_link("post/list") becomes "/v2/index.php?q=post/list"
|
||||
*
|
||||
* @param null|string $page
|
||||
* @param null|string $query
|
||||
* @return string
|
||||
*/
|
||||
function make_link(string $page=null, string $query=null): string {
|
||||
global $config;
|
||||
|
||||
if(is_null($page)) $page = $config->get_string('main_page');
|
||||
|
||||
if(!is_null(BASE_URL)) {
|
||||
$base = BASE_URL;
|
||||
}
|
||||
elseif(NICE_URLS || $config->get_bool('nice_urls', false)) {
|
||||
$base = str_replace('/'.basename($_SERVER["SCRIPT_FILENAME"]), "", $_SERVER["PHP_SELF"]);
|
||||
}
|
||||
else {
|
||||
$base = "./".basename($_SERVER["SCRIPT_FILENAME"])."?q=";
|
||||
}
|
||||
|
||||
if(is_null($query)) {
|
||||
return str_replace("//", "/", $base.'/'.$page );
|
||||
}
|
||||
else {
|
||||
if(strpos($base, "?")) {
|
||||
return $base .'/'. $page .'&'. $query;
|
||||
}
|
||||
else if(strpos($query, "#") === 0) {
|
||||
return $base .'/'. $page . $query;
|
||||
}
|
||||
else {
|
||||
return $base .'/'. $page .'?'. $query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Take the current URL and modify some parameters
|
||||
*
|
||||
* @param array $changes
|
||||
* @return string
|
||||
*/
|
||||
function modify_current_url(array $changes): string {
|
||||
return modify_url($_SERVER['QUERY_STRING'], $changes);
|
||||
}
|
||||
|
||||
function modify_url(string $url, array $changes): string {
|
||||
// SHIT: PHP is officially the worst web API ever because it does not
|
||||
// have a built-in function to do this.
|
||||
|
||||
// SHIT: parse_str is magically retarded; not only is it a useless name, it also
|
||||
// didn't return the parsed array, preferring to overwrite global variables with
|
||||
// whatever data the user supplied. Thankfully, 4.0.3 added an extra option to
|
||||
// give it an array to use...
|
||||
$params = array();
|
||||
parse_str($url, $params);
|
||||
|
||||
if(isset($changes['q'])) {
|
||||
$base = $changes['q'];
|
||||
unset($changes['q']);
|
||||
}
|
||||
else {
|
||||
$base = _get_query();
|
||||
}
|
||||
|
||||
if(isset($params['q'])) {
|
||||
unset($params['q']);
|
||||
}
|
||||
|
||||
foreach($changes as $k => $v) {
|
||||
if(is_null($v) and isset($params[$k])) unset($params[$k]);
|
||||
$params[$k] = $v;
|
||||
}
|
||||
|
||||
return make_link($base, http_build_query($params));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Turn a relative link into an absolute one, including hostname
|
||||
*
|
||||
* @param string $link
|
||||
* @return string
|
||||
*/
|
||||
function make_http(string $link) {
|
||||
if(strpos($link, "://") > 0) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
if(strlen($link) > 0 && $link[0] != '/') {
|
||||
$link = get_base_href() . '/' . $link;
|
||||
}
|
||||
|
||||
$protocol = is_https_enabled() ? "https://" : "http://";
|
||||
$link = $protocol . $_SERVER["HTTP_HOST"] . $link;
|
||||
$link = str_replace("/./", "/", $link);
|
||||
|
||||
return $link;
|
||||
}
|
@ -223,18 +223,3 @@ class User {
|
||||
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
||||
}
|
||||
}
|
||||
|
||||
class MockUser extends User {
|
||||
public function __construct(string $name) {
|
||||
$row = array(
|
||||
"name" => $name,
|
||||
"id" => 1,
|
||||
"email" => "",
|
||||
"joindate" => "",
|
||||
"pass" => "",
|
||||
"class" => "admin",
|
||||
);
|
||||
parent::__construct($row);
|
||||
}
|
||||
}
|
||||
|
1772
core/util.inc.php
1772
core/util.inc.php
File diff suppressed because it is too large
Load Diff
597
core/util.php
Normal file
597
core/util.php
Normal file
@ -0,0 +1,597 @@
|
||||
<?php
|
||||
require_once "vendor/shish/libcontext-php/context.php";
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Misc *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* @param string $file The filename
|
||||
* @return string
|
||||
*/
|
||||
function mtimefile(string $file): string {
|
||||
$data_href = get_base_href();
|
||||
$mtime = filemtime($file);
|
||||
return "$data_href/$file?$mtime";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current theme as a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_theme(): string {
|
||||
global $config;
|
||||
$theme = $config->get_string("theme", "default");
|
||||
if(!file_exists("themes/$theme")) $theme = "default";
|
||||
return $theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets contact link as mailto: or http:
|
||||
* @return string|null
|
||||
*/
|
||||
function contact_link() {
|
||||
global $config;
|
||||
$text = $config->get_string('contact_link');
|
||||
if(is_null($text)) return null;
|
||||
|
||||
if(
|
||||
startsWith($text, "http:") ||
|
||||
startsWith($text, "https:") ||
|
||||
startsWith($text, "mailto:")
|
||||
) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
if(strpos($text, "@")) {
|
||||
return "mailto:$text";
|
||||
}
|
||||
|
||||
if(strpos($text, "/")) {
|
||||
return "http://$text";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if HTTPS is enabled for the server.
|
||||
*
|
||||
* @return bool True if HTTPS is enabled
|
||||
*/
|
||||
function is_https_enabled(): bool {
|
||||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Block objects, used to sort them before being displayed
|
||||
*
|
||||
* @param Block $a
|
||||
* @param Block $b
|
||||
* @return int
|
||||
*/
|
||||
function blockcmp(Block $a, Block $b) {
|
||||
if($a->position == $b->position) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return ($a->position > $b->position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out PHP's internal memory limit
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function get_memory_limit(): int {
|
||||
global $config;
|
||||
|
||||
// thumbnail generation requires lots of memory
|
||||
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
|
||||
$shimmie_limit = parse_shorthand_int($config->get_int("thumb_mem_limit"));
|
||||
|
||||
if($shimmie_limit < 3*1024*1024) {
|
||||
// we aren't going to fit, override
|
||||
$shimmie_limit = $default_limit;
|
||||
}
|
||||
|
||||
/*
|
||||
Get PHP's configured memory limit.
|
||||
Note that this is set to -1 for NO memory limit.
|
||||
|
||||
http://ca2.php.net/manual/en/ini.core.php#ini.memory-limit
|
||||
*/
|
||||
$memory = parse_shorthand_int(ini_get("memory_limit"));
|
||||
|
||||
if($memory == -1) {
|
||||
// No memory limit.
|
||||
// Return the larger of the set limits.
|
||||
return max($shimmie_limit, $default_limit);
|
||||
}
|
||||
else {
|
||||
// PHP has a memory limit set.
|
||||
if ($shimmie_limit > $memory) {
|
||||
// Shimmie wants more memory than what PHP is currently set for.
|
||||
|
||||
// Attempt to set PHP's memory limit.
|
||||
if ( ini_set("memory_limit", $shimmie_limit) === false ) {
|
||||
/* We can't change PHP's limit, oh well, return whatever its currently set to */
|
||||
return $memory;
|
||||
}
|
||||
$memory = parse_shorthand_int(ini_get("memory_limit"));
|
||||
}
|
||||
|
||||
// PHP's memory limit is more than Shimmie needs.
|
||||
return $memory; // return the current setting
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active IP, masked to make it not change when the last
|
||||
* octet or two change, for use in session cookies and such
|
||||
*
|
||||
* @param Config $config
|
||||
* @return string
|
||||
*/
|
||||
function get_session_ip(Config $config): string {
|
||||
$mask = $config->get_string("session_hash_mask", "255.255.0.0");
|
||||
$addr = $_SERVER['REMOTE_ADDR'];
|
||||
$addr = inet_ntop(inet_pton($addr) & inet_pton($mask));
|
||||
return $addr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set (or extend) a flash-message cookie.
|
||||
*
|
||||
* This can optionally be done at the same time as saving a log message with log_*()
|
||||
*
|
||||
* Generally one should flash a message in onPageRequest and log a message wherever
|
||||
* the action actually takes place (eg onWhateverElse) - but much of the time, actions
|
||||
* are taken from within onPageRequest...
|
||||
*
|
||||
* @param string $text
|
||||
* @param string $type
|
||||
*/
|
||||
function flash_message(string $text, string $type="info") {
|
||||
global $page;
|
||||
$current = $page->get_cookie("flash_message");
|
||||
if($current) {
|
||||
$text = $current . "\n" . $text;
|
||||
}
|
||||
# the message should be viewed pretty much immediately,
|
||||
# so 60s timeout should be more than enough
|
||||
$page->add_cookie("flash_message", $text, time()+60, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* A shorthand way to send a TextFormattingEvent and get the results.
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
function format_text(string $string): string {
|
||||
$tfe = new TextFormattingEvent($string);
|
||||
send_event($tfe);
|
||||
return $tfe->formatted;
|
||||
}
|
||||
|
||||
function warehouse_path(string $base, string $hash, bool $create=true): string {
|
||||
$ab = substr($hash, 0, 2);
|
||||
$cd = substr($hash, 2, 2);
|
||||
if(WH_SPLITS == 2) {
|
||||
$pa = $base.'/'.$ab.'/'.$cd.'/'.$hash;
|
||||
}
|
||||
else {
|
||||
$pa = $base.'/'.$ab.'/'.$hash;
|
||||
}
|
||||
if($create && !file_exists(dirname($pa))) mkdir(dirname($pa), 0755, true);
|
||||
return $pa;
|
||||
}
|
||||
|
||||
function data_path(string $filename): string {
|
||||
$filename = "data/" . $filename;
|
||||
if(!file_exists(dirname($filename))) mkdir(dirname($filename), 0755, true);
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $mfile
|
||||
* @return array|bool
|
||||
*/
|
||||
function transload(string $url, string $mfile) {
|
||||
global $config;
|
||||
|
||||
if($config->get_string("transload_engine") === "curl" && function_exists("curl_init")) {
|
||||
$ch = curl_init($url);
|
||||
$fp = fopen($mfile, "w");
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_REFERER, $url);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, "Shimmie-".VERSION);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||||
$headers = http_parse_headers(implode("\n", preg_split('/\R/', rtrim(substr($response, 0, $header_size)))));
|
||||
$body = substr($response, $header_size);
|
||||
|
||||
curl_close($ch);
|
||||
fwrite($fp, $body);
|
||||
fclose($fp);
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
return file_exists($mfile);
|
||||
}
|
||||
|
||||
if($config->get_string("transload_engine") === "fopen") {
|
||||
$fp_in = @fopen($url, "r");
|
||||
$fp_out = fopen($mfile, "w");
|
||||
if(!$fp_in || !$fp_out) {
|
||||
return false;
|
||||
}
|
||||
$length = 0;
|
||||
while(!feof($fp_in) && $length <= $config->get_int('upload_size')) {
|
||||
$data = fread($fp_in, 8192);
|
||||
$length += strlen($data);
|
||||
fwrite($fp_out, $data);
|
||||
}
|
||||
fclose($fp_in);
|
||||
fclose($fp_out);
|
||||
|
||||
$headers = http_parse_headers(implode("\n", $http_response_header));
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active contents of a .php file
|
||||
*
|
||||
* @param string $fname
|
||||
* @return string|null
|
||||
*/
|
||||
function manual_include(string $fname) {
|
||||
static $included = array();
|
||||
|
||||
if(!file_exists($fname)) return null;
|
||||
|
||||
if(in_array($fname, $included)) return null;
|
||||
|
||||
$included[] = $fname;
|
||||
|
||||
print "$fname\n";
|
||||
|
||||
$text = file_get_contents($fname);
|
||||
|
||||
// we want one continuous file
|
||||
$text = str_replace('<'.'?php', '', $text);
|
||||
$text = str_replace('?'.'>', '', $text);
|
||||
|
||||
// most requires are built-in, but we want /lib separately
|
||||
$text = str_replace('require_', '// require_', $text);
|
||||
$text = str_replace('// require_once "lib', 'require_once "lib', $text);
|
||||
|
||||
// @include_once is used for user-creatable config files
|
||||
$text = preg_replace('/@include_once "(.*)";/e', "manual_include('$1')", $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
function path_to_tags(string $path): string {
|
||||
$matches = array();
|
||||
if(preg_match("/\d+ - (.*)\.([a-zA-Z]+)/", basename($path), $matches)) {
|
||||
$tags = $matches[1];
|
||||
}
|
||||
else {
|
||||
$tags = dirname($path);
|
||||
$tags = str_replace("/", " ", $tags);
|
||||
$tags = str_replace("__", " ", $tags);
|
||||
$tags = trim($tags);
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Debugging functions *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
// SHIT by default this returns the time as a string. And it's not even a
|
||||
// string representation of a number, it's two numbers separated by a space.
|
||||
// What the fuck were the PHP developers smoking.
|
||||
$_shm_load_start = microtime(true);
|
||||
|
||||
/**
|
||||
* Collects some debug information (execution time, memory usage, queries, etc)
|
||||
* and formats it to stick in the footer of the page.
|
||||
*
|
||||
* @return string debug info to add to the page.
|
||||
*/
|
||||
function get_debug_info(): string {
|
||||
global $config, $_shm_event_count, $database, $_shm_load_start;
|
||||
|
||||
$i_mem = sprintf("%5.2f", ((memory_get_peak_usage(true)+512)/1024)/1024);
|
||||
|
||||
if($config->get_string("commit_hash", "unknown") == "unknown"){
|
||||
$commit = "";
|
||||
}
|
||||
else {
|
||||
$commit = " (".$config->get_string("commit_hash").")";
|
||||
}
|
||||
$time = sprintf("%.2f", microtime(true) - $_shm_load_start);
|
||||
$dbtime = sprintf("%.2f", $database->dbtime);
|
||||
$i_files = count(get_included_files());
|
||||
$hits = $database->cache->get_hits();
|
||||
$miss = $database->cache->get_misses();
|
||||
|
||||
$debug = "<br>Took $time seconds (db:$dbtime) and {$i_mem}MB of RAM";
|
||||
$debug .= "; Used $i_files files and {$database->query_count} queries";
|
||||
$debug .= "; Sent $_shm_event_count events";
|
||||
$debug .= "; $hits cache hits and $miss misses";
|
||||
$debug .= "; Shimmie version ". VERSION . $commit; // .", SCore Version ". SCORE_VERSION;
|
||||
|
||||
return $debug;
|
||||
}
|
||||
|
||||
function score_assert_handler($file, $line, $code, $desc = null) {
|
||||
$file = basename($file);
|
||||
print("Assertion failed at $file:$line: $code ($desc)");
|
||||
/*
|
||||
print("<pre>");
|
||||
debug_print_backtrace();
|
||||
print("</pre>");
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Request initialisation stuff *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/** @privatesection */
|
||||
|
||||
function _version_check() {
|
||||
if(MIN_PHP_VERSION)
|
||||
{
|
||||
if(version_compare(phpversion(), MIN_PHP_VERSION, ">=") === FALSE) {
|
||||
print "
|
||||
Shimmie (SCore Engine) does not support versions of PHP lower than ".MIN_PHP_VERSION."
|
||||
(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _sanitise_environment() {
|
||||
global $_shm_ctx;
|
||||
|
||||
if(TIMEZONE) {
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
}
|
||||
|
||||
if(DEBUG) {
|
||||
error_reporting(E_ALL);
|
||||
assert_options(ASSERT_ACTIVE, 1);
|
||||
assert_options(ASSERT_BAIL, 1);
|
||||
assert_options(ASSERT_WARNING, 0);
|
||||
assert_options(ASSERT_QUIET_EVAL, 1);
|
||||
assert_options(ASSERT_CALLBACK, 'score_assert_handler');
|
||||
}
|
||||
|
||||
$_shm_ctx = new Context();
|
||||
if(CONTEXT) {
|
||||
$_shm_ctx->set_log(CONTEXT);
|
||||
}
|
||||
|
||||
if(COVERAGE) {
|
||||
_start_coverage();
|
||||
register_shutdown_function("_end_coverage");
|
||||
}
|
||||
|
||||
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>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $_theme
|
||||
* @return string[]
|
||||
*/
|
||||
function _get_themelet_files(string $_theme): array {
|
||||
$base_themelets = array();
|
||||
if(file_exists('themes/'.$_theme.'/custompage.class.php')) $base_themelets[] = 'themes/'.$_theme.'/custompage.class.php';
|
||||
$base_themelets[] = 'themes/'.$_theme.'/layout.class.php';
|
||||
$base_themelets[] = 'themes/'.$_theme.'/themelet.class.php';
|
||||
|
||||
$ext_themelets = zglob("ext/{".ENABLED_EXTS."}/theme.php");
|
||||
$custom_themelets = zglob('themes/'.$_theme.'/{'.ENABLED_EXTS.'}.theme.php');
|
||||
|
||||
return array_merge($base_themelets, $ext_themelets, $custom_themelets);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to display fatal errors to the web user.
|
||||
* @param Exception $e
|
||||
*/
|
||||
function _fatal_error(Exception $e) {
|
||||
$version = VERSION;
|
||||
$message = $e->getMessage();
|
||||
|
||||
//$trace = var_dump($e->getTrace());
|
||||
|
||||
//$hash = exec("git rev-parse HEAD");
|
||||
//$h_hash = $hash ? "<p><b>Hash:</b> $hash" : "";
|
||||
//'.$h_hash.'
|
||||
|
||||
header("HTTP/1.0 500 Internal Error");
|
||||
echo '
|
||||
<html>
|
||||
<head>
|
||||
<title>Internal error - SCore-'.$version.'</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal Error</h1>
|
||||
<p><b>Message:</b> '.$message.'
|
||||
<p><b>Version:</b> '.$version.' (on '.phpversion().')
|
||||
</body>
|
||||
</html>
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn ^^ into ^ and ^s into /
|
||||
*
|
||||
* Necessary because various servers and various clients
|
||||
* think that / is special...
|
||||
*
|
||||
* @param string $str
|
||||
* @return string
|
||||
*/
|
||||
function _decaret(string $str): string {
|
||||
$out = "";
|
||||
$length = strlen($str);
|
||||
for($i=0; $i<$length; $i++) {
|
||||
if($str[$i] == "^") {
|
||||
$i++;
|
||||
if($str[$i] == "^") $out .= "^";
|
||||
if($str[$i] == "s") $out .= "/";
|
||||
if($str[$i] == "b") $out .= "\\";
|
||||
}
|
||||
else {
|
||||
$out .= $str[$i];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function _get_user(): User {
|
||||
global $config, $page;
|
||||
$user = null;
|
||||
if($page->get_cookie("user") && $page->get_cookie("session")) {
|
||||
$tmp_user = User::by_session($page->get_cookie("user"), $page->get_cookie("session"));
|
||||
if(!is_null($tmp_user)) {
|
||||
$user = $tmp_user;
|
||||
}
|
||||
}
|
||||
if(is_null($user)) {
|
||||
$user = User::by_id($config->get_int("anon_id", 0));
|
||||
}
|
||||
assert(!is_null($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
function _get_query() {
|
||||
return (@$_POST["q"]?:@$_GET["q"])?:"/";
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* Code coverage *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
function _start_coverage() {
|
||||
if(function_exists("xdebug_start_code_coverage")) {
|
||||
#xdebug_start_code_coverage(XDEBUG_CC_UNUSED|XDEBUG_CC_DEAD_CODE);
|
||||
xdebug_start_code_coverage(XDEBUG_CC_UNUSED);
|
||||
}
|
||||
}
|
||||
|
||||
function _end_coverage() {
|
||||
if(function_exists("xdebug_get_code_coverage")) {
|
||||
// Absolute path is necessary because working directory
|
||||
// inside register_shutdown_function is unpredictable.
|
||||
$absolute_path = dirname(dirname(__FILE__)) . "/data/coverage";
|
||||
if(!file_exists($absolute_path)) mkdir($absolute_path);
|
||||
$n = 0;
|
||||
$t = time();
|
||||
while(file_exists("$absolute_path/$t.$n.log")) $n++;
|
||||
file_put_contents("$absolute_path/$t.$n.log", gzdeflate(serialize(xdebug_get_code_coverage())));
|
||||
}
|
||||
}
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* HTML Generation *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
/**
|
||||
* Give a HTML string which shows an IP (if the user is allowed to see IPs),
|
||||
* and a link to ban that IP (if the user is allowed to ban IPs)
|
||||
*
|
||||
* FIXME: also check that IP ban ext is installed
|
||||
*
|
||||
* @param string $ip
|
||||
* @param string $ban_reason
|
||||
* @return string
|
||||
*/
|
||||
function show_ip(string $ip, string $ban_reason): string {
|
||||
global $user;
|
||||
$u_reason = url_escape($ban_reason);
|
||||
$u_end = url_escape("+1 week");
|
||||
$ban = $user->can("ban_ip") ? ", <a href='".make_link("ip_ban/list", "ip=$ip&reason=$u_reason&end=$u_end#add")."'>Ban</a>" : "";
|
||||
$ip = $user->can("view_ip") ? $ip.$ban : "";
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a form tag with relevant auth token and stuff
|
||||
*
|
||||
* @param string $target
|
||||
* @param string $method
|
||||
* @param bool $multipart
|
||||
* @param string $form_id
|
||||
* @param string $onsubmit
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function make_form(string $target, string $method="POST", bool $multipart=False, string $form_id="", string $onsubmit=""): string {
|
||||
global $user;
|
||||
if($method == "GET") {
|
||||
$link = html_escape($target);
|
||||
$target = make_link($target);
|
||||
$extra_inputs = "<input type='hidden' name='q' value='$link'>";
|
||||
}
|
||||
else {
|
||||
$extra_inputs = $user->get_auth_html();
|
||||
}
|
||||
|
||||
$extra = empty($form_id) ? '' : 'id="'. $form_id .'"';
|
||||
if($multipart) {
|
||||
$extra .= " enctype='multipart/form-data'";
|
||||
}
|
||||
if($onsubmit) {
|
||||
$extra .= ' onsubmit="'.$onsubmit.'"';
|
||||
}
|
||||
return '<form action="'.$target.'" method="'.$method.'" '.$extra.'>'.$extra_inputs;
|
||||
}
|
18
ext/bbcode/script.js
Normal file
18
ext/bbcode/script.js
Normal file
@ -0,0 +1,18 @@
|
||||
$(document).ready(function() {
|
||||
$(".shm-clink").each(function(idx, elm) {
|
||||
var target_id = $(elm).data("clink-sel");
|
||||
if(target_id && $(target_id).length > 0) {
|
||||
// if the target comment is already on this page, don't bother
|
||||
// switching pages
|
||||
$(elm).attr("href", target_id);
|
||||
// highlight it when clicked
|
||||
$(elm).click(function(e) {
|
||||
// This needs jQuery UI
|
||||
$(target_id).highlight();
|
||||
});
|
||||
// vanilla target name should already be in the URL tag, but this
|
||||
// will include the anon ID as displayed on screen
|
||||
$(elm).html("@"+$(target_id+" .username").html());
|
||||
}
|
||||
});
|
||||
});
|
8
ext/comment/script.js
Normal file
8
ext/comment/script.js
Normal file
@ -0,0 +1,8 @@
|
||||
function replyTo(imageId, commentId, userId) {
|
||||
var box = $("#comment_on_"+imageId);
|
||||
var text = "[url=site://post/view/"+imageId+"#c"+commentId+"]@"+userId+"[/url]: ";
|
||||
|
||||
box.focus();
|
||||
box.val(box.val() + text);
|
||||
$("#c"+commentId).highlight();
|
||||
}
|
@ -40,7 +40,7 @@ class MP3FileHandler extends DataHandlerExtension {
|
||||
$success = FALSE;
|
||||
|
||||
if (file_exists($tmpname)) {
|
||||
$mimeType = mime_content_type($tmpname);
|
||||
$mimeType = getMimeType($tmpname);
|
||||
|
||||
$success = ($mimeType == 'audio/mpeg');
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ class VideoFileHandler extends DataHandlerExtension {
|
||||
$image->width = $size[0];
|
||||
$image->height = $size[1];
|
||||
|
||||
switch (mime_content_type($filename)) {
|
||||
switch (getMimeType($filename)) {
|
||||
case "video/webm":
|
||||
$image->ext = "webm";
|
||||
break;
|
||||
@ -167,7 +167,7 @@ class VideoFileHandler extends DataHandlerExtension {
|
||||
protected function check_contents(string $tmpname): bool {
|
||||
$success = FALSE;
|
||||
if (file_exists($tmpname)) {
|
||||
$mimeType = mime_content_type($tmpname);
|
||||
$mimeType = getMimeType($tmpname);
|
||||
|
||||
$success = in_array($mimeType, [
|
||||
'video/webm',
|
||||
|
@ -9,7 +9,7 @@ class VideoFileHandlerTheme extends Themelet {
|
||||
$full_url = make_http($ilink);
|
||||
$autoplay = $config->get_bool("video_playback_autoplay");
|
||||
$loop = $config->get_bool("video_playback_loop");
|
||||
$player = make_link('lib/vendor/swf/flashmediaelement.swf');
|
||||
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
|
||||
|
||||
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
|
||||
|
||||
|
@ -31,6 +31,21 @@ $(function() {
|
||||
input.val(tagArr.join(" "));
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* If an image list has a data-query attribute, append
|
||||
* that query string to all thumb links inside the list.
|
||||
* This allows us to cache the same thumb for all query
|
||||
* strings, adding the query in the browser.
|
||||
*/
|
||||
$(".shm-image-list").each(function(idx, elm) {
|
||||
var query = $(this).data("query");
|
||||
if(query) {
|
||||
$(this).find(".shm-thumb-link").each(function(idx2, elm2) {
|
||||
$(this).attr("href", $(this).attr("href") + query);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function select_blocked_tags() {
|
||||
|
@ -15,7 +15,7 @@ class IndexTest extends ShimmiePHPUnitTestCase {
|
||||
|
||||
public function testIndexPage() {
|
||||
$this->get_page('post/list');
|
||||
$this->assert_title("Welcome to Shimmie ".VERSION);
|
||||
$this->assert_title("Welcome to Shimmie");
|
||||
$this->assert_no_text("Prev | Index | Next");
|
||||
|
||||
$this->log_in_as_user();
|
||||
|
@ -140,7 +140,7 @@ class PoolsTheme extends Themelet {
|
||||
$page->add_block(new NavBlock());
|
||||
$page->add_block(new Block("Pool Navigation", $poolnav_html, "left", 10));
|
||||
|
||||
if(count($pools) == 1) {
|
||||
if(!is_null($pools) && count($pools) == 1) {
|
||||
$pool = $pools[0];
|
||||
if($pool['public'] == "Y" || $user->is_admin()) {// IF THE POOL IS PUBLIC OR IS ADMIN SHOW EDIT PANEL
|
||||
if(!$user->is_anonymous()) {// IF THE USER IS REGISTERED AND LOGGED IN SHOW EDIT PANEL
|
||||
|
@ -6,10 +6,8 @@ class QRImageTheme extends Themelet {
|
||||
public function links_block($link) {
|
||||
global $page;
|
||||
|
||||
$protocol = is_https_enabled() ? "https://" : "http://";
|
||||
|
||||
$page->add_block( new Block(
|
||||
"QR Code","<img alt='QR Code' src='{$protocol}chart.apis.google.com/chart?chs=150x150&cht=qr&chl={$link}' />","left",50));
|
||||
"QR Code","<img alt='QR Code' src='//chart.apis.google.com/chart?chs=150x150&cht=qr&chl={$link}' />","left",50));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ if( // kill these glitched requests immediately
|
||||
) {die("No");}
|
||||
|
||||
class Rule34 extends Extension {
|
||||
protected $db_support = ['pgsql']; # Only PG has the NOTIFY pubsub system
|
||||
|
||||
public function onImageDeletion(ImageDeletionEvent $event) {
|
||||
global $database;
|
||||
$database->execute("NOTIFY shm_image_bans, '{$event->image->hash}';");
|
||||
|
@ -233,7 +233,7 @@ class Setup extends Extension {
|
||||
$host .= ":" . $_SERVER["SERVER_PORT"];
|
||||
}
|
||||
}
|
||||
$full = (@$_SERVER["HTTPS"] ? "https://" : "http://") . $host . $_SERVER["PHP_SELF"];
|
||||
$full = "//" . $host . $_SERVER["PHP_SELF"];
|
||||
$test_url = str_replace("/index.php", "/nicetest", $full);
|
||||
|
||||
$nicescript = "<script type='text/javascript'>
|
||||
|
@ -6,6 +6,10 @@
|
||||
* Do not remove this notice. *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
var Tagger = {
|
||||
initialize : function (image_id) {
|
||||
// object navigation
|
||||
|
@ -144,6 +144,7 @@ class ViewImage extends Extension {
|
||||
$iibbe = new ImageInfoBoxBuildingEvent($event->get_image(), $user);
|
||||
send_event($iibbe);
|
||||
ksort($iibbe->parts);
|
||||
$this->theme->display_meta_headers($event->get_image());
|
||||
$this->theme->display_page($event->get_image(), $iibbe->parts);
|
||||
}
|
||||
}
|
||||
|
12
ext/view/script.js
Normal file
12
ext/view/script.js
Normal file
@ -0,0 +1,12 @@
|
||||
$(document).ready(function() {
|
||||
if(document.location.hash.length > 3) {
|
||||
var query = document.location.hash.substring(1);
|
||||
|
||||
$('#prevlink').attr('href', function(i, attr) {
|
||||
return attr + '?' + query;
|
||||
});
|
||||
$('#nextlink').attr('href', function(i, attr) {
|
||||
return attr + '?' + query;
|
||||
});
|
||||
}
|
||||
});
|
@ -1,20 +1,24 @@
|
||||
<?php
|
||||
|
||||
class ViewImageTheme extends Themelet {
|
||||
public function display_meta_headers(Image $image) {
|
||||
global $page;
|
||||
|
||||
$h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list()));
|
||||
$page->add_html_header("<meta name=\"keywords\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:title\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:type\" content=\"article\">");
|
||||
$page->add_html_header("<meta property=\"og:image\" content=\"".make_http($image->get_thumb_link())."\">");
|
||||
$page->add_html_header("<meta property=\"og:url\" content=\"".make_http(make_link("post/view/{$image->id}"))."\">");
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a page showing $image and some info about it
|
||||
*/
|
||||
public function display_page(Image $image, $editor_parts) {
|
||||
global $page;
|
||||
|
||||
$h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list()));
|
||||
|
||||
$page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
|
||||
$page->add_html_header("<meta name=\"keywords\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:title\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:type\" content=\"article\">");
|
||||
$page->add_html_header("<meta property=\"og:image\" content=\"".make_http($image->get_thumb_link())."\">");
|
||||
$page->add_html_header("<meta property=\"og:url\" content=\"".make_http(make_link("post/view/{$image->id}"))."\">");
|
||||
$page->set_heading(html_escape($image->get_tag_list()));
|
||||
$page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0));
|
||||
$page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 20));
|
||||
|
@ -1,11 +1,12 @@
|
||||
|
||||
ARTICLE SELECT {width: 150px;}
|
||||
INPUT, TEXTAREA {box-sizing: border-box;}
|
||||
TD>INPUT[type="button"] {width: 100%;}
|
||||
TD>INPUT[type="submit"] {width: 100%;}
|
||||
TD>INPUT[type="text"] {width: 100%;}
|
||||
TD>INPUT[type="password"] {width: 100%;}
|
||||
TD>SELECT {width: 100%;}
|
||||
TD>INPUT[type="button"],
|
||||
TD>INPUT[type="submit"],
|
||||
TD>INPUT[type="text"],
|
||||
TD>INPUT[type="password"],
|
||||
TD>INPUT[type="email"],
|
||||
TD>SELECT,
|
||||
TD>TEXTAREA {width: 100%;}
|
||||
|
||||
TABLE.form {width: 300px;}
|
||||
@ -30,3 +31,44 @@ IMG.lazy {display: none;}
|
||||
margin: 8px;
|
||||
border: 1px solid #882;
|
||||
}
|
||||
|
||||
#installer {
|
||||
background: #EEE;
|
||||
font-family: "Arial", sans-serif;
|
||||
font-size: 14px;
|
||||
width: 512px;
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
border: 1px solid black;
|
||||
border-radius: 16px;
|
||||
}
|
||||
#installer P {
|
||||
padding: 5px;
|
||||
}
|
||||
#installer A {
|
||||
text-decoration: none;
|
||||
}
|
||||
#installer A:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#installer H1, #installer H3 {
|
||||
background: #DDD;
|
||||
text-align: center;
|
||||
margin: 0px;
|
||||
padding: 2px;
|
||||
}
|
||||
#installer H1 {
|
||||
border-bottom: 1px solid black;
|
||||
border-radius: 16px 16px 0px 0px;
|
||||
}
|
||||
#installer H3 {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
#installer TH {
|
||||
text-align: right;
|
||||
}
|
||||
#installer INPUT,
|
||||
#installer SELECT {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -32,23 +32,6 @@ $(document).ready(function() {
|
||||
/** Setup tablesorter **/
|
||||
$("table.sortable").tablesorter();
|
||||
|
||||
$(".shm-clink").each(function(idx, elm) {
|
||||
var target_id = $(elm).data("clink-sel");
|
||||
if(target_id && $(target_id).length > 0) {
|
||||
// if the target comment is already on this page, don't bother
|
||||
// switching pages
|
||||
$(elm).attr("href", target_id);
|
||||
// highlight it when clicked
|
||||
$(elm).click(function(e) {
|
||||
// This needs jQuery UI
|
||||
$(target_id).highlight();
|
||||
});
|
||||
// vanilla target name should already be in the URL tag, but this
|
||||
// will include the anon ID as displayed on screen
|
||||
$(elm).html("@"+$(target_id+" .username").html());
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
var sidebar_hidden = (Cookies.get("ui-sidebar-hidden") || "").split("|");
|
||||
for(var i in sidebar_hidden) {
|
||||
@ -87,69 +70,4 @@ $(document).ready(function() {
|
||||
tob.attr("disabled", false);
|
||||
});
|
||||
});
|
||||
|
||||
if(document.location.hash.length > 3) {
|
||||
var query = document.location.hash.substring(1);
|
||||
|
||||
$('#prevlink').attr('href', function(i, attr) {
|
||||
return attr + '?' + query;
|
||||
});
|
||||
$('#nextlink').attr('href', function(i, attr) {
|
||||
return attr + '?' + query;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* If an image list has a data-query attribute, append
|
||||
* that query string to all thumb links inside the list.
|
||||
* This allows us to cache the same thumb for all query
|
||||
* strings, adding the query in the browser.
|
||||
*/
|
||||
$(".shm-image-list").each(function(idx, elm) {
|
||||
var query = $(this).data("query");
|
||||
if(query) {
|
||||
$(this).find(".shm-thumb-link").each(function(idx2, elm2) {
|
||||
$(this).attr("href", $(this).attr("href") + query);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
||||
* LibShish-JS *
|
||||
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
function addEvent(obj, event, func, capture){
|
||||
if (obj.addEventListener){
|
||||
obj.addEventListener(event, func, capture);
|
||||
} else if (obj.attachEvent){
|
||||
obj.attachEvent("on"+event, func);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
|
||||
// used once in ext/setup/main
|
||||
function getHTTPObject() {
|
||||
if (window.XMLHttpRequest){
|
||||
return new XMLHttpRequest();
|
||||
}
|
||||
else if(window.ActiveXObject){
|
||||
return new ActiveXObject("Microsoft.XMLHTTP");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function replyTo(imageId, commentId, userId) {
|
||||
var box = $("#comment_on_"+imageId);
|
||||
var text = "[url=site://post/view/"+imageId+"#c"+commentId+"]@"+userId+"[/url]: ";
|
||||
|
||||
box.focus();
|
||||
box.val(box.val() + text);
|
||||
$("#c"+commentId).highlight();
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ define("CLI_LOG_LEVEL", 50);
|
||||
$_SERVER['QUERY_STRING'] = '/';
|
||||
|
||||
chdir(dirname(dirname(__FILE__)));
|
||||
require_once "core/_bootstrap.inc.php";
|
||||
require_once "core/_bootstrap.php";
|
||||
|
||||
if(is_null(User::by_name("demo"))) {
|
||||
$userPage = new UserPage();
|
||||
|
@ -1,14 +1,17 @@
|
||||
<phpunit bootstrap="../tests/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="shimmie">
|
||||
<testsuite name="core">
|
||||
<directory suffix="test.php">../core/</directory>
|
||||
</testsuite>
|
||||
<testsuite name="ext">
|
||||
<directory suffix="test.php">../ext/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">core</directory>
|
||||
<directory suffix=".php">ext</directory>
|
||||
<directory suffix=".php">themes/default</directory>
|
||||
<directory suffix=".php">../core</directory>
|
||||
<directory suffix=".php">../ext</directory>
|
||||
<directory suffix=".php">../themes/default</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
||||
|
@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Name: Danbooru Theme
|
||||
* Author: Bzchan <bzchan@animemahou.com>
|
||||
* Link: http://trac.shishnet.org/shimmie2/
|
||||
* Link: https://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: This is a simple theme changing the css to make shimme
|
||||
* look more like danbooru as well as adding a custom links
|
||||
@ -220,10 +220,10 @@ $header_html
|
||||
</article>
|
||||
<footer><em>
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
|
@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Name: Danbooru 2 Theme
|
||||
* Author: Bzchan <bzchan@animemahou.com>, updated by Daniel Oaks <danneh@danneh.net>
|
||||
* Link: http://trac.shishnet.org/shimmie2/
|
||||
* Link: https://code.shishnet.org/shimmie2/
|
||||
* License: GPLv2
|
||||
* Description: This is a simple theme changing the css to make shimme
|
||||
* look more like danbooru as well as adding a custom links
|
||||
@ -246,10 +246,10 @@ $header_html
|
||||
<footer><div>
|
||||
Running Shimmie –
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept<br />
|
||||
$debug
|
||||
$contact
|
||||
|
@ -77,10 +77,10 @@ $header_html
|
||||
</article>
|
||||
<footer>
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
|
@ -82,10 +82,10 @@ $header_html
|
||||
<footer>
|
||||
<hr>
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept.
|
||||
<br>Futaba theme based on 4chan's layout and CSS :3
|
||||
$debug
|
||||
|
@ -190,10 +190,10 @@ class Layout {
|
||||
$main_block_html
|
||||
<footer>
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept.<br />
|
||||
Lite Theme by <a href="http://seemslegit.com">Zach</a>
|
||||
$debug
|
||||
|
@ -3,14 +3,8 @@
|
||||
class CustomViewImageTheme extends ViewImageTheme {
|
||||
public function display_page(Image $image, $editor_parts) {
|
||||
global $page;
|
||||
$metatags = str_replace(" ", ", ", html_escape($image->get_tag_list()));
|
||||
$page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
|
||||
$page->set_heading(html_escape($image->get_tag_list()));
|
||||
$page->add_html_header("<meta name=\"keywords\" content=\"$metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:title\" content=\"$metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:type\" content=\"article\">");
|
||||
$page->add_html_header("<meta property=\"og:image\" content=\"".make_http($image->get_thumb_link())."\">");
|
||||
$page->add_html_header("<meta property=\"og:url\" content=\"".make_http(make_link("post/view/{$image->id}"))."\">");
|
||||
$page->add_block(new Block("Navigation", $this->build_navigation($image), "left", 0));
|
||||
$page->add_block(new Block("Statistics", $this->build_stats($image), "left", 15));
|
||||
$page->add_block(new Block(null, $this->build_info($image, $editor_parts), "main", 11));
|
||||
|
@ -60,7 +60,7 @@ EOD
|
||||
$message_html
|
||||
$counter_html
|
||||
<div class='mdl-typography--text-center' id='foot'>
|
||||
<p>$contact_link Serving $num_comma posts - Running <a href='http://code.shishnet.org/shimmie2/'>Shimmie</a>
|
||||
<p>$contact_link Serving $num_comma posts - Running <a href='https://code.shishnet.org/shimmie2/'>Shimmie</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -147,7 +147,7 @@ class Layout {
|
||||
$drawer_block_html
|
||||
</div>
|
||||
<nav class="mdl-navigation">
|
||||
<a class="mdl-navigation__link" href="http://code.shishnet.org/shimmie2/">Shimmie ©</a>
|
||||
<a class="mdl-navigation__link" href="https://code.shishnet.org/shimmie2/">Shimmie ©</a>
|
||||
</nav>
|
||||
</div>
|
||||
<main class="mdl-layout__content">
|
||||
|
@ -1,22 +1,12 @@
|
||||
<?php
|
||||
|
||||
class CustomViewImageTheme extends ViewImageTheme {
|
||||
|
||||
|
||||
/*
|
||||
* Build a page showing $image and some info about it
|
||||
*/
|
||||
public function display_page(Image $image, $editor_parts) {
|
||||
global $page;
|
||||
|
||||
$h_metatags = str_replace(" ", ", ", html_escape($image->get_tag_list()));
|
||||
|
||||
$page->set_title("Image {$image->id}: ".html_escape($image->get_tag_list()));
|
||||
$page->add_html_header("<meta name=\"keywords\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:title\" content=\"$h_metatags\">");
|
||||
$page->add_html_header("<meta property=\"og:type\" content=\"article\">");
|
||||
$page->add_html_header("<meta property=\"og:image\" content=\"".make_http($image->get_thumb_link())."\">");
|
||||
$page->add_html_header("<meta property=\"og:url\" content=\"".make_http(make_link("post/view/{$image->id}"))."\">");
|
||||
$page->set_heading(html_escape($image->get_tag_list()));
|
||||
$page->add_block(new Block(null, $this->build_pin($image), "subtoolbar", 0));
|
||||
$page->add_block(new Block(null, $this->build_info($image, $editor_parts), "left", 20));
|
||||
|
@ -93,10 +93,10 @@ $header_html
|
||||
</article>
|
||||
<footer>
|
||||
Images © their respective owners,
|
||||
<a href="http://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="http://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://code.shishnet.org/shimmie2/">Shimmie</a> ©
|
||||
<a href="https://www.shishnet.org/">Shish</a> &
|
||||
<a href="https://github.com/shish/shimmie2/graphs/contributors">The Team</a>
|
||||
2007-2016,
|
||||
2007-2018,
|
||||
based on the Danbooru concept.
|
||||
$debug
|
||||
$contact
|
||||
|
Loading…
x
Reference in New Issue
Block a user