split up files in core/ for saner management
This commit is contained in:
parent
3c5c44d75f
commit
b2f10ea5ab
32 changed files with 2799 additions and 2842 deletions
|
@ -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" : {
|
||||
|
|
|
@ -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) {
|
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;
|
||||
}
|
|
@ -407,6 +407,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.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
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
require_once "util.inc.php";
|
||||
require_once "core/polyfills.php";
|
||||
|
||||
class CoreUtilIO extends \PHPUnit\Framework\TestCase {
|
||||
class PolyfillsTest extends \PHPUnit\Framework\TestCase {
|
||||
public function test_html_escape() {
|
||||
$this->assertEquals(
|
||||
html_escape("Foo & <waffles>"),
|
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);
|
||||
}
|
||||
}
|
||||
|
1774
core/util.inc.php
1774
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;
|
||||
}
|
|
@ -86,7 +86,7 @@ EOD;
|
|||
}
|
||||
|
||||
try {
|
||||
require_once "core/_bootstrap.inc.php";
|
||||
require_once "core/_bootstrap.php";
|
||||
$_shm_ctx->log_start(@$_SERVER["REQUEST_URI"], true, true);
|
||||
|
||||
// start the page generation waterfall
|
||||
|
|
|
@ -103,7 +103,6 @@ assert_options(ASSERT_BAIL, 1);
|
|||
define('__SHIMMIE_ROOT__', trim(rtrim(dirname(__FILE__), '/\\')) . '/');
|
||||
|
||||
// Pull in necessary files
|
||||
require_once __SHIMMIE_ROOT__."core/util.inc.php";
|
||||
require_once __SHIMMIE_ROOT__."core/exceptions.class.php";
|
||||
require_once __SHIMMIE_ROOT__."core/database.class.php";
|
||||
|
||||
|
@ -112,7 +111,7 @@ if(is_readable("data/config/shimmie.conf.php")) die("Shimmie is already installe
|
|||
do_install();
|
||||
|
||||
// utilities {{{
|
||||
// TODO: Can some of these be pushed into "core/util.inc.php" ?
|
||||
// TODO: Can some of these be pushed into "core/???.inc.php" ?
|
||||
|
||||
function check_gd_version(): int {
|
||||
$gdversion = 0;
|
||||
|
|
|
@ -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();
|
||||
|
|
Reference in a new issue