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