ef9040870a
That is, albums can now contain other albums, which are shown at the top of the album view. This required some changes to album.js and the contextMenu.js, because this view contains now both photos and albums. The contextMenu on this view has been kept simple by requiring the user to select either only albums or only photos, but not a mixture of both. This feature required a database change, so that the version has been updated to 3.1.3. At the moment, album and photo operations (make public, download, delete, merge) are still "flat", i.e. don't respect the album hierarchy.
422 lines
11 KiB
PHP
Executable File
422 lines
11 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Lychee\Modules;
|
|
|
|
use Mysqli;
|
|
|
|
final class Database {
|
|
|
|
private $connection = null;
|
|
private static $instance = null;
|
|
|
|
private static $versions = array(
|
|
'020700', // 2.7.0
|
|
'030000', // 3.0.0
|
|
'030001', // 3.0.1
|
|
'030003', // 3.0.3
|
|
'030100', // 3.1.0
|
|
'030102', // 3.1.2
|
|
'030103' // 3.1.3
|
|
);
|
|
|
|
/**
|
|
* @return object Returns a new or cached connection.
|
|
*/
|
|
public static function get() {
|
|
|
|
if (!self::$instance) {
|
|
|
|
$credentials = Config::get();
|
|
|
|
self::$instance = new self(
|
|
$credentials['host'],
|
|
$credentials['user'],
|
|
$credentials['password'],
|
|
$credentials['name'],
|
|
$credentials['prefix']
|
|
);
|
|
|
|
}
|
|
|
|
return self::$instance->connection;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exits on error.
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
private function __construct($host, $user, $password, $name = 'lychee', $dbTablePrefix) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($host, $user, $password, $name), __METHOD__);
|
|
|
|
// Define the table prefix
|
|
defineTablePrefix($dbTablePrefix);
|
|
|
|
// Open a new connection to the MySQL server
|
|
$connection = self::connect($host, $user, $password);
|
|
|
|
// Check if the connection was successful
|
|
if ($connection===false) Response::error(self::connect_error());
|
|
|
|
if (self::setCharset($connection)===false) Response::error('Could not set database charset!');
|
|
|
|
// Create database
|
|
if (self::createDatabase($connection, $name)===false) Response::error('Could not create database!');
|
|
|
|
// Create tables
|
|
if (self::createTables($connection)===false) Response::error('Could not create tables!');
|
|
|
|
// Update database
|
|
if (self::update($connection, $name)===false) Response::error('Could not update database and tables!');
|
|
|
|
$this->connection = $connection;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return object|false Returns the connection when successful.
|
|
*/
|
|
public static function connect($host = 'localhost', $user, $password) {
|
|
|
|
// Open a new connection to the MySQL server
|
|
$connection = @new Mysqli($host, $user, $password);
|
|
|
|
// Check if the connection was successful
|
|
if ($connection->connect_errno) return false;
|
|
|
|
return $connection;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return string Returns the string description of the last connect error
|
|
*/
|
|
private static function connect_error() {
|
|
|
|
return mysqli_connect_error();
|
|
|
|
}
|
|
|
|
/**
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
private static function setCharset($connection) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection), __METHOD__);
|
|
|
|
// Avoid sql injection on older MySQL versions by using GBK
|
|
if ($connection->server_version<50500) @$connection->set_charset('GBK');
|
|
else @$connection->set_charset('utf8');
|
|
|
|
// Set unicode
|
|
$query = 'SET NAMES utf8';
|
|
$result = self::execute($connection, $query, null, null);
|
|
|
|
if ($result===false) return false;
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
public static function createDatabase($connection, $name = 'lychee') {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection), __METHOD__);
|
|
|
|
// Check if database exists
|
|
if ($connection->select_db($name)===true) return true;
|
|
|
|
// Create database
|
|
$query = self::prepare($connection, 'CREATE DATABASE IF NOT EXISTS ?', array($name));
|
|
$result = self::execute($connection, $query, null, null);
|
|
|
|
if ($result===false) return false;
|
|
if ($connection->select_db($name)===false) return false;
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
private static function createTables($connection) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection), __METHOD__);
|
|
|
|
// Check if tables exist
|
|
$query = self::prepare($connection, 'SELECT * FROM ?, ?, ?, ? LIMIT 0', array(LYCHEE_TABLE_PHOTOS, LYCHEE_TABLE_ALBUMS, LYCHEE_TABLE_SETTINGS, LYCHEE_TABLE_LOG));
|
|
$result = self::execute($connection, $query, null, null);
|
|
if ($result!==false) return true;
|
|
|
|
// Check if log table exists
|
|
$exist = self::prepare($connection, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_LOG));
|
|
$result = self::execute($connection, $exist, null, null);
|
|
|
|
if ($result===false) {
|
|
|
|
// Read file
|
|
$file = __DIR__ . '/../database/log_table.sql';
|
|
$query = @file_get_contents($file);
|
|
|
|
if ($query===false) return false;
|
|
|
|
// Create table
|
|
$query = self::prepare($connection, $query, array(LYCHEE_TABLE_LOG));
|
|
$result = self::execute($connection, $query, null, null);
|
|
|
|
if ($result===false) return false;
|
|
|
|
}
|
|
|
|
// Check if settings table exists
|
|
$exist = self::prepare($connection, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_SETTINGS));
|
|
$result = self::execute($connection, $exist, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) {
|
|
|
|
// Read file
|
|
$file = __DIR__ . '/../database/settings_table.sql';
|
|
$query = @file_get_contents($file);
|
|
|
|
if ($query===false) {
|
|
Log::error($connection, __METHOD__, __LINE__, 'Could not load query for lychee_settings');
|
|
return false;
|
|
}
|
|
|
|
// Create table
|
|
$query = self::prepare($connection, $query, array(LYCHEE_TABLE_SETTINGS));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
// Read file
|
|
$file = __DIR__ . '/../database/settings_content.sql';
|
|
$query = @file_get_contents($file);
|
|
|
|
if ($query===false) {
|
|
Log::error($connection, __METHOD__, __LINE__, 'Could not load content-query for lychee_settings');
|
|
return false;
|
|
}
|
|
|
|
// Add content
|
|
$query = self::prepare($connection, $query, array(LYCHEE_TABLE_SETTINGS));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
// Generate identifier
|
|
$identifier = md5(microtime(true));
|
|
$query = self::prepare($connection, "UPDATE `?` SET `value` = '?' WHERE `key` = 'identifier' LIMIT 1", array(LYCHEE_TABLE_SETTINGS, $identifier));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
}
|
|
|
|
// Check if albums table exists
|
|
$exist = self::prepare($connection, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_ALBUMS));
|
|
$result = self::execute($connection, $exist, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) {
|
|
|
|
// Read file
|
|
$file = __DIR__ . '/../database/albums_table.sql';
|
|
$query = @file_get_contents($file);
|
|
|
|
if ($query===false) {
|
|
Log::error($connection, __METHOD__, __LINE__, 'Could not load query for lychee_albums');
|
|
return false;
|
|
}
|
|
|
|
// Create table
|
|
$query = self::prepare($connection, $query, array(LYCHEE_TABLE_ALBUMS));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
}
|
|
|
|
// Check if photos table exists
|
|
$exist = self::prepare($connection, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_PHOTOS));
|
|
$result = self::execute($connection, $exist, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) {
|
|
|
|
// Read file
|
|
$file = __DIR__ . '/../database/photos_table.sql';
|
|
$query = @file_get_contents($file);
|
|
|
|
if ($query===false) {
|
|
Log::error($connection, __METHOD__, __LINE__, 'Could not load query for lychee_photos');
|
|
return false;
|
|
}
|
|
|
|
// Create table
|
|
$query = self::prepare($connection, $query, array(LYCHEE_TABLE_PHOTOS));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exits when an update fails.
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
private static function update($connection, $dbName) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection, $dbName), __METHOD__);
|
|
|
|
// Get current version
|
|
$query = self::prepare($connection, "SELECT * FROM ? WHERE `key` = 'version'", array(LYCHEE_TABLE_SETTINGS));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
|
|
// Extract current version
|
|
$current = $result->fetch_object()->value;
|
|
|
|
// For each update
|
|
foreach (self::$versions as $version) {
|
|
|
|
// Only update when newer version available
|
|
if ($version<=$current) continue;
|
|
|
|
// Load update
|
|
include(__DIR__ . '/../database/update_' . $version . '.php');
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return boolean Returns true when successful.
|
|
*/
|
|
public static function setVersion($connection, $version) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection), __METHOD__);
|
|
|
|
$query = self::prepare($connection, "UPDATE ? SET value = '?' WHERE `key` = 'version'", array(LYCHEE_TABLE_SETTINGS, $version));
|
|
$result = self::execute($connection, $query, __METHOD__, __LINE__);
|
|
|
|
if ($result===false) return false;
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return string Returns a escaped query.
|
|
*/
|
|
public static function prepare($connection, $query, array $data) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection, $query), __METHOD__);
|
|
|
|
// Count the number of placeholders and compare it with the number of arguments
|
|
// If it doesn't match, calculate the difference and skip this number of placeholders before starting the replacement
|
|
// This avoids problems with placeholders in user-input
|
|
// $skip = Number of placeholders which need to be skipped
|
|
$skip = 0;
|
|
$temp = '';
|
|
$num = array(
|
|
'placeholder' => substr_count($query, '?'),
|
|
'data' => count($data)
|
|
);
|
|
|
|
if (($num['data']-$num['placeholder'])<0) Log::notice($connection, __METHOD__, __LINE__, 'Could not completely prepare query. Query has more placeholders than values.');
|
|
|
|
foreach ($data as $value) {
|
|
|
|
// Escape
|
|
$value = mysqli_real_escape_string($connection, $value);
|
|
|
|
// Recalculate number of placeholders
|
|
$num['placeholder'] = substr_count($query, '?');
|
|
|
|
// Calculate number of skips
|
|
if ($num['placeholder']>$num['data']) $skip = $num['placeholder'] - $num['data'];
|
|
|
|
if ($skip>0) {
|
|
|
|
// Need to skip $skip placeholders, because the user input contained placeholders
|
|
// Calculate a substring which does not contain the user placeholders
|
|
// 1 or -1 is the length of the placeholder (placeholder = ?)
|
|
|
|
$pos = -1;
|
|
for ($i=$skip; $i>0; $i--) $pos = strpos($query, '?', $pos + 1);
|
|
$pos++;
|
|
|
|
$temp = substr($query, 0, $pos); // First part of $query
|
|
$query = substr($query, $pos); // Last part of $query
|
|
|
|
}
|
|
|
|
// Put a backslash in front of every character that is part of the regular
|
|
// expression syntax. Avoids a backreference when using preg_replace.
|
|
$value = preg_quote($value);
|
|
|
|
// Replace
|
|
$query = preg_replace('/\?/', $value, $query, 1);
|
|
|
|
if ($skip>0) {
|
|
|
|
// Reassemble the parts of $query
|
|
$query = $temp . $query;
|
|
|
|
}
|
|
|
|
// Reset skip
|
|
$skip = 0;
|
|
|
|
// Decrease number of data elements
|
|
$num['data']--;
|
|
|
|
}
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
/**
|
|
* @return object|false Returns the results on success.
|
|
*/
|
|
public static function execute($connection, $query, $function, $line) {
|
|
|
|
// Check dependencies
|
|
Validator::required(isset($connection, $query), __METHOD__);
|
|
|
|
// Only activate logging when $function and $line is set
|
|
$logging = ($function===null||$line===null ? false : true);
|
|
|
|
// Execute query
|
|
$result = $connection->query($query);
|
|
|
|
// Check if execution failed
|
|
if ($result===false) {
|
|
if ($logging===true) Log::error($connection, $function, $line, $connection->error);
|
|
return false;
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
?>
|