Initial setup for framework
This commit is contained in:
		
							parent
							
								
									3ddce5e41f
								
							
						
					
					
						commit
						3cc233de3a
					
				
					 16 changed files with 720 additions and 0 deletions
				
			
		
							
								
								
									
										6
									
								
								DatabaseMigrations.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								DatabaseMigrations.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| <?php | ||||
| require_once("Settings.php"); | ||||
| 
 | ||||
| $db = new Database(!file_exists(SQLITE_DB)); | ||||
| $db->runMigrations(); | ||||
| $db->close(); | ||||
							
								
								
									
										19
									
								
								LastLog.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								LastLog.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| <?php | ||||
| require_once("Settings.php"); | ||||
| 
 | ||||
| $limit = 100; | ||||
| if (isset($argv[1]) && is_numeric($argv[1])) { | ||||
| 	$limit = $argv[1]; | ||||
| } | ||||
| 
 | ||||
| $db = new Database(); | ||||
| $stmt = $db::$handle->prepare("SELECT * FROM (SELECT * FROM `logs` ORDER BY datetime(`created`) DESC, `id` DESC LIMIT " . $limit . ") ORDER BY `created` ASC, `id` ASC"); | ||||
| $result = $stmt->execute(); | ||||
| 
 | ||||
| while ($row = $result->fetchArray(SQLITE3_ASSOC)) { | ||||
| 	$levelColor = Logger::getColor($row["level"]); | ||||
| 
 | ||||
| 	echo "\033[0;37m[" . $row["created"] . "] " . $levelColor . $row["level"] . "\033[0;37m: " . $row["message"] . "\033[0m" . PHP_EOL; | ||||
| } | ||||
| 
 | ||||
| $db->close(); | ||||
							
								
								
									
										63
									
								
								Loader.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Loader.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| <?php | ||||
| class Loader { | ||||
| 	protected static array $dirs = [ | ||||
| 		"LIB_MIGRATIONS_DIR", | ||||
| 		"LIB_TASKS_DIR", | ||||
| 		"LIB_TEMPLATE_DIR", | ||||
| 		"LIB_LIB_DIR", | ||||
| 		"TASKS_DIR", | ||||
| 		"TEMPLATE_DIR", | ||||
| 		"LIB_DIR" | ||||
| 	]; | ||||
| 
 | ||||
| 	protected static $classmap = []; | ||||
| 
 | ||||
| 	public static function _autoload($class) { | ||||
| 
 | ||||
| 		if (count(self::$classmap) === 0) { | ||||
| 			self::buildRecursiveClassMap(); | ||||
| 		} | ||||
| 
 | ||||
| 		if (isset(self::$classmap[$class])) { | ||||
| 			require_once(self::$classmap[$class]); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	protected static function buildRecursiveClassMap() { | ||||
| 		foreach (self::$dirs as $dir) { | ||||
| 			if (!defined($dir)) { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			$dir = constant($dir); | ||||
| 
 | ||||
| 			if (!is_dir($dir) || !is_readable($dir)) { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			self::buildClassMap($dir); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	protected static function buildClassMap($dir) { | ||||
| 		$files = scandir($dir); | ||||
| 		foreach ($files as $file) { | ||||
| 			if ($file === "." || $file === "..") { | ||||
| 				continue; | ||||
| 			} | ||||
| 			if (is_dir($file)) { | ||||
| 				self::buildClassMap($file); | ||||
| 			} else { | ||||
| 				$parts = explode(".", $file); | ||||
| 				$ext = array_pop($parts); | ||||
| 				if ($ext === "php") { | ||||
| 					$classname = array_shift($parts); | ||||
| 					self::$classmap[$classname] = $dir . "/" . $file; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| spl_autoload_register("Loader::_autoload"); | ||||
							
								
								
									
										17
									
								
								RunTask.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								RunTask.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| <?php | ||||
| require_once("Settings.php"); | ||||
| 
 | ||||
| if (!isset($argv[1]) || empty($argv[1])) { | ||||
| 	Logger::log("No task specified", Logger::ERROR, "RunTask"); | ||||
| 	exit(1); | ||||
| } | ||||
| 
 | ||||
| $task = $argv[1] . "Task"; | ||||
| if (!file_exists(LIB_DIR . "/tasks/" . $task . ".php")) { | ||||
| 	Logger::log("Task " . $task . " does not exist", Logger::ERROR, "RunTask"); | ||||
| 	exit(1); | ||||
| } | ||||
| 
 | ||||
| require_once(LIB_DIR . "/tasks/" . $task . ".php"); | ||||
| $task = new $task(); | ||||
| $task->run(); | ||||
							
								
								
									
										24
									
								
								Settings.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Settings.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| <?php | ||||
| set_error_handler(function ($errno, $errstr, $errfile, $errline) { | ||||
| 	throw new ErrorException($errstr, 0, $errno, $errfile, $errline); | ||||
| }); | ||||
| 
 | ||||
| set_exception_handler(function ($exception) { | ||||
| 	echo $exception->getMessage() . " on line " . $exception->getLine() . " in " . $exception->getFile() . PHP_EOL; | ||||
| 	exit; | ||||
| }); | ||||
| 
 | ||||
| if (!defined("PROJECT_DIR") ) { | ||||
| 	define("PROJECT_DIR", rtrim( dirname(__FILE__ . "/../.."), "/")); | ||||
| } | ||||
| 
 | ||||
| if (!defined("SQLITE_DB")) { | ||||
| 	throw new Exception("SQLITE_DB not defined"); | ||||
| } | ||||
| 
 | ||||
| define("LIB_MIGRATIONS_DIR", dirname(__FILE__) . "/schema"); | ||||
| define("LIB_TASKS_DIR", dirname(__FILE__) . "/tasks"); | ||||
| define("LIB_TEMPLATE_DIR", dirname(__FILE__) . "/templates"); | ||||
| define("LIB_LIB_DIR", dirname(__FILE__) . "/lib"); | ||||
| 
 | ||||
| require_once(dirname(__FILE__) . "/Loader.php"); | ||||
							
								
								
									
										120
									
								
								lib/Cache.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								lib/Cache.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,120 @@ | |||
| <?php | ||||
| class Cache { | ||||
| 	public static function getCacheFile(string $name, $maxAge = 3600) { | ||||
| 		$cacheFile = CACHE_DIR . "/" . $name; | ||||
| 
 | ||||
| 		if (file_exists($cacheFile) && filemtime($cacheFile) > time() - $maxAge) { | ||||
| 			Logger::log("Cache hit for " . $name, Logger::IOREAD, "Cache"); | ||||
| 			return COMPRESS_LOCAL_CACHE ? gzuncompress(file_get_contents($cacheFile)) : file_get_contents($cacheFile); | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	public static function saveCacheFile(string $name, string $data) { | ||||
| 		$cacheFile = CACHE_DIR . "/" . $name; | ||||
| 		file_put_contents($cacheFile, COMPRESS_LOCAL_CACHE ? gzcompress($data,9) : $data ); | ||||
| 		Logger::log("Saved cache file " . $name, Logger::IOWRITE, "Cache"); | ||||
| 	} | ||||
| 
 | ||||
| 	public static function publicCacheExists(string $name, bool $skipExtensionCheck = false) { | ||||
| 		// For now cache is not reliable since we use a generated name.
 | ||||
| 		return false; | ||||
| 		/*if (!$skipExtensionCheck) { | ||||
| 			$ext = substr($name, strrpos($name, ".") + 1); | ||||
| 			if (strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png") { | ||||
| 				$name = str_replace("." . $ext, ".webp", strtolower($name)); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		$cacheFile = PUBLIC_CACHE_DIR . "/" . $name; | ||||
| 
 | ||||
| 		try { | ||||
| 			$fe = file_exists($cacheFile); | ||||
| 		} catch (Exception $e) { | ||||
| 			$fe = false; | ||||
| 			Logger::log("Could not check if public cache file " . $name . " exists: " . $e->getMessage(), Logger::ERROR, "Cache"); | ||||
| 		} | ||||
| 
 | ||||
| 		return $fe;*/ | ||||
| 	} | ||||
| 
 | ||||
| 	public static function savePublicCacheFile(string $name, string $data, int $cacheLevel = 70, bool $skipExtensionCheck = false) { | ||||
| 		$name = strtolower($name); | ||||
| 
 | ||||
| 		$ext = substr($name, strrpos($name, ".") + 1); | ||||
| 		if ($skipExtensionCheck || strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png") { | ||||
| 			$size = strlen($data); | ||||
| 			$img = new Imagick(); | ||||
| 			$img->readImageBlob($data); | ||||
| 			$img->setImageFormat('webp'); | ||||
| 			$img->setImageCompressionQuality($cacheLevel); | ||||
| 			$img->stripImage(); | ||||
| 			$data = $img->getImageBlob(); | ||||
| 			$name = str_replace("." . $ext, ".webp", strtolower($name)); | ||||
| 
 | ||||
| 			$cacheFile = PUBLIC_CACHE_DIR . "/" . $name; | ||||
| 			$blurhash = self::generateBlurHash($cacheFile, $img, $skipExtensionCheck); | ||||
| 			if (!empty($blurhash)) { | ||||
| 				$name = "BH-" . bin2hex($blurhash) . "-W" . $img->getImageWidth() . "xH" . $img->getImageHeight() . ".webp"; | ||||
| 				$cacheFile = PUBLIC_CACHE_DIR . "/" . $name; | ||||
| 			} | ||||
| 
 | ||||
| 			// For now don't transfer file if its generated name exists
 | ||||
| 			if (file_exists($cacheFile)) { | ||||
| 				Logger::log("Public cache file " . $name . " already exists", Logger::INFO, "Cache"); | ||||
| 				return PUBLIC_CACHE_URL . "/" . $name; | ||||
| 			} | ||||
| 
 | ||||
| 			Logger::log("Compressed image from " . $size . " to " . strlen($data) . " bytes", Logger::METRICS, "Cache"); | ||||
| 		} | ||||
| 
 | ||||
| 		$context = stream_context_create([ | ||||
| 			's3' => [ | ||||
| 				'ACL' => 'public-read' | ||||
| 			] | ||||
| 		]); | ||||
| 
 | ||||
| 		file_put_contents($cacheFile, $data, 0, $context); | ||||
| 
 | ||||
| 		Logger::log("Saved public cache file " . $name, Logger::IOWRITE, "Cache"); | ||||
| 
 | ||||
| 		return PUBLIC_CACHE_URL . "/" . $name; | ||||
| 	} | ||||
| 
 | ||||
| 	public static function generateBlurHash(string $cacheFile, ?IMagick $img = null, bool $skipExtensionCheck = false):?string { | ||||
| 		$ext = substr($cacheFile, strrpos($cacheFile, ".") + 1); | ||||
| 		if ($skipExtensionCheck || strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png" || strtolower($ext) == "webp") { | ||||
| 
 | ||||
| 			$hashFile = strtolower($cacheFile); | ||||
| 			$hashFileName = substr($hashFile, strrpos($hashFile, "/") + 1); | ||||
| 			if (file_exists($hashFile)) { | ||||
| 				return null; | ||||
| 			} | ||||
| 
 | ||||
| 			$pixels = []; | ||||
| 
 | ||||
| 			if ($img === null) { | ||||
| 				$img = new Imagick(); | ||||
| 				$img->readImage($cacheFile); | ||||
| 			} | ||||
| 
 | ||||
| 			$width = $img->getImageWidth(); | ||||
| 			$height = $img->getImageHeight(); | ||||
| 
 | ||||
| 			for ($y = 0; $y < $height; $y++) { | ||||
| 				$row = []; | ||||
| 				for ($x = 0; $x < $width; $x++) { | ||||
| 					$pixel = $img->getImagePixelColor($x, $y); | ||||
| 					$color = $pixel->getColor(); | ||||
| 					$row[] = [$color["r"], $color["g"], $color["b"]]; | ||||
| 				} | ||||
| 				$pixels[] = $row; | ||||
| 			} | ||||
| 
 | ||||
| 			$hash = \kornrunner\Blurhash\Blurhash::encode($pixels, BLURHASH_X, BLURHASH_Y); | ||||
| 
 | ||||
| 			return $hash; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										95
									
								
								lib/Database.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								lib/Database.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| <?php | ||||
| class Database | ||||
| { | ||||
| 	public static $handle = null; | ||||
| 
 | ||||
| 	public function __construct($createDatabase = false) | ||||
| 	{ | ||||
| 		if (self::$handle === null) { | ||||
| 			self::$handle = new SQLite3(SQLITE_DB); | ||||
| 			self::$handle->enableExceptions(true); | ||||
| 
 | ||||
| 
 | ||||
| 			if ($createDatabase === true) { | ||||
| 				$this->createDatabase(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function createDatabase() | ||||
| 	{ | ||||
| 		$schemaFiles = [ | ||||
| 			LIB_MIGRATIONS_DIR . "/_schema.sql", | ||||
| 			MIGRATIONS_DIR . "/_schema.sql" | ||||
| 		]; | ||||
| 
 | ||||
| 		foreach ($schemaFiles as $schemaFile) { | ||||
| 			if (file_exists($schemaFile)) { | ||||
| 				$schema = file_get_contents($schemaFile); | ||||
| 				self::$handle->exec($schema); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function runMigrations() | ||||
| 	{ | ||||
| 		$currentTableVersions = []; | ||||
| 
 | ||||
| 		$result = self::$handle->query("SELECT `table`,`version` FROM `migrations`"); | ||||
| 		while ($row = $result->fetchArray(SQLITE3_ASSOC)) { | ||||
| 			$currentTableVersions[$row["table"]] = $row["version"]; | ||||
| 		} | ||||
| 
 | ||||
| 		$migrations = []; | ||||
| 		$migrationPaths = [LIB_MIGRATIONS_DIR, MIGRATIONS_DIR]; | ||||
| 		foreach ($migrationPaths as $migrationPath) { | ||||
| 			if (is_dir($migrationPath)) { | ||||
| 				$migrations = array_merge($migrations, scandir($migrationPath)); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		$migrationCount = 0; | ||||
| 
 | ||||
| 		foreach ($migrations as $migration) { | ||||
| 			if ($migration == "." || $migration == ".." || $migration == "_schema.sql") { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			$migrationParts = explode("-", str_replace(".sql", "", $migration)); | ||||
| 			$migrationTable = $migrationParts[0]; | ||||
| 			$migrationVersion = $migrationParts[1]; | ||||
| 
 | ||||
| 			$run = false; | ||||
| 
 | ||||
| 			if (!isset($currentTableVersions[$migrationTable])) { | ||||
| 				$run = true; | ||||
| 			} else { | ||||
| 				if ($migrationVersion > $currentTableVersions[$migrationTable]) { | ||||
| 					$run = true; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if ($run === true) { | ||||
| 				Logger::log("Found migration for table " . $migrationTable . " version " . $migrationVersion, LOGGER::INFO, "Database"); | ||||
| 				$migrationContents = file_get_contents(MIGRATIONS_DIR . "/" . $migration); | ||||
| 				if (@self::$handle->exec($migrationContents)) { | ||||
| 					self::$handle->exec("INSERT INTO `migrations` (`table`,`version`, `schemafile`, `created`) VALUES ('" . $migrationTable . "'," . $migrationVersion . ",'" . $migration . "', strftime('%Y-%m-%d %H:%M:%S','now'))"); | ||||
| 					$migrationCount++; | ||||
| 				} else { | ||||
| 					Logger::log("Failed to run migration for table " . $migrationTable . " version " . $migrationVersion . " with error: " . self::$handle->lastErrorMsg(), LOGGER::ERROR, "Database"); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if ($migrationCount > 0) { | ||||
| 			Logger::log("Ran " . $migrationCount . " migrations", LOGGER::DEBUG, "Database"); | ||||
| 		} else { | ||||
| 			Logger::log("No migrations to run", LOGGER::DEBUG, "Database"); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function close() | ||||
| 	{ | ||||
| 		self::$handle->close(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										28
									
								
								lib/EmptyTemplate.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								lib/EmptyTemplate.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| <?php | ||||
| class EmptyTemplate implements ITemplate | ||||
| { | ||||
| 	protected Template $template; | ||||
| 	protected ?object $options = null; | ||||
| 
 | ||||
| 	public function __construct(Template &$template, ?object $options = null) | ||||
| 	{ | ||||
| 		$this->template = $template; | ||||
| 		$this->options = $options; | ||||
| 	} | ||||
| 
 | ||||
| 	public function load() | ||||
| 	{ | ||||
| 	} | ||||
| 
 | ||||
| 	public function pretransform() | ||||
| 	{ | ||||
| 	} | ||||
| 
 | ||||
| 	public function transform() | ||||
| 	{ | ||||
| 	} | ||||
| 
 | ||||
| 	public function save() | ||||
| 	{ | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										8
									
								
								lib/ITemplate.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								lib/ITemplate.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| <?php | ||||
| interface ITemplate { | ||||
| 	public function __construct(Template &$template, ?object $options = null); | ||||
| 	public function load(); | ||||
| 	public function pretransform(); | ||||
| 	public function transform(); | ||||
| 	public function save(); | ||||
| } | ||||
							
								
								
									
										53
									
								
								lib/KeyValue.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/KeyValue.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| <?php | ||||
| class KeyValue { | ||||
| 	protected static $kv = null; | ||||
| 
 | ||||
| 	public static function get(string $keyname, ?string $channel = null) { | ||||
| 		if (self::$kv === null) { | ||||
| 			self::load(); | ||||
| 		} | ||||
| 
 | ||||
| 		if ($channel === null) { | ||||
| 			$channel = "all"; | ||||
| 		} | ||||
| 
 | ||||
| 		if (isset(self::$kv[$keyname][$channel])) { | ||||
| 			return self::$kv[$keyname][$channel]; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	public static function save(string $keyname, string $value, ?string $channel = null) { | ||||
| 		if (self::$kv === null) { | ||||
| 			self::load(); | ||||
| 		} | ||||
| 
 | ||||
| 		if ($channel === null) { | ||||
| 			$channel = "all"; | ||||
| 		} | ||||
| 
 | ||||
| 		$db = new Database(); | ||||
| 		$stmt = $db::$handle->prepare("INSERT OR REPLACE INTO `keyvalue` (`key`, `value`, `channel`, `created`) VALUES (:key, :value, :channel, strftime('%Y-%m-%d %H:%M:%S','now'))"); | ||||
| 		$stmt->bindValue(":key", $keyname, SQLITE3_TEXT); | ||||
| 		$stmt->bindValue(":value", $value, SQLITE3_TEXT); | ||||
| 		$stmt->bindValue(":channel", $channel, SQLITE3_TEXT); | ||||
| 		$stmt->execute(); | ||||
| 
 | ||||
| 		self::$kv[$keyname][$channel] = $value; | ||||
| 	} | ||||
| 
 | ||||
| 	protected static function load() { | ||||
| 		$db = new Database(); | ||||
| 		$stmt = $db::$handle->prepare("SELECT * FROM `keyvalue`"); | ||||
| 		$result = $stmt->execute(); | ||||
| 		while ($row = $result->fetchArray(SQLITE3_ASSOC)) { | ||||
| 			$channel = $row["channel"]; | ||||
| 			if ($channel === null) { | ||||
| 				$channel = "all"; | ||||
| 			} | ||||
| 
 | ||||
| 			self::$kv[$row["key"]][$channel] = $row["value"]; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										40
									
								
								lib/Logger.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								lib/Logger.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| <?php | ||||
| class Logger { | ||||
| 	const INFO = "INFO"; | ||||
| 	const WARNING = "WARNING"; | ||||
| 	const ERROR = "ERROR"; | ||||
| 	const DEBUG = "DEBUG"; | ||||
| 	const IOREAD = "IOREAD"; | ||||
| 	const IOWRITE = "IOWRITE"; | ||||
| 	const METRICS = "METRICS"; | ||||
| 
 | ||||
| 	public static function getColor(string $level):string { | ||||
| 		match ($level) { | ||||
| 			self::INFO => $levelColor = "\033[0;32m", | ||||
| 			self::WARNING => $levelColor = "\033[0;33m", | ||||
| 			self::ERROR => $levelColor = "\033[0;31m", | ||||
| 			self::DEBUG => $levelColor = "\033[0;34m", | ||||
| 			self::IOREAD => $levelColor = "\033[0;35m", | ||||
| 			self::IOWRITE => $levelColor = "\033[0;36m", | ||||
| 			self::METRICS => $levelColor = "\033[0;41m", | ||||
| 			default => $levelColor = "\033[0;37m" | ||||
| 		}; | ||||
| 
 | ||||
| 		return $levelColor; | ||||
| 	} | ||||
| 
 | ||||
| 	public static function log(string $message, string $level = "INFO", string $context = "") { | ||||
| 
 | ||||
| 		$db = new Database(); | ||||
| 		$stmt = $db::$handle->prepare("INSERT INTO `logs` (`level`, `message`, `context`, `created`) VALUES (:level, :message, :context, strftime('%Y-%m-%d %H:%M:%S','now'))"); | ||||
| 		$stmt->bindValue(":level", $level, SQLITE3_TEXT); | ||||
| 		$stmt->bindValue(":message", $message, SQLITE3_TEXT); | ||||
| 		$stmt->bindValue(":context", $context, SQLITE3_TEXT); | ||||
| 
 | ||||
| 		@$stmt->execute(); | ||||
| 
 | ||||
| 		$levelColor = self::getColor($level); | ||||
| 
 | ||||
| 		echo "\033[0;37m[" . date("Y-m-d H:i:s") . "] " . $levelColor . $level . "\033[0;37m: " . $message . "\033[0m" . PHP_EOL; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										10
									
								
								lib/Task.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/Task.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| <?php | ||||
| abstract class Task { | ||||
| 	public function run() { | ||||
| 		Logger::log("Running task " . get_class($this), Logger::DEBUG, "Task"); | ||||
| 		$this->execute(); | ||||
| 		Logger::log("Finished task " . get_class($this), Logger::DEBUG, "Task"); | ||||
| 	} | ||||
| 
 | ||||
| 	abstract protected function execute(); | ||||
| } | ||||
							
								
								
									
										207
									
								
								lib/Template.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								lib/Template.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,207 @@ | |||
| <?php | ||||
| class Template { | ||||
| 	protected $template = null; | ||||
| 	protected $data = array(); | ||||
| 
 | ||||
| 	public function __construct(string $template, ?array $parentData = null) { | ||||
| 		$this->template = $template; | ||||
| 
 | ||||
| 		if (isset($parentData)) { | ||||
| 			$this->data = $parentData; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function set(string $key, $value) { | ||||
| 		$this->data[$key] = $value; | ||||
| 	} | ||||
| 
 | ||||
| 	public function render($options = null): string { | ||||
| 		Logger::log("Rendering template " . $this->template, Logger::IOREAD, "Template"); | ||||
| 
 | ||||
| 		$class = is_object($options) && isset($options->class) ? new $options->class($this, $options) : new EmptyTemplate($this, $options); | ||||
| 		$class->load(); | ||||
| 		$contents = file_get_contents(TEMPLATE_DIR . "/" . $this->template); | ||||
| 
 | ||||
| 		$class->pretransform(); | ||||
| 		// Replace includes of templates
 | ||||
| 		$matches = array(); | ||||
| 		preg_match_all("/{{include (.*\.html)(.*)}}/", $contents, $matches); | ||||
| 		for ($i = 0; $i < count($matches[1]); $i++) { | ||||
| 			$matchString = $matches[0][$i]; | ||||
| 			$match = $matches[1][$i]; | ||||
| 			$options = trim($matches[2][$i]); | ||||
| 
 | ||||
| 			$include = new Template(dirname($this->template) . "/" . $match, $this->data); | ||||
| 			$contents = str_replace($matchString, $include->render( | ||||
| 				!empty($options) ? json_decode($options, false, 512, JSON_THROW_ON_ERROR) : null | ||||
| 			), $contents); | ||||
| 		} | ||||
| 
 | ||||
| 		$class->transform(); | ||||
| 
 | ||||
| 		// Replace variables
 | ||||
| 		foreach ($this->data as $key => $value) { | ||||
| 			if (is_string($value) || is_numeric($value)) { | ||||
| 				$contents = str_replace("{{" . $key . "}}", $value, $contents); | ||||
| 			} else { | ||||
| 				$contents = str_replace("{{" . $key . "}}", "", $contents); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// Conditional tags are written as {{?<variablename>}}some content{{/<variablename>}}, find conditional tags and evaluate them if they exist in $this->data
 | ||||
| 		$matches = array(); | ||||
| 		preg_match_all("/{{\?(.*)}}(.*){{\/(.*)}}/sU", $contents, $matches); | ||||
| 		for ($i = 0; $i < count($matches[1]); $i++) { | ||||
| 			$matchString = $matches[0][$i]; | ||||
| 			$match = $matches[1][$i]; | ||||
| 			$content = $matches[2][$i]; | ||||
| 
 | ||||
| 			if (isset($this->data[$match])) { | ||||
| 				$contents = str_replace($matchString, $content, $contents); | ||||
| 			} else { | ||||
| 				$contents = str_replace($matchString, "", $contents); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		$class->save(); | ||||
| 
 | ||||
| 		return $contents; | ||||
| 	} | ||||
| 
 | ||||
| 	public function save(string $filename, string $contents, bool $minify = true) { | ||||
| 		Logger::log("Saving template to " . $filename, Logger::IOWRITE, "Template"); | ||||
| 
 | ||||
| 		if ($minify) { | ||||
| 			$size = strlen($contents); | ||||
| 
 | ||||
| 			$doc = new DOMDocument(); | ||||
| 			$doc->preserveWhiteSpace = false; | ||||
| 			$doc->formatOutput = false; | ||||
| 			$doc->normalizeDocument(); | ||||
| 
 | ||||
| 			libxml_use_internal_errors(true); | ||||
| 
 | ||||
| 			$doc->loadHTML($contents); | ||||
| 
 | ||||
| 			libxml_use_internal_errors(false); | ||||
| 
 | ||||
| 			$options = [ | ||||
| 				'keep_whitespace_around' => [ | ||||
| 					// keep whitespace around inline elements
 | ||||
| 					'b', 'big', 'i', 'small', 'tt', | ||||
| 					'abbr', 'acronym', 'cite', 'code', 'dfn', 'em', 'kbd', 'strong', 'samp', 'var', | ||||
| 					'a', 'bdo', 'br', 'img', 'map', 'object', 'q', 'span', 'sub', 'sup', | ||||
| 					'button', 'input', 'label', 'select', 'textarea' | ||||
| 				], | ||||
| 				'keep_whitespace_in' => [/*'script', 'style',*/ 'pre'], | ||||
| 				'remove_empty_attributes' => ['style', 'class'], | ||||
| 				'indent_characters' => "\t" | ||||
| 			]; | ||||
| 
 | ||||
| 			//Comments,empty,whitespace
 | ||||
| 			$xpath = new \DOMXPath($doc); | ||||
| 			foreach ($xpath->query('//comment()') as $comment) { | ||||
| 				$comment->parentNode->removeChild($comment); | ||||
| 			} | ||||
| 
 | ||||
| 			$xpath = new \DOMXPath($doc); | ||||
| 			foreach ($options['remove_empty_attributes'] as $attr) { | ||||
| 				foreach ($xpath->query('//*[@' . $attr . ']') as $el) { | ||||
| 					if (trim($el->getAttribute($attr)) == '') { | ||||
| 						$el->removeAttribute($attr); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			$x = new \DOMXPath($doc); | ||||
| 			$nodeList = $x->query("//text()"); | ||||
| 			foreach ($nodeList as $node) { | ||||
| 				/** @var \DOMNode $node */ | ||||
| 
 | ||||
| 				if (in_array($node->parentNode->nodeName, $options['keep_whitespace_in'])) { | ||||
| 					continue; | ||||
| 				}; | ||||
| 
 | ||||
| 				$node->nodeValue = str_replace(["\r", "\n", "\t"], ' ', $node->nodeValue); | ||||
| 
 | ||||
| 				while (strpos($node->nodeValue, '  ') !== false) { | ||||
| 					$node->nodeValue = str_replace('  ', ' ', $node->nodeValue); | ||||
| 				} | ||||
| 
 | ||||
| 				if (!in_array($node->parentNode->nodeName, $options['keep_whitespace_around'])) { | ||||
| 					if (!($node->previousSibling && in_array( | ||||
| 						$node->previousSibling->nodeName, | ||||
| 						$options['keep_whitespace_around'] | ||||
| 					))) { | ||||
| 						$node->nodeValue = ltrim($node->nodeValue); | ||||
| 					} | ||||
| 
 | ||||
| 					if (!($node->nextSibling && in_array( | ||||
| 						$node->nextSibling->nodeName, | ||||
| 						$options['keep_whitespace_around'] | ||||
| 					))) { | ||||
| 						$node->nodeValue = rtrim($node->nodeValue); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				if ((strlen($node->nodeValue) == 0)) { | ||||
| 					$node->parentNode->removeChild($node); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			$contents = $doc->saveHTML(); | ||||
| 
 | ||||
| 			Logger::log("Minified template from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template"); | ||||
| 		} | ||||
| 
 | ||||
| 		file_put_contents(PUBLIC_DIR . "/" . $filename, $contents); | ||||
| 	} | ||||
| 
 | ||||
| 	public function saveCss(string $filename, string $contents, bool $minify = true) | ||||
| 	{ | ||||
| 		Logger::log("Saving css to " . $filename, Logger::IOWRITE, "Template"); | ||||
| 
 | ||||
| 		if ($minify) { | ||||
| 			$size = strlen($contents); | ||||
| 
 | ||||
| 			$contents = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $contents); | ||||
| 			$contents = str_replace(': ', ':', $contents); | ||||
| 			$contents = str_replace(array("\r\n", "\r", "\n", "\t", '  ', '    ', '    '), '', $contents); | ||||
| 
 | ||||
| 			Logger::log("Minified " . $filename . " from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template"); | ||||
| 		} | ||||
| 
 | ||||
| 		file_put_contents(PUBLIC_DIR . "/" . $filename, $contents); | ||||
| 	} | ||||
| 
 | ||||
| 	public function saveScript(string $filename, string $contents, bool $minify = true) | ||||
| 	{ | ||||
| 		Logger::log("Saving script to " . $filename, Logger::IOWRITE, "Template"); | ||||
| 
 | ||||
| 		if ($minify) { | ||||
| 			$size = strlen($contents); | ||||
| 
 | ||||
| 			$contents = preg_replace('/([-\+])\s+\+([^\s;]*)/', '$1 (+$2)', $contents); | ||||
| 			$contents = preg_replace("/\s*\n\s*/", "\n", $contents); | ||||
| 			$contents = preg_replace("/\h+/", " ", $contents); | ||||
| 			$contents = preg_replace("/\h([^A-Za-z0-9\_\$])/", '$1', $contents); | ||||
| 			$contents = preg_replace("/([^A-Za-z0-9\_\$])\h/", '$1', $contents); | ||||
| 			$contents = preg_replace("/\s?([\(\[{])\s?/", '$1', $contents); | ||||
| 			$contents = preg_replace("/\s([\)\]}])/", '$1', $contents); | ||||
| 			$contents = preg_replace("/\s?([\.=:\-+,])\s?/", '$1', $contents); | ||||
| 			$contents = preg_replace("/;\n/", ";", $contents); | ||||
| 			$contents = preg_replace('/;}/', '}', $contents); | ||||
| 			$contents = preg_replace('/\s+/', ' ', $contents); | ||||
| 			$contents = preg_replace('/\s*([;{}])\s*/', '$1', $contents); | ||||
| 
 | ||||
| 			$contents = preg_replace('/\s*(;{})\s*/', '$1', $contents); | ||||
| 
 | ||||
| 			$contents = str_replace(array("\r\n", "\r", "\n", "\t", '  ', '    ', '    '), '', $contents); | ||||
| 
 | ||||
| 			Logger::log("Minified " . $filename . " from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template"); | ||||
| 		} | ||||
| 
 | ||||
| 		file_put_contents(PUBLIC_DIR . "/" . $filename, $contents); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										30
									
								
								schema/_schema.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								schema/_schema.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| CREATE TABLE IF NOT EXISTS `migrations` ( | ||||
| 	`id` INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
| 	`table` TEXT NOT NULL, | ||||
| 	`version` INTEGER NOT NULL, | ||||
| 	`schemafile` TEXT NOT NULL, | ||||
| 	`created` TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE INDEX IF NOT EXISTS `migrations_table` ON `migrations` (`table`, `version`); | ||||
| CREATE INDEX IF NOT EXISTS `migrations_created` ON `migrations` (`created`); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS `logs` ( | ||||
| 	`id` INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
| 	`level` TEXT NOT NULL, | ||||
| 	`message` TEXT NOT NULL, | ||||
| 	`context` TEXT, | ||||
| 	`created` TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE INDEX IF NOT EXISTS `logs_level` ON `logs` (`level`, `created`); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS `keyvalue` ( | ||||
| 	`id` INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
| 	`key` TEXT NOT NULL, | ||||
| 	`channel` TEXT DEFAULT NULL, | ||||
| 	`value` TEXT NOT NULL, | ||||
| 	`created` TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE UNIQUE INDEX IF NOT EXISTS `keyvalue_key` ON `keyvalue` (`key`, `channel`); | ||||
							
								
								
									
										0
									
								
								schema/logs-000.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								schema/logs-000.sql
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								schema/migrations-000.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								schema/migrations-000.sql
									
										
									
									
									
										Normal file
									
								
							
		Loading…
	
	Add table
		
		Reference in a new issue