diff --git a/lib/Artwork.php b/lib/Artwork.php
new file mode 100644
index 00000000..31530e8f
--- /dev/null
+++ b/lib/Artwork.php
@@ -0,0 +1,7 @@
+
+
+class Artwork extends PropertiesBase{
+ public $ArtworkId;
+ public $Title;
+ protected $DisplayTitle = null;
+}
diff --git a/lib/Collection.php b/lib/Collection.php
index 259847bf..8d047dbb 100644
--- a/lib/Collection.php
+++ b/lib/Collection.php
@@ -1,4 +1,5 @@
+
class Collection{
public $Name;
public $Url;
diff --git a/lib/Constants.php b/lib/Constants.php
index 93529807..2e941567 100644
--- a/lib/Constants.php
+++ b/lib/Constants.php
@@ -3,6 +3,13 @@
use function Safe\define;
use function Safe\strtotime;
+const SITE_STATUS_LIVE = 'live';
+const SITE_STATUS_DEV = 'dev';
+define('SITE_STATUS', getenv('SITE_STATUS') ?: SITE_STATUS_DEV); // Set in the PHP FPM pool configuration. Have to use define() and not const so we can use a function.
+
+const DATABASE_DEFAULT_DATABASE = 'se';
+const DATABASE_DEFAULT_HOST = 'localhost';
+
const EBOOKS_PER_PAGE = 12;
const SORT_NEWEST = 'newest';
const SORT_AUTHOR_ALPHA = 'author-alpha';
diff --git a/lib/Core.php b/lib/Core.php
index f83331d1..eb8e2b06 100644
--- a/lib/Core.php
+++ b/lib/Core.php
@@ -24,3 +24,5 @@ set_exception_handler(function(Throwable $ex): void{
throw $ex; // Send the exception back to PHP for its usual logging routine.
});
+
+$GLOBALS['DbConnection'] = new DbConnection(DATABASE_DEFAULT_DATABASE, DATABASE_DEFAULT_HOST);
diff --git a/lib/Db.php b/lib/Db.php
new file mode 100644
index 00000000..6be4db21
--- /dev/null
+++ b/lib/Db.php
@@ -0,0 +1,15 @@
+
+
+class Db{
+ public static function GetLastInsertedId(){
+ return $GLOBALS['DbConnection']->GetLastInsertedId();
+ }
+
+ public static function Query(string $query, $args = []){
+ if(!is_array($args)){
+ $args = [$args];
+ }
+
+ return $GLOBALS['DbConnection']->Query($query, $args);
+ }
+}
diff --git a/lib/DbConnection.php b/lib/DbConnection.php
new file mode 100644
index 00000000..ee040bbc
--- /dev/null
+++ b/lib/DbConnection.php
@@ -0,0 +1,183 @@
+
+use function Safe\preg_match;
+
+class DbConnection{
+ private $_link = null;
+ public $IsConnected = false;
+ public $QueryCount = 0;
+
+ public function __construct(?string $defaultDatabase = null, string $host = 'localhost', ?string $user = null, string$password = '', bool $forceUtf8 = true, bool $require = true){
+ if($user === null){
+ // Get the user running the script for local socket login
+ $user = posix_getpwuid(posix_geteuid())['name'];
+ }
+
+ $connectionString = 'mysql:';
+
+ try{
+ if(stripos($host, ':') !== false){
+ $port = null;
+ preg_match('/([^:]*):([0-9]+)/ius', $host, $matches);
+ $host = $matches[1];
+ if(sizeof($matches) > 2){
+ $port = $matches[2];
+ }
+
+ $connectionString .= 'host=' . $host;
+
+ if($port !== null){
+ $connectionString .= ';port=' . $port;
+ }
+ }
+ else{
+ $connectionString .= 'host=' . $host;
+ }
+
+ if($defaultDatabase !== null){
+ $connectionString .= ';dbname=' . $defaultDatabase;
+ }
+
+ // Have to use ATTR_EMULATE_PREPARES = true, otherwise transactions don't work for some reason.
+ $params = [\PDO::ATTR_EMULATE_PREPARES => true, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_PERSISTENT => false];
+
+ if($forceUtf8){
+ $params[\PDO::MYSQL_ATTR_INIT_COMMAND] = 'set names utf8mb4 collate utf8mb4_unicode_ci;';
+ }
+
+ // We can't use persistent connections (connection pooling) because we would have race condition problems with last_insert_id()
+ $this->_link = new \PDO($connectionString, $user, $password, $params);
+
+ $this->IsConnected = true;
+ }
+ catch(Exception $ex){
+ if(SITE_STATUS == SITE_STATUS_DEV){
+ var_dump($ex);
+ }
+ else{
+ Logger::WriteErrorLogEntry('Error connecting to ' . $connectionString . '. Exception: ' . vds($ex));
+ }
+
+ if($require){
+ print("Something crazy happened in our database. Drop us a line and let us know exactly what you were doing and we'll take a look.\n
The more detail you can provide, the better!");
+ exit();
+ }
+ }
+ }
+
+ // Inputs: string $sql = the SQL query to execute
+ // array $params = an array of parameters to bind to the SQL statement
+ // Returns: a resource record or null on error
+ public function Query(string $sql, array $params = []){
+ if(!$this->IsConnected){
+ return;
+ }
+
+ $this->QueryCount++;
+ $result = [];
+ $preparedSql = $sql;
+
+ $handle = $this->_link->prepare($preparedSql);
+ if(!is_array($params)){
+ $params = [$params];
+ }
+
+ $name = 0;
+ foreach($params as $parameter){
+ $name++;
+
+ if(is_a($parameter, 'DateTime')){
+ $parameter = $parameter->format('Y-m-d H:i:s');
+ }
+
+ // MySQL strict mode requires 0 or 1 instead of true or false
+ // Can't use PDO::PARAM_BOOL, it just doesn't work
+ if(is_bool($parameter)){
+ if($parameter){
+ $parameter = 1;
+ }
+ else{
+ $parameter = 0;
+ }
+ }
+
+ if(is_int($parameter)){
+ $handle->bindValue($name, $parameter, PDO::PARAM_INT);
+ }
+ else{
+ $handle->bindValue($name, $parameter);
+ }
+ }
+
+ $deadlockRetries = 0;
+ $done = false;
+ while(!$done){
+ try{
+ $result = $this->ExecuteQuery($handle, $preparedSql, $params);
+ $done = true;
+ }
+ catch(\PDOException $ex){
+ if($ex->errorInfo[1] == 1213 && $deadlockRetries < 3){ // InnoDB deadlock, this is normal and happens occasionally. All we have to do is retry the query.
+ $deadlockRetries++;
+
+ usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds
+ }
+ elseif(stripos($ex->getMessage(), '1064 offset out of bounds') !== false){
+ $done = true;
+ // We reach here if Sphinx tries to get a record past its page limit. Just silently do nothing.
+ }
+ else{
+ $done = true;
+ if(SITE_STATUS == SITE_STATUS_DEV){
+ print($sql);
+ throw($ex);
+ }
+ else{
+ Logger::WriteErrorLogEntry($ex->getMessage());
+ Logger::WriteErrorLogEntry($preparedSql);
+ Logger::WriteErrorLogEntry(vds($params));
+ }
+ }
+ }
+ }
+
+ // If only one rowset is returned, change the result object
+ if(sizeof($result) == 1){
+ $result = $result[0];
+ }
+
+ return $result;
+ }
+
+ private function ExecuteQuery($handle, string $preparedSql, array $params){
+ $handle->execute();
+
+ $result = [];
+ do{
+ try{
+ if($handle->columnCount() > 0){
+ $result[] = $handle->fetchAll(\PDO::FETCH_OBJ);
+ }
+ }
+ catch(\PDOException $ex){
+ // HY000 is thrown when there is no result set, e.g. for an update operation.
+ // If anything besides that is thrown, then send it up the stack
+ if($ex->errorInfo[0] != "HY000"){
+ throw $ex;
+ }
+ }
+ }while($handle->nextRowset());
+
+ return $result;
+ }
+
+ // Gets the last AUTO-INCREMENT id
+ public function GetLastInsertedId(){
+ $id = $this->_link->lastInsertId();
+
+ if($id == 0){
+ return null;
+ }
+
+ return $id;
+ }
+}
diff --git a/lib/OrmBase.php b/lib/OrmBase.php
new file mode 100644
index 00000000..b7043264
--- /dev/null
+++ b/lib/OrmBase.php
@@ -0,0 +1,30 @@
+
+use function Safe\substr;
+
+abstract class OrmBase{
+ public static function FillObject($object, $row){
+ foreach($row as $property => $value){
+ if(substr($property, strlen($property) - 9) == 'Timestamp'){
+ if($value !== null){
+ $object->$property = new DateTime($value, new DateTimeZone('UTC'));
+ }
+ else{
+ $object->$property = null;
+ }
+ }
+ elseif(substr($property, strlen($property) - 5) == 'Cache'){
+ $property = substr($property, 0, strlen($property) - 5);
+ $object->$property = $value;
+ }
+ else{
+ $object->$property = $value;
+ }
+ }
+
+ return $object;
+ }
+
+ public static function FromRow($row){
+ return self::FillObject(new static(), $row);
+ }
+}
diff --git a/lib/PropertiesBase.php b/lib/PropertiesBase.php
new file mode 100644
index 00000000..5ad0b39d
--- /dev/null
+++ b/lib/PropertiesBase.php
@@ -0,0 +1,33 @@
+
+
+abstract class PropertiesBase extends OrmBase{
+ public function __get($var){
+ $function = 'Get' . $var;
+
+ if(method_exists($this, $function)){
+ return $this->$function();
+ }
+ elseif(substr($var, 0, 7) == 'Display'){
+ // If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML.
+ if($this->$var === null){
+ $target = substr($var, 7, strlen($var));
+ $this->$var = Formatter::ToPlainText($this->$target);
+ }
+
+ return $this->$var;
+ }
+ else{
+ return $this->$var;
+ }
+ }
+
+ public function __set($var, $val){
+ $function = 'Set' . $var;
+ if(method_exists($this, $function)){
+ $this->$function($val);
+ }
+ else{
+ $this->$var = $val;
+ }
+ }
+}