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