Compare commits
1 Commits
555a3dbb95
...
4412db1679
| Author | SHA1 | Date | |
|---|---|---|---|
| 4412db1679 |
8
app.php
8
app.php
@@ -14,13 +14,7 @@ require_once 'vendor/autoload.php';
|
||||
|
||||
// Initialise the database.
|
||||
Registry::set('start', microtime(true));
|
||||
if (defined('DB_DRIVER') && DB_DRIVER === 'sqlite')
|
||||
Registry::set('db', new Database('sqlite', ['file' => DB_FILE]));
|
||||
else
|
||||
Registry::set('db', new Database('mysql', [
|
||||
'host' => DB_SERVER, 'user' => DB_USER,
|
||||
'password' => DB_PASS, 'name' => DB_NAME,
|
||||
]));
|
||||
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
||||
|
||||
// Handle errors our own way.
|
||||
ErrorHandler::enable();
|
||||
|
||||
@@ -40,4 +40,3 @@ const OIDC_PROVIDER_URL = ''; // e.g. 'https://kanidm.example.com/oauth2/op
|
||||
const OIDC_CLIENT_ID = '';
|
||||
const OIDC_CLIENT_SECRET = '';
|
||||
const OIDC_PROVIDER_NAME = ''; // e.g. 'Kanidm' — used as button label
|
||||
const OIDC_ADMIN_GROUP = ''; // OIDC group claim value that grants admin, e.g. 'pics_admins'
|
||||
|
||||
@@ -206,7 +206,7 @@ class EditAlbum extends HTMLController
|
||||
$data = $this->form->getData();
|
||||
|
||||
// Sanity check: don't let an album be its own parent
|
||||
if ($id_tag && $data['id_parent'] == $id_tag)
|
||||
if ($data['id_parent'] == $id_tag)
|
||||
{
|
||||
return $this->formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
|
||||
}
|
||||
|
||||
@@ -109,14 +109,6 @@ class EditUser extends HTMLController
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
'oidc_sub' => [
|
||||
'header' => 'OIDC',
|
||||
'type' => 'text',
|
||||
'label' => 'OIDC subject identifier',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
'is_admin' => [
|
||||
'header' => 'Privileges',
|
||||
'type' => 'checkbox',
|
||||
@@ -153,10 +145,6 @@ class EditUser extends HTMLController
|
||||
// Quick stripping.
|
||||
$data['slug'] = strtr(strtolower($data['slug']), [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
|
||||
|
||||
// Normalise empty OIDC sub to null (unique constraint).
|
||||
if (empty($data['oidc_sub']))
|
||||
$data['oidc_sub'] = null;
|
||||
|
||||
// Checkboxes, fun!
|
||||
$data['is_admin'] = empty($data['is_admin']) ? 0 : 1;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class OIDCLogin
|
||||
|
||||
$oidc = new OpenIDConnectClient(OIDC_PROVIDER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET);
|
||||
$oidc->setRedirectURL(BASEURL . '/oidclogin/');
|
||||
$oidc->addScope(['openid', 'email', 'profile', 'groups']);
|
||||
$oidc->addScope(['openid', 'email']);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -42,62 +42,22 @@ class OIDCLogin
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the stable subject identifier from the ID token.
|
||||
$sub = $oidc->getVerifiedClaims('sub');
|
||||
if (empty($sub))
|
||||
$email = $oidc->requestUserInfo('email');
|
||||
if (empty($email))
|
||||
{
|
||||
$_SESSION['login_msg'] = ['', 'No subject identifier received from OIDC provider.', 'danger'];
|
||||
$_SESSION['login_msg'] = ['', 'No email address received from OIDC provider.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Step 1: Look up user by oidc_sub.
|
||||
$user = Member::fromOidcSub($sub);
|
||||
|
||||
// Step 2: One-time email migration — link existing account by email.
|
||||
$user = Member::fromEmailAddress($email);
|
||||
if ($user === null || $user === false)
|
||||
{
|
||||
$email = $oidc->requestUserInfo('email');
|
||||
if (!empty($email))
|
||||
{
|
||||
$user = Member::fromEmailAddress($email);
|
||||
if ($user !== null && $user !== false)
|
||||
$user->update(['oidc_sub' => $sub]);
|
||||
}
|
||||
$_SESSION['login_msg'] = ['', 'No account found for this email address. Please contact an administrator.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Step 3: Auto-enroll — create a new user from OIDC claims.
|
||||
if ($user === null || $user === false)
|
||||
{
|
||||
$first_name = $oidc->requestUserInfo('given_name') ?: '';
|
||||
$last_name = $oidc->requestUserInfo('family_name') ?: '';
|
||||
$email = $oidc->requestUserInfo('email') ?: $sub . '@oidc.placeholder';
|
||||
|
||||
$slug = $this->generateSlug($first_name, $last_name);
|
||||
|
||||
$user = Member::createNew([
|
||||
'first_name' => $first_name ?: 'OIDC',
|
||||
'surname' => $last_name ?: 'User',
|
||||
'slug' => $slug,
|
||||
'emailaddress' => $email,
|
||||
'oidc_sub' => $sub,
|
||||
]);
|
||||
|
||||
if ($user === false)
|
||||
{
|
||||
$_SESSION['login_msg'] = ['', 'Failed to create account. Please contact an administrator.', 'danger'];
|
||||
header('Location: ' . BASEURL . '/login/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Sync admin status from groups claim.
|
||||
$groups = $oidc->requestUserInfo('groups');
|
||||
$should_be_admin = is_array($groups) && in_array(OIDC_ADMIN_GROUP, $groups);
|
||||
if ($should_be_admin !== $user->isAdmin())
|
||||
$user->update(['is_admin' => $should_be_admin ? 1 : 0]);
|
||||
|
||||
// Set session and redirect.
|
||||
$_SESSION['user_id'] = $user->getUserId();
|
||||
|
||||
if (!empty($_SESSION['oidc_redirect_url']))
|
||||
@@ -117,36 +77,4 @@ class OIDCLogin
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
private function generateSlug($first_name, $last_name)
|
||||
{
|
||||
$base = trim($first_name . ' ' . $last_name);
|
||||
if (empty($base))
|
||||
$base = 'user';
|
||||
|
||||
$slug = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $base));
|
||||
$slug = trim($slug, '-');
|
||||
if (empty($slug))
|
||||
$slug = 'user';
|
||||
|
||||
// Check if slug is available.
|
||||
$candidate = $slug;
|
||||
for ($i = 0; $i < 10; $i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Member::fromSlug($candidate);
|
||||
// Slug taken — append random suffix.
|
||||
$candidate = $slug . '-' . bin2hex(random_bytes(3));
|
||||
}
|
||||
catch (NotFoundException $e)
|
||||
{
|
||||
// Slug is available.
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use sub-based slug.
|
||||
return 'user-' . bin2hex(random_bytes(4));
|
||||
}
|
||||
}
|
||||
|
||||
27
flake.lock
generated
27
flake.lock
generated
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770843696,
|
||||
"narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
47
flake.nix
47
flake.nix
@@ -1,47 +0,0 @@
|
||||
{
|
||||
description = "HashRU Pics dev environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs [
|
||||
"x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"
|
||||
] (system: f nixpkgs.legacyPackages.${system});
|
||||
|
||||
php = pkgs: pkgs.php.buildEnv {
|
||||
extensions = { enabled, all }: enabled ++ (with all; [
|
||||
imagick
|
||||
pdo_mysql
|
||||
pdo_sqlite
|
||||
]);
|
||||
extraConfig = ''
|
||||
memory_limit = 256M
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 50M
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
(php pkgs)
|
||||
(php pkgs).packages.composer
|
||||
pkgs.sqlite
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export COMPOSER_HOME="$PWD/.composer"
|
||||
|
||||
if [ ! -d vendor ]; then
|
||||
echo "Running composer install..."
|
||||
composer install
|
||||
fi
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE users ADD COLUMN oidc_sub TEXT;
|
||||
CREATE UNIQUE INDEX idx_users_oidc_sub ON users (oidc_sub);
|
||||
@@ -10,6 +10,7 @@ class AssetIterator implements Iterator
|
||||
{
|
||||
private $direction;
|
||||
private $return_format;
|
||||
private $rowCount;
|
||||
|
||||
private $assets_iterator;
|
||||
private $meta_iterator;
|
||||
@@ -20,6 +21,7 @@ class AssetIterator implements Iterator
|
||||
{
|
||||
$this->direction = $direction;
|
||||
$this->return_format = $return_format;
|
||||
$this->rowCount = $stmt_assets->rowCount();
|
||||
|
||||
$this->assets_iterator = new CachedPDOIterator($stmt_assets);
|
||||
$this->assets_iterator->rewind();
|
||||
@@ -207,7 +209,7 @@ class AssetIterator implements Iterator
|
||||
|
||||
public function num(): int
|
||||
{
|
||||
return count($this->assets_iterator);
|
||||
return $this->rowCount;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
|
||||
@@ -9,35 +9,18 @@
|
||||
class Database
|
||||
{
|
||||
private $connection;
|
||||
private $driver;
|
||||
private $query_count = 0;
|
||||
private $logged_queries = [];
|
||||
|
||||
public function __construct($driver, array $options)
|
||||
public function __construct($host, $user, $password, $name)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
|
||||
try
|
||||
{
|
||||
if ($driver === 'sqlite')
|
||||
{
|
||||
$this->connection = new PDO("sqlite:" . $options['file'], null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$this->connection->exec('PRAGMA journal_mode=WAL');
|
||||
$this->registerSQLiteFunctions();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->connection = new PDO(
|
||||
"mysql:host={$options['host']};dbname={$options['name']};charset=utf8mb4",
|
||||
$options['user'], $options['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
}
|
||||
$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
}
|
||||
// Give up if we have a connection error.
|
||||
catch (PDOException $e)
|
||||
@@ -48,54 +31,6 @@ class Database
|
||||
}
|
||||
}
|
||||
|
||||
private function registerSQLiteFunctions()
|
||||
{
|
||||
$pdo = $this->connection;
|
||||
|
||||
$pdo->sqliteCreateFunction('CONCAT', function () {
|
||||
return implode('', func_get_args());
|
||||
}, -1);
|
||||
|
||||
$pdo->sqliteCreateFunction('IF', function ($cond, $t, $f) {
|
||||
return $cond ? $t : $f;
|
||||
}, 3);
|
||||
|
||||
$pdo->sqliteCreateFunction('FROM_UNIXTIME', function ($ts) {
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}, 1);
|
||||
|
||||
$pdo->sqliteCreateFunction('UNIX_TIMESTAMP', function () {
|
||||
return time();
|
||||
}, 0);
|
||||
|
||||
$pdo->sqliteCreateFunction('CURRENT_TIMESTAMP', function () {
|
||||
return date('Y-m-d H:i:s');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public function getDriver()
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
private function rewriteForSQLite($sql)
|
||||
{
|
||||
// REPLACE INTO → INSERT OR REPLACE INTO
|
||||
$sql = preg_replace('/\bREPLACE\s+INTO\b/i', 'INSERT OR REPLACE INTO', $sql);
|
||||
|
||||
// INSERT IGNORE INTO → INSERT OR IGNORE INTO
|
||||
$sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT OR IGNORE INTO', $sql);
|
||||
|
||||
// LIMIT :offset, :limit → LIMIT :limit OFFSET :offset
|
||||
$sql = preg_replace(
|
||||
'/\bLIMIT\s+:offset\s*,\s*:limit\b/i',
|
||||
'LIMIT :limit OFFSET :offset',
|
||||
$sql
|
||||
);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getQueryCount()
|
||||
{
|
||||
return $this->query_count;
|
||||
@@ -245,10 +180,6 @@ class Database
|
||||
// Preprocessing/checks: prepare any arrays for binding
|
||||
$db_string = $this->expandPlaceholders($db_string, $db_values);
|
||||
|
||||
// SQLite query rewriting
|
||||
if ($this->driver === 'sqlite')
|
||||
$db_string = $this->rewriteForSQLite($db_string);
|
||||
|
||||
// Prepare query for execution
|
||||
$statement = $this->connection->prepare($db_string);
|
||||
|
||||
@@ -292,10 +223,13 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return null;
|
||||
|
||||
$object = $this->fetchObject($res, $class);
|
||||
$this->free($res);
|
||||
|
||||
return $object ?: null;
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +239,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($object = $this->fetchObject($res, $class))
|
||||
$rows[] = $object;
|
||||
@@ -321,10 +258,13 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$row = $this->fetchNum($res);
|
||||
$this->free($res);
|
||||
|
||||
return $row ?: [];
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,6 +274,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[] = $row;
|
||||
@@ -350,6 +293,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[$row[0]] = $row[1];
|
||||
@@ -366,6 +312,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchAssoc($res))
|
||||
{
|
||||
@@ -385,10 +334,13 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$row = $this->fetchAssoc($res);
|
||||
$this->free($res);
|
||||
|
||||
return $row ?: [];
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,6 +350,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchAssoc($res))
|
||||
$rows[] = $row;
|
||||
@@ -414,13 +369,14 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
$row = $this->fetchNum($res);
|
||||
$this->free($res);
|
||||
|
||||
if (!$row)
|
||||
// If this happens, you're doing it wrong.
|
||||
if ($this->rowCount($res) === 0)
|
||||
return null;
|
||||
|
||||
return $row[0];
|
||||
list($value) = $this->fetchNum($res);
|
||||
$this->free($res);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,6 +386,9 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[] = $row[0];
|
||||
@@ -453,10 +412,7 @@ class Database
|
||||
$data = [$data];
|
||||
|
||||
// Determine the method of insertion.
|
||||
if ($this->driver === 'sqlite')
|
||||
$method = $method == 'replace' ? 'INSERT OR REPLACE' : ($method == 'ignore' ? 'INSERT OR IGNORE' : 'INSERT');
|
||||
else
|
||||
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||
|
||||
// What columns are we inserting?
|
||||
$columns = array_keys($data[0]);
|
||||
|
||||
@@ -90,7 +90,7 @@ class Dispatcher
|
||||
private static function trigger404()
|
||||
{
|
||||
http_response_code(404);
|
||||
self::errorPage('Page not found!', 'The page you requested could not be found.');
|
||||
exit;
|
||||
$page = new ViewErrorPage('Page not found!');
|
||||
$page->showContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class ErrorHandler
|
||||
'debug_info' => $debug_info,
|
||||
'file' => str_replace(BASEDIR, '', $file),
|
||||
'line' => $line,
|
||||
'id_user' => Registry::has('user') && Registry::get('user')->getUserId() ? Registry::get('user')->getUserId() : 0,
|
||||
'id_user' => Registry::has('user') ? Registry::get('user')->getUserId() : 0,
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
|
||||
]))
|
||||
|
||||
@@ -27,15 +27,6 @@ class Member extends User
|
||||
['email_address' => $email_address]);
|
||||
}
|
||||
|
||||
public static function fromOidcSub($sub)
|
||||
{
|
||||
return Registry::get('db')->queryObject(static::class, '
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE oidc_sub = :oidc_sub',
|
||||
['oidc_sub' => $sub]);
|
||||
}
|
||||
|
||||
public static function fromId($id_user)
|
||||
{
|
||||
$row = Registry::get('db')->queryAssoc('
|
||||
@@ -82,27 +73,18 @@ class Member extends User
|
||||
'surname' => !empty($data['surname']) ? $data['surname'] : $error |= true,
|
||||
'slug' => !empty($data['slug']) ? $data['slug'] : $error |= true,
|
||||
'emailaddress' => !empty($data['emailaddress']) ? $data['emailaddress'] : $error |= true,
|
||||
'password_hash' => !empty($data['password']) ? Authentication::computeHash($data['password']) : $error |= true,
|
||||
'creation_time' => time(),
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'is_admin' => empty($data['is_admin']) ? 0 : 1,
|
||||
'reset_key' => '',
|
||||
];
|
||||
|
||||
// Password is required unless oidc_sub is provided.
|
||||
if (!empty($data['password']))
|
||||
$new_user['password_hash'] = Authentication::computeHash($data['password']);
|
||||
elseif (!empty($data['oidc_sub']))
|
||||
$new_user['password_hash'] = '';
|
||||
else
|
||||
$error |= true;
|
||||
|
||||
if (!empty($data['oidc_sub']))
|
||||
$new_user['oidc_sub'] = $data['oidc_sub'];
|
||||
|
||||
if ($error)
|
||||
return false;
|
||||
|
||||
$columns = [
|
||||
$db = Registry::get('db');
|
||||
$bool = $db->insert('insert', 'users', [
|
||||
'first_name' => 'string-30',
|
||||
'surname' => 'string-60',
|
||||
'slug' => 'string-90',
|
||||
@@ -111,14 +93,8 @@ class Member extends User
|
||||
'creation_time' => 'int',
|
||||
'ip_address' => 'string-45',
|
||||
'is_admin' => 'int',
|
||||
'reset_key' => 'string-16',
|
||||
];
|
||||
|
||||
if (isset($new_user['oidc_sub']))
|
||||
$columns['oidc_sub'] = 'string-255';
|
||||
|
||||
$db = Registry::get('db');
|
||||
$bool = $db->insert('insert', 'users', $columns, $new_user, ['id_user']);
|
||||
'reset_key' => 'string-16'
|
||||
], $new_user, ['id_user']);
|
||||
|
||||
if (!$bool)
|
||||
return false;
|
||||
@@ -137,7 +113,7 @@ class Member extends User
|
||||
{
|
||||
foreach ($new_data as $key => $value)
|
||||
{
|
||||
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress', 'oidc_sub']))
|
||||
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress']))
|
||||
$this->$key = $value;
|
||||
elseif ($key === 'password')
|
||||
$this->password_hash = Authentication::computeHash($value);
|
||||
@@ -156,8 +132,7 @@ class Member extends User
|
||||
slug = :slug,
|
||||
emailaddress = :emailaddress,
|
||||
password_hash = :password_hash,
|
||||
is_admin = :is_admin,
|
||||
oidc_sub = :oidc_sub
|
||||
is_admin = :is_admin
|
||||
WHERE id_user = :id_user',
|
||||
get_object_vars($this));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class Router
|
||||
$possibleActions = [
|
||||
'accountsettings' => 'AccountSettings',
|
||||
'addalbum' => 'EditAlbum',
|
||||
'albums' => 'ViewPhotoAlbum',
|
||||
'albums' => 'ViewPhotoAlbums',
|
||||
'editalbum' => 'EditAlbum',
|
||||
'editasset' => 'EditAsset',
|
||||
'edittag' => 'EditTag',
|
||||
@@ -55,7 +55,7 @@ class Router
|
||||
return new GenerateThumbnail();
|
||||
}
|
||||
// Look for particular actions...
|
||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?$~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||
{
|
||||
$_GET = array_merge($_GET, $path);
|
||||
return new $possibleActions[$path['action']]();
|
||||
|
||||
@@ -122,12 +122,10 @@ class Tag
|
||||
|
||||
public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
||||
{
|
||||
$parent_clause = empty($id_parent) ? '(id_parent = :id_parent OR id_parent IS NULL)' : 'id_parent = :id_parent';
|
||||
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE ' . $parent_clause . ' AND kind = :kind
|
||||
WHERE id_parent = :id_parent AND kind = :kind
|
||||
ORDER BY tag ASC
|
||||
LIMIT :offset, :limit',
|
||||
[
|
||||
@@ -165,12 +163,10 @@ class Tag
|
||||
|
||||
public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
||||
{
|
||||
$parent_clause = empty($id_parent) ? '(id_parent = :id_parent OR id_parent IS NULL)' : 'id_parent = :id_parent';
|
||||
|
||||
$rows = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE ' . $parent_clause . ' AND kind = :kind
|
||||
WHERE id_parent = :id_parent AND kind = :kind
|
||||
ORDER BY tag ASC
|
||||
LIMIT :offset, :limit',
|
||||
[
|
||||
@@ -253,21 +249,7 @@ class Tag
|
||||
|
||||
public static function recount(array $id_tags = [])
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
if ($db->getDriver() === 'sqlite')
|
||||
{
|
||||
return $db->query('
|
||||
UPDATE tags SET count = (
|
||||
SELECT COUNT(*)
|
||||
FROM `assets_tags` AS at
|
||||
WHERE at.id_tag = tags.id_tag
|
||||
)' . (!empty($id_tags) ? '
|
||||
WHERE tags.id_tag IN(@id_tags)' : ''),
|
||||
['id_tags' => $id_tags]);
|
||||
}
|
||||
|
||||
return $db->query('
|
||||
return Registry::get('db')->query('
|
||||
UPDATE tags AS t SET count = (
|
||||
SELECT COUNT(*)
|
||||
FROM `assets_tags` AS at
|
||||
@@ -290,26 +272,13 @@ class Tag
|
||||
if (!isset($data['count']))
|
||||
$data['count'] = 0;
|
||||
|
||||
if ($db->getDriver() === 'sqlite')
|
||||
{
|
||||
$res = $db->query('
|
||||
INSERT INTO tags
|
||||
(id_parent, tag, slug, kind, description, count)
|
||||
VALUES
|
||||
(:id_parent, :tag, :slug, :kind, :description, :count)
|
||||
ON CONFLICT(slug) DO UPDATE SET count = count + 1',
|
||||
$data);
|
||||
}
|
||||
else
|
||||
{
|
||||
$res = $db->query('
|
||||
INSERT IGNORE INTO tags
|
||||
(id_parent, tag, slug, kind, description, count)
|
||||
VALUES
|
||||
(:id_parent, :tag, :slug, :kind, :description, :count)
|
||||
ON DUPLICATE KEY UPDATE count = count + 1',
|
||||
$data);
|
||||
}
|
||||
$res = $db->query('
|
||||
INSERT IGNORE INTO tags
|
||||
(id_parent, tag, slug, kind, description, count)
|
||||
VALUES
|
||||
(:id_parent, :tag, :slug, :kind, :description, :count)
|
||||
ON DUPLICATE KEY UPDATE count = count + 1',
|
||||
$data);
|
||||
|
||||
if (!$res)
|
||||
throw new Exception('Could not create the requested tag.');
|
||||
@@ -325,8 +294,6 @@ class Tag
|
||||
|
||||
public function save()
|
||||
{
|
||||
$vars = get_object_vars($this);
|
||||
|
||||
return Registry::get('db')->query('
|
||||
UPDATE tags
|
||||
SET
|
||||
@@ -339,7 +306,7 @@ class Tag
|
||||
description = :description,
|
||||
count = :count
|
||||
WHERE id_tag = :id_tag',
|
||||
$vars);
|
||||
get_object_vars($this));
|
||||
}
|
||||
|
||||
public function delete()
|
||||
@@ -494,12 +461,10 @@ class Tag
|
||||
$albums_by_parent = [];
|
||||
while ($row = $db->fetchAssoc($res))
|
||||
{
|
||||
$parent = $row['id_parent'];
|
||||
if (!isset($albums_by_parent[$row['id_parent']]))
|
||||
$albums_by_parent[$row['id_parent']] = [];
|
||||
|
||||
if (!isset($albums_by_parent[$parent]))
|
||||
$albums_by_parent[$parent] = [];
|
||||
|
||||
$albums_by_parent[$parent][] = $row + ['children' => []];
|
||||
$albums_by_parent[$row['id_parent']][] = $row + ['children' => []];
|
||||
}
|
||||
|
||||
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);
|
||||
|
||||
@@ -24,7 +24,6 @@ abstract class User
|
||||
protected $is_admin;
|
||||
protected $reset_key;
|
||||
protected $reset_blocked_until;
|
||||
protected $oidc_sub;
|
||||
|
||||
protected bool $is_logged;
|
||||
protected bool $is_guest;
|
||||
|
||||
@@ -30,38 +30,21 @@ function disableKeyDownPropagation(obj) {
|
||||
function enableTouchNavigation() {
|
||||
var x_down = null;
|
||||
var y_down = null;
|
||||
var cancelled = false;
|
||||
|
||||
document.addEventListener('touchstart', function(event) {
|
||||
if (event.touches.length > 1) {
|
||||
cancelled = true;
|
||||
return;
|
||||
}
|
||||
x_down = event.touches[0].clientX;
|
||||
y_down = event.touches[0].clientY;
|
||||
cancelled = false;
|
||||
}, false);
|
||||
|
||||
document.addEventListener('touchmove', function(event) {
|
||||
if (event.touches.length > 1) {
|
||||
cancelled = true;
|
||||
}
|
||||
}, false);
|
||||
|
||||
document.addEventListener('touchend', function(event) {
|
||||
if (cancelled || x_down === null || y_down === null) {
|
||||
x_down = null;
|
||||
y_down = null;
|
||||
if (!x_down || !y_down) {
|
||||
return;
|
||||
}
|
||||
|
||||
var x_diff = x_down - event.changedTouches[0].clientX;
|
||||
var y_diff = y_down - event.changedTouches[0].clientY;
|
||||
var x_diff = x_down - event.touches[0].clientX;
|
||||
var y_diff = y_down - event.touches[0].clientY;
|
||||
|
||||
x_down = null;
|
||||
y_down = null;
|
||||
|
||||
if (Math.abs(x_diff) < 40 || Math.abs(y_diff) > 50) {
|
||||
if (Math.abs(y_diff) > 50) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,11 +52,13 @@ function enableTouchNavigation() {
|
||||
if (x_diff > 0) {
|
||||
var target = document.getElementById("previous_photo").href;
|
||||
if (target) {
|
||||
event.preventDefault();
|
||||
document.location.href = target + '#photo_frame';
|
||||
}
|
||||
} else {
|
||||
var target = document.getElementById("next_photo").href;
|
||||
if (target) {
|
||||
event.preventDefault();
|
||||
document.location.href = target + '#photo_frame';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
-- SQLite schema for Kabuki CMS / pics
|
||||
--
|
||||
-- Usage:
|
||||
-- sqlite3 data/pics.sqlite < schema.sqlite.sql
|
||||
--
|
||||
-- Config (add to config.php):
|
||||
-- define('DB_DRIVER', 'sqlite');
|
||||
-- define('DB_FILE', __DIR__ . '/data/pics.sqlite');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id_user INTEGER PRIMARY KEY,
|
||||
first_name TEXT NOT NULL,
|
||||
surname TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
emailaddress TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
creation_time INTEGER NOT NULL,
|
||||
last_action_time INTEGER,
|
||||
ip_address TEXT,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
reset_key TEXT,
|
||||
reset_blocked_until INTEGER,
|
||||
oidc_sub TEXT UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id_asset INTEGER PRIMARY KEY,
|
||||
id_user_uploaded INTEGER NOT NULL,
|
||||
subdir TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
title TEXT,
|
||||
slug TEXT UNIQUE,
|
||||
mimetype TEXT,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
date_captured TEXT,
|
||||
priority INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets_meta (
|
||||
id_asset INTEGER NOT NULL,
|
||||
variable TEXT NOT NULL,
|
||||
value TEXT,
|
||||
PRIMARY KEY (id_asset, variable)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets_thumbs (
|
||||
id_asset INTEGER NOT NULL,
|
||||
width INTEGER NOT NULL,
|
||||
height INTEGER NOT NULL,
|
||||
mode TEXT,
|
||||
filename TEXT,
|
||||
PRIMARY KEY (id_asset, width, height, mode)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id_tag INTEGER PRIMARY KEY,
|
||||
id_parent INTEGER,
|
||||
id_asset_thumb INTEGER,
|
||||
id_user_owner INTEGER,
|
||||
tag TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
kind TEXT NOT NULL DEFAULT 'Tag',
|
||||
count INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assets_tags (
|
||||
id_asset INTEGER NOT NULL,
|
||||
id_tag INTEGER NOT NULL,
|
||||
PRIMARY KEY (id_asset, id_tag)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts_assets (
|
||||
id_post INTEGER NOT NULL,
|
||||
id_asset INTEGER NOT NULL,
|
||||
PRIMARY KEY (id_post, id_asset)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts_tags (
|
||||
id_post INTEGER NOT NULL,
|
||||
id_tag INTEGER NOT NULL,
|
||||
PRIMARY KEY (id_post, id_tag)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id_user INTEGER NOT NULL,
|
||||
variable TEXT NOT NULL,
|
||||
value TEXT,
|
||||
time_set TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id_user, variable)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS log_errors (
|
||||
id_entry INTEGER PRIMARY KEY,
|
||||
id_user INTEGER,
|
||||
message TEXT,
|
||||
debug_info TEXT,
|
||||
file TEXT,
|
||||
line INTEGER,
|
||||
request_uri TEXT,
|
||||
time TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
ip_address TEXT
|
||||
);
|
||||
58
seed.php
58
seed.php
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
/*****************************************************************************
|
||||
* seed.php
|
||||
* Seeds a fresh database with an admin user and root album.
|
||||
*
|
||||
* Usage: php seed.php
|
||||
*****************************************************************************/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (!defined('DB_DRIVER') || DB_DRIVER !== 'sqlite')
|
||||
{
|
||||
echo "Error: seed.php currently only supports SQLite.\n";
|
||||
echo "Set DB_DRIVER to 'sqlite' in config.php.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!file_exists(DB_FILE))
|
||||
{
|
||||
echo "Error: database file not found at " . DB_FILE . "\n";
|
||||
echo "Create it first: sqlite3 " . DB_FILE . " < schema.sqlite.sql\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$db = new Database('sqlite', ['file' => DB_FILE]);
|
||||
Registry::set('db', $db);
|
||||
|
||||
// Create admin user.
|
||||
$password = 'admin';
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$db->insert('insert', 'users', [], [
|
||||
'first_name' => 'Admin',
|
||||
'surname' => 'User',
|
||||
'slug' => 'admin',
|
||||
'emailaddress' => 'admin@localhost',
|
||||
'password_hash' => $hash,
|
||||
'creation_time' => time(),
|
||||
'ip_address' => '',
|
||||
'is_admin' => 1,
|
||||
'reset_key' => '',
|
||||
]);
|
||||
|
||||
echo "Created admin user (admin@localhost / admin)\n";
|
||||
|
||||
// Create root album (id_tag = 1).
|
||||
$db->insert('insert', 'tags', [], [
|
||||
'id_parent' => 0,
|
||||
'tag' => 'Albums',
|
||||
'slug' => 'albums',
|
||||
'kind' => 'Album',
|
||||
'description' => '',
|
||||
'count' => 0,
|
||||
]);
|
||||
|
||||
echo "Created root album (id_tag = 1)\n";
|
||||
echo "\nDone. You can now log in at the web UI.\n";
|
||||
Reference in New Issue
Block a user