diff --git a/composer.json b/composer.json index 09469b50..025aadfe 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "bower-asset/jquery-timeago" : "1.5.2", "bower-asset/tablesorter" : "dev-master", "bower-asset/mediaelement" : "2.21.1", - "bower-asset/js-cookie" : "2.1.1" + "bower-asset/js-cookie" : "2.1.1", + "ext-pdo": "*" }, "require-dev" : { diff --git a/core/_bootstrap.inc.php b/core/_bootstrap.php similarity index 89% rename from core/_bootstrap.inc.php rename to core/_bootstrap.php index 5ed87783..025a4e6a 100644 --- a/core/_bootstrap.inc.php +++ b/core/_bootstrap.php @@ -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) { diff --git a/core/basethemelet.class.php b/core/basethemelet.php similarity index 100% rename from core/basethemelet.class.php rename to core/basethemelet.php diff --git a/core/block.class.php b/core/block.php similarity index 100% rename from core/block.class.php rename to core/block.php diff --git a/core/cacheengine.php b/core/cacheengine.php new file mode 100644 index 00000000..4103e3d2 --- /dev/null +++ b/core/cacheengine.php @@ -0,0 +1,207 @@ +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;} +} diff --git a/core/captcha.php b/core/captcha.php new file mode 100644 index 00000000..99f5e77d --- /dev/null +++ b/core/captcha.php @@ -0,0 +1,55 @@ +is_anonymous() && $config->get_bool("comment_captcha")) { + $r_publickey = $config->get_string("api_recaptcha_pubkey"); + if(!empty($r_publickey)) { + $captcha = " +
+ "; + } 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; +} + + diff --git a/core/config.class.php b/core/config.php similarity index 100% rename from core/config.class.php rename to core/config.php diff --git a/core/database.class.php b/core/database.class.php deleted file mode 100644 index a8addf7a..00000000 --- a/core/database.class.php +++ /dev/null @@ -1,803 +0,0 @@ -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("Database Transaction Error: Unable to call commit() as there is no transaction currently open."); - } - } - else { - throw new SCoreException("
Database Transaction Error: 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("
Database Transaction Error: Unable to call rollback() as there is no transaction currently open."); - } - } - else { - throw new SCoreException("
Database Transaction Error: 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()."
Query: ".$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() {} -} - diff --git a/core/database.php b/core/database.php new file mode 100644 index 00000000..5cd531fd --- /dev/null +++ b/core/database.php @@ -0,0 +1,402 @@ +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("
Database Transaction Error: Unable to call commit() as there is no transaction currently open."); + } + } + else { + throw new SCoreException("
Database Transaction Error: 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("
Database Transaction Error: Unable to call rollback() as there is no transaction currently open."); + } + } + else { + throw new SCoreException("
Database Transaction Error: 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()."
Query: ".$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() {}
+}
+
diff --git a/core/dbengine.php b/core/dbengine.php
new file mode 100644
index 00000000..18b6f512
--- /dev/null
+++ b/core/dbengine.php
@@ -0,0 +1,142 @@
+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";
+ }
+}
diff --git a/core/email.class.php b/core/email.php
similarity index 100%
rename from core/email.class.php
rename to core/email.php
diff --git a/core/event.class.php b/core/event.php
similarity index 100%
rename from core/event.class.php
rename to core/event.php
diff --git a/core/exceptions.class.php b/core/exceptions.php
similarity index 100%
rename from core/exceptions.class.php
rename to core/exceptions.php
diff --git a/core/extension.class.php b/core/extension.php
similarity index 100%
rename from core/extension.class.php
rename to core/extension.php
diff --git a/core/imageboard.pack.php b/core/imageboard/image.php
similarity index 80%
rename from core/imageboard.pack.php
rename to core/imageboard/image.php
index ecfae75a..68ccd612 100644
--- a/core/imageboard.pack.php
+++ b/core/imageboard/image.php
@@ -1,28 +1,4 @@
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));
- }
-}
-
diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php
new file mode 100644
index 00000000..cbc0e723
--- /dev/null
+++ b/core/imageboard/misc.php
@@ -0,0 +1,107 @@
+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));
+ }
+}
diff --git a/core/imageboard/search.php b/core/imageboard/search.php
new file mode 100644
index 00000000..8c1e4079
--- /dev/null
+++ b/core/imageboard/search.php
@@ -0,0 +1,49 @@
+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;
+ }
+}
diff --git a/core/imageboard/tag.php b/core/imageboard/tag.php
new file mode 100644
index 00000000..e3e7df7b
--- /dev/null
+++ b/core/imageboard/tag.php
@@ -0,0 +1,104 @@
+ 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;
+ }
+}
diff --git a/core/logging.php b/core/logging.php
new file mode 100644
index 00000000..b39c4137
--- /dev/null
+++ b/core/logging.php
@@ -0,0 +1,100 @@
+= $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;
+}
diff --git a/core/page.class.php b/core/page.php
similarity index 99%
rename from core/page.class.php
rename to core/page.php
index 924f200d..f802dbaf 100644
--- a/core/page.class.php
+++ b/core/page.php
@@ -407,6 +407,3 @@ class Page {
$this->add_html_header("", 100);
}
}
-
-class MockPage extends Page {
-}
diff --git a/core/polyfills.php b/core/polyfills.php
new file mode 100644
index 00000000..9e867cfd
--- /dev/null
+++ b/core/polyfills.php
@@ -0,0 +1,779 @@
+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 ? "" : $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;
+}
diff --git a/core/send_event.php b/core/send_event.php
new file mode 100644
index 00000000..473e0bc6
--- /dev/null
+++ b/core/send_event.php
@@ -0,0 +1,134 @@
+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();
+}
diff --git a/core/sys_config.inc.php b/core/sys_config.php
similarity index 95%
rename from core/sys_config.inc.php
rename to core/sys_config.php
index b54726f6..6ae38347 100644
--- a/core/sys_config.inc.php
+++ b/core/sys_config.php
@@ -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.1');// 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
diff --git a/core/util.test.php b/core/tests/polyfills.test.php
similarity index 92%
rename from core/util.test.php
rename to core/tests/polyfills.test.php
index 3187f682..c1797092 100644
--- a/core/util.test.php
+++ b/core/tests/polyfills.test.php
@@ -1,7 +1,7 @@
assertEquals(
html_escape("Foo &