forked from Public/pics
Compare commits
9 Commits
fix/viewpe
...
add-oid
| Author | SHA1 | Date | |
|---|---|---|---|
| 555a3dbb95 | |||
| a361df2668 | |||
| c862d14e45 | |||
| d788391f4a | |||
| a4d453792d | |||
| 5832ce6228 | |||
| a71b8c9717 | |||
| b0ee3081a6 | |||
| 2cd2f472d0 |
8
app.php
8
app.php
@@ -14,7 +14,13 @@ require_once 'vendor/autoload.php';
|
|||||||
|
|
||||||
// Initialise the database.
|
// Initialise the database.
|
||||||
Registry::set('start', microtime(true));
|
Registry::set('start', microtime(true));
|
||||||
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
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,
|
||||||
|
]));
|
||||||
|
|
||||||
// Handle errors our own way.
|
// Handle errors our own way.
|
||||||
ErrorHandler::enable();
|
ErrorHandler::enable();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"ext-imagick": "*",
|
"ext-imagick": "*",
|
||||||
"ext-mysqli": "*",
|
"ext-mysqli": "*",
|
||||||
"twbs/bootstrap": "^5.3",
|
"twbs/bootstrap": "^5.3",
|
||||||
"twbs/bootstrap-icons": "^1.10"
|
"twbs/bootstrap-icons": "^1.10",
|
||||||
|
"jumbojett/openid-connect-php": "^1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,3 +34,10 @@ const DB_LOG_QUERIES = false;
|
|||||||
|
|
||||||
const SITE_TITLE = 'HashRU Pics';
|
const SITE_TITLE = 'HashRU Pics';
|
||||||
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
||||||
|
|
||||||
|
// OIDC authentication (e.g. Kanidm). OIDC is enabled when OIDC_PROVIDER_URL is non-empty.
|
||||||
|
const OIDC_PROVIDER_URL = ''; // e.g. 'https://kanidm.example.com/oauth2/openid/pics'
|
||||||
|
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();
|
$data = $this->form->getData();
|
||||||
|
|
||||||
// Sanity check: don't let an album be its own parent
|
// Sanity check: don't let an album be its own parent
|
||||||
if ($data['id_parent'] == $id_tag)
|
if ($id_tag && $data['id_parent'] == $id_tag)
|
||||||
{
|
{
|
||||||
return $this->formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
|
return $this->formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ class EditUser extends HTMLController
|
|||||||
'maxlength' => 255,
|
'maxlength' => 255,
|
||||||
'is_optional' => true,
|
'is_optional' => true,
|
||||||
],
|
],
|
||||||
|
'oidc_sub' => [
|
||||||
|
'header' => 'OIDC',
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => 'OIDC subject identifier',
|
||||||
|
'size' => 50,
|
||||||
|
'maxlength' => 255,
|
||||||
|
'is_optional' => true,
|
||||||
|
],
|
||||||
'is_admin' => [
|
'is_admin' => [
|
||||||
'header' => 'Privileges',
|
'header' => 'Privileges',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
@@ -145,6 +153,10 @@ class EditUser extends HTMLController
|
|||||||
// Quick stripping.
|
// Quick stripping.
|
||||||
$data['slug'] = strtr(strtolower($data['slug']), [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
|
$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!
|
// Checkboxes, fun!
|
||||||
$data['is_admin'] = empty($data['is_admin']) ? 0 : 1;
|
$data['is_admin'] = empty($data['is_admin']) ? 0 : 1;
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ class ManageAlbums extends HTMLController
|
|||||||
'items_per_page' => 9999,
|
'items_per_page' => 9999,
|
||||||
'base_url' => BASEURL . '/managealbums/',
|
'base_url' => BASEURL . '/managealbums/',
|
||||||
'get_data' => function($offset, $limit, $order, $direction) {
|
'get_data' => function($offset, $limit, $order, $direction) {
|
||||||
return Tag::getOffset($offset, $limit, $order, $direction, true);
|
return Tag::getOffset($offset, $limit, $order, $direction);
|
||||||
},
|
},
|
||||||
'get_count' => function() {
|
'get_count' => function() {
|
||||||
return Tag::getCount(false, 'Album', true);
|
return Tag::getCount(false, 'Album');
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ class ManageTags extends HTMLController
|
|||||||
'items_per_page' => 9999,
|
'items_per_page' => 9999,
|
||||||
'base_url' => BASEURL . '/managetags/',
|
'base_url' => BASEURL . '/managetags/',
|
||||||
'get_data' => function($offset, $limit, $order, $direction) {
|
'get_data' => function($offset, $limit, $order, $direction) {
|
||||||
return Tag::getOffset($offset, $limit, $order, $direction, false);
|
return Tag::getOffset($offset, $limit, $order, $direction, true);
|
||||||
},
|
},
|
||||||
'get_count' => function() {
|
'get_count' => function() {
|
||||||
return Tag::getCount(false, null, false);
|
return Tag::getCount(false, null, true);
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
152
controllers/OIDCLogin.php
Normal file
152
controllers/OIDCLogin.php
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
/*****************************************************************************
|
||||||
|
* OIDCLogin.php
|
||||||
|
* Handles OIDC authentication via Kanidm (or other OIDC providers).
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
use Jumbojett\OpenIDConnectClient;
|
||||||
|
|
||||||
|
class OIDCLogin
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
if (empty(OIDC_PROVIDER_URL))
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/login/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already logged in? Redirect home.
|
||||||
|
if (Registry::get('user')->isLoggedIn())
|
||||||
|
{
|
||||||
|
header('Location: ' . BASEURL . '/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store redirect URL in session before OIDC flow.
|
||||||
|
if (isset($_GET['redirect']))
|
||||||
|
$_SESSION['oidc_redirect_url'] = base64_decode($_GET['redirect']);
|
||||||
|
|
||||||
|
$oidc = new OpenIDConnectClient(OIDC_PROVIDER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET);
|
||||||
|
$oidc->setRedirectURL(BASEURL . '/oidclogin/');
|
||||||
|
$oidc->addScope(['openid', 'email', 'profile', 'groups']);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$oidc->authenticate();
|
||||||
|
}
|
||||||
|
catch (\Exception $e)
|
||||||
|
{
|
||||||
|
$_SESSION['login_msg'] = ['', 'OIDC authentication failed: ' . $e->getMessage(), 'danger'];
|
||||||
|
header('Location: ' . BASEURL . '/login/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the stable subject identifier from the ID token.
|
||||||
|
$sub = $oidc->getVerifiedClaims('sub');
|
||||||
|
if (empty($sub))
|
||||||
|
{
|
||||||
|
$_SESSION['login_msg'] = ['', 'No subject identifier 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.
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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']))
|
||||||
|
{
|
||||||
|
$redirect = $_SESSION['oidc_redirect_url'];
|
||||||
|
unset($_SESSION['oidc_redirect_url']);
|
||||||
|
header('Location: ' . $redirect);
|
||||||
|
}
|
||||||
|
elseif (!empty($_SESSION['login_url']))
|
||||||
|
{
|
||||||
|
$redirect = $_SESSION['login_url'];
|
||||||
|
unset($_SESSION['login_url']);
|
||||||
|
header('Location: ' . $redirect);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
header('Location: ' . BASEURL . '/');
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ class ViewPeople extends HTMLController
|
|||||||
|
|
||||||
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
|
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
|
||||||
$start = ($page - 1) * self::PER_PAGE;
|
$start = ($page - 1) * self::PER_PAGE;
|
||||||
$total_count = Tag::getCount(1, 'Person', true);
|
$total_count = Tag::getCount(1, 'Person');
|
||||||
|
|
||||||
// Fetch subalbums.
|
// Fetch subalbums.
|
||||||
$subalbums = Tag::getPeople(0, $start, self::PER_PAGE);
|
$subalbums = Tag::getPeople(0, $start, self::PER_PAGE);
|
||||||
|
|||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
47
flake.nix
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
2
migrations/2026-02-15-oidc-sub.sql
Normal file
2
migrations/2026-02-15-oidc-sub.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN oidc_sub TEXT;
|
||||||
|
CREATE UNIQUE INDEX idx_users_oidc_sub ON users (oidc_sub);
|
||||||
@@ -10,7 +10,6 @@ class AssetIterator implements Iterator
|
|||||||
{
|
{
|
||||||
private $direction;
|
private $direction;
|
||||||
private $return_format;
|
private $return_format;
|
||||||
private $rowCount;
|
|
||||||
|
|
||||||
private $assets_iterator;
|
private $assets_iterator;
|
||||||
private $meta_iterator;
|
private $meta_iterator;
|
||||||
@@ -21,7 +20,6 @@ class AssetIterator implements Iterator
|
|||||||
{
|
{
|
||||||
$this->direction = $direction;
|
$this->direction = $direction;
|
||||||
$this->return_format = $return_format;
|
$this->return_format = $return_format;
|
||||||
$this->rowCount = $stmt_assets->rowCount();
|
|
||||||
|
|
||||||
$this->assets_iterator = new CachedPDOIterator($stmt_assets);
|
$this->assets_iterator = new CachedPDOIterator($stmt_assets);
|
||||||
$this->assets_iterator->rewind();
|
$this->assets_iterator->rewind();
|
||||||
@@ -209,7 +207,7 @@ class AssetIterator implements Iterator
|
|||||||
|
|
||||||
public function num(): int
|
public function num(): int
|
||||||
{
|
{
|
||||||
return $this->rowCount;
|
return count($this->assets_iterator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rewind(): void
|
public function rewind(): void
|
||||||
|
|||||||
@@ -9,18 +9,35 @@
|
|||||||
class Database
|
class Database
|
||||||
{
|
{
|
||||||
private $connection;
|
private $connection;
|
||||||
|
private $driver;
|
||||||
private $query_count = 0;
|
private $query_count = 0;
|
||||||
private $logged_queries = [];
|
private $logged_queries = [];
|
||||||
|
|
||||||
public function __construct($host, $user, $password, $name)
|
public function __construct($driver, array $options)
|
||||||
{
|
{
|
||||||
|
$this->driver = $driver;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
|
if ($driver === 'sqlite')
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
{
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
$this->connection = new PDO("sqlite:" . $options['file'], null, null, [
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Give up if we have a connection error.
|
// Give up if we have a connection error.
|
||||||
catch (PDOException $e)
|
catch (PDOException $e)
|
||||||
@@ -31,6 +48,54 @@ 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()
|
public function getQueryCount()
|
||||||
{
|
{
|
||||||
return $this->query_count;
|
return $this->query_count;
|
||||||
@@ -180,6 +245,10 @@ class Database
|
|||||||
// Preprocessing/checks: prepare any arrays for binding
|
// Preprocessing/checks: prepare any arrays for binding
|
||||||
$db_string = $this->expandPlaceholders($db_string, $db_values);
|
$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
|
// Prepare query for execution
|
||||||
$statement = $this->connection->prepare($db_string);
|
$statement = $this->connection->prepare($db_string);
|
||||||
|
|
||||||
@@ -223,13 +292,10 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->rowCount($res) === 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
$object = $this->fetchObject($res, $class);
|
$object = $this->fetchObject($res, $class);
|
||||||
$this->free($res);
|
$this->free($res);
|
||||||
|
|
||||||
return $object;
|
return $object ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,9 +305,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($object = $this->fetchObject($res, $class))
|
while ($object = $this->fetchObject($res, $class))
|
||||||
$rows[] = $object;
|
$rows[] = $object;
|
||||||
@@ -258,13 +321,10 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$row = $this->fetchNum($res);
|
$row = $this->fetchNum($res);
|
||||||
$this->free($res);
|
$this->free($res);
|
||||||
|
|
||||||
return $row;
|
return $row ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,9 +334,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($row = $this->fetchNum($res))
|
while ($row = $this->fetchNum($res))
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
@@ -293,9 +350,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($row = $this->fetchNum($res))
|
while ($row = $this->fetchNum($res))
|
||||||
$rows[$row[0]] = $row[1];
|
$rows[$row[0]] = $row[1];
|
||||||
@@ -312,9 +366,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if (!$res || $this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($row = $this->fetchAssoc($res))
|
while ($row = $this->fetchAssoc($res))
|
||||||
{
|
{
|
||||||
@@ -334,13 +385,10 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$row = $this->fetchAssoc($res);
|
$row = $this->fetchAssoc($res);
|
||||||
$this->free($res);
|
$this->free($res);
|
||||||
|
|
||||||
return $row;
|
return $row ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,9 +398,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($row = $this->fetchAssoc($res))
|
while ($row = $this->fetchAssoc($res))
|
||||||
$rows[] = $row;
|
$rows[] = $row;
|
||||||
@@ -369,14 +414,13 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
// If this happens, you're doing it wrong.
|
$row = $this->fetchNum($res);
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
list($value) = $this->fetchNum($res);
|
|
||||||
$this->free($res);
|
$this->free($res);
|
||||||
|
|
||||||
return $value;
|
if (!$row)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return $row[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -386,9 +430,6 @@ class Database
|
|||||||
{
|
{
|
||||||
$res = $this->query($db_string, $db_values);
|
$res = $this->query($db_string, $db_values);
|
||||||
|
|
||||||
if ($this->rowCount($res) === 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
while ($row = $this->fetchNum($res))
|
while ($row = $this->fetchNum($res))
|
||||||
$rows[] = $row[0];
|
$rows[] = $row[0];
|
||||||
@@ -412,7 +453,10 @@ class Database
|
|||||||
$data = [$data];
|
$data = [$data];
|
||||||
|
|
||||||
// Determine the method of insertion.
|
// Determine the method of insertion.
|
||||||
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
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');
|
||||||
|
|
||||||
// What columns are we inserting?
|
// What columns are we inserting?
|
||||||
$columns = array_keys($data[0]);
|
$columns = array_keys($data[0]);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class Dispatcher
|
|||||||
private static function trigger404()
|
private static function trigger404()
|
||||||
{
|
{
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
$page = new ViewErrorPage('Page not found!');
|
self::errorPage('Page not found!', 'The page you requested could not be found.');
|
||||||
$page->showContent();
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class ErrorHandler
|
|||||||
'debug_info' => $debug_info,
|
'debug_info' => $debug_info,
|
||||||
'file' => str_replace(BASEDIR, '', $file),
|
'file' => str_replace(BASEDIR, '', $file),
|
||||||
'line' => $line,
|
'line' => $line,
|
||||||
'id_user' => Registry::has('user') ? Registry::get('user')->getUserId() : 0,
|
'id_user' => Registry::has('user') && Registry::get('user')->getUserId() ? Registry::get('user')->getUserId() : 0,
|
||||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||||
'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
|
'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
|
||||||
]))
|
]))
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ class Member extends User
|
|||||||
['email_address' => $email_address]);
|
['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)
|
public static function fromId($id_user)
|
||||||
{
|
{
|
||||||
$row = Registry::get('db')->queryAssoc('
|
$row = Registry::get('db')->queryAssoc('
|
||||||
@@ -73,18 +82,27 @@ class Member extends User
|
|||||||
'surname' => !empty($data['surname']) ? $data['surname'] : $error |= true,
|
'surname' => !empty($data['surname']) ? $data['surname'] : $error |= true,
|
||||||
'slug' => !empty($data['slug']) ? $data['slug'] : $error |= true,
|
'slug' => !empty($data['slug']) ? $data['slug'] : $error |= true,
|
||||||
'emailaddress' => !empty($data['emailaddress']) ? $data['emailaddress'] : $error |= true,
|
'emailaddress' => !empty($data['emailaddress']) ? $data['emailaddress'] : $error |= true,
|
||||||
'password_hash' => !empty($data['password']) ? Authentication::computeHash($data['password']) : $error |= true,
|
|
||||||
'creation_time' => time(),
|
'creation_time' => time(),
|
||||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||||
'is_admin' => empty($data['is_admin']) ? 0 : 1,
|
'is_admin' => empty($data['is_admin']) ? 0 : 1,
|
||||||
'reset_key' => '',
|
'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)
|
if ($error)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
$db = Registry::get('db');
|
$columns = [
|
||||||
$bool = $db->insert('insert', 'users', [
|
|
||||||
'first_name' => 'string-30',
|
'first_name' => 'string-30',
|
||||||
'surname' => 'string-60',
|
'surname' => 'string-60',
|
||||||
'slug' => 'string-90',
|
'slug' => 'string-90',
|
||||||
@@ -93,8 +111,14 @@ class Member extends User
|
|||||||
'creation_time' => 'int',
|
'creation_time' => 'int',
|
||||||
'ip_address' => 'string-45',
|
'ip_address' => 'string-45',
|
||||||
'is_admin' => 'int',
|
'is_admin' => 'int',
|
||||||
'reset_key' => 'string-16'
|
'reset_key' => 'string-16',
|
||||||
], $new_user, ['id_user']);
|
];
|
||||||
|
|
||||||
|
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']);
|
||||||
|
|
||||||
if (!$bool)
|
if (!$bool)
|
||||||
return false;
|
return false;
|
||||||
@@ -113,7 +137,7 @@ class Member extends User
|
|||||||
{
|
{
|
||||||
foreach ($new_data as $key => $value)
|
foreach ($new_data as $key => $value)
|
||||||
{
|
{
|
||||||
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress']))
|
if (in_array($key, ['first_name', 'surname', 'slug', 'emailaddress', 'oidc_sub']))
|
||||||
$this->$key = $value;
|
$this->$key = $value;
|
||||||
elseif ($key === 'password')
|
elseif ($key === 'password')
|
||||||
$this->password_hash = Authentication::computeHash($value);
|
$this->password_hash = Authentication::computeHash($value);
|
||||||
@@ -132,7 +156,8 @@ class Member extends User
|
|||||||
slug = :slug,
|
slug = :slug,
|
||||||
emailaddress = :emailaddress,
|
emailaddress = :emailaddress,
|
||||||
password_hash = :password_hash,
|
password_hash = :password_hash,
|
||||||
is_admin = :is_admin
|
is_admin = :is_admin,
|
||||||
|
oidc_sub = :oidc_sub
|
||||||
WHERE id_user = :id_user',
|
WHERE id_user = :id_user',
|
||||||
get_object_vars($this));
|
get_object_vars($this));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ class Router
|
|||||||
$possibleActions = [
|
$possibleActions = [
|
||||||
'accountsettings' => 'AccountSettings',
|
'accountsettings' => 'AccountSettings',
|
||||||
'addalbum' => 'EditAlbum',
|
'addalbum' => 'EditAlbum',
|
||||||
'albums' => 'ViewPhotoAlbums',
|
'albums' => 'ViewPhotoAlbum',
|
||||||
'editalbum' => 'EditAlbum',
|
'editalbum' => 'EditAlbum',
|
||||||
'editasset' => 'EditAsset',
|
'editasset' => 'EditAsset',
|
||||||
'edittag' => 'EditTag',
|
'edittag' => 'EditTag',
|
||||||
'edituser' => 'EditUser',
|
'edituser' => 'EditUser',
|
||||||
'login' => 'Login',
|
'login' => 'Login',
|
||||||
'logout' => 'Logout',
|
'logout' => 'Logout',
|
||||||
|
'oidclogin' => 'OIDCLogin',
|
||||||
'managealbums' => 'ManageAlbums',
|
'managealbums' => 'ManageAlbums',
|
||||||
'manageassets' => 'ManageAssets',
|
'manageassets' => 'ManageAssets',
|
||||||
'manageerrors' => 'ManageErrors',
|
'manageerrors' => 'ManageErrors',
|
||||||
@@ -54,7 +55,7 @@ class Router
|
|||||||
return new GenerateThumbnail();
|
return new GenerateThumbnail();
|
||||||
}
|
}
|
||||||
// Look for particular actions...
|
// 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);
|
$_GET = array_merge($_GET, $path);
|
||||||
return new $possibleActions[$path['action']]();
|
return new $possibleActions[$path['action']]();
|
||||||
|
|||||||
@@ -122,10 +122,12 @@ class Tag
|
|||||||
|
|
||||||
public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
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('
|
$rows = Registry::get('db')->queryAssocs('
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM tags
|
FROM tags
|
||||||
WHERE id_parent = :id_parent AND kind = :kind
|
WHERE ' . $parent_clause . ' AND kind = :kind
|
||||||
ORDER BY tag ASC
|
ORDER BY tag ASC
|
||||||
LIMIT :offset, :limit',
|
LIMIT :offset, :limit',
|
||||||
[
|
[
|
||||||
@@ -163,10 +165,12 @@ class Tag
|
|||||||
|
|
||||||
public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
|
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('
|
$rows = Registry::get('db')->queryAssocs('
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM tags
|
FROM tags
|
||||||
WHERE id_parent = :id_parent AND kind = :kind
|
WHERE ' . $parent_clause . ' AND kind = :kind
|
||||||
ORDER BY tag ASC
|
ORDER BY tag ASC
|
||||||
LIMIT :offset, :limit',
|
LIMIT :offset, :limit',
|
||||||
[
|
[
|
||||||
@@ -249,7 +253,21 @@ class Tag
|
|||||||
|
|
||||||
public static function recount(array $id_tags = [])
|
public static function recount(array $id_tags = [])
|
||||||
{
|
{
|
||||||
return Registry::get('db')->query('
|
$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('
|
||||||
UPDATE tags AS t SET count = (
|
UPDATE tags AS t SET count = (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM `assets_tags` AS at
|
FROM `assets_tags` AS at
|
||||||
@@ -272,13 +290,26 @@ class Tag
|
|||||||
if (!isset($data['count']))
|
if (!isset($data['count']))
|
||||||
$data['count'] = 0;
|
$data['count'] = 0;
|
||||||
|
|
||||||
$res = $db->query('
|
if ($db->getDriver() === 'sqlite')
|
||||||
INSERT IGNORE INTO tags
|
{
|
||||||
(id_parent, tag, slug, kind, description, count)
|
$res = $db->query('
|
||||||
VALUES
|
INSERT INTO tags
|
||||||
(:id_parent, :tag, :slug, :kind, :description, :count)
|
(id_parent, tag, slug, kind, description, count)
|
||||||
ON DUPLICATE KEY UPDATE count = count + 1',
|
VALUES
|
||||||
$data);
|
(: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);
|
||||||
|
}
|
||||||
|
|
||||||
if (!$res)
|
if (!$res)
|
||||||
throw new Exception('Could not create the requested tag.');
|
throw new Exception('Could not create the requested tag.');
|
||||||
@@ -294,6 +325,8 @@ class Tag
|
|||||||
|
|
||||||
public function save()
|
public function save()
|
||||||
{
|
{
|
||||||
|
$vars = get_object_vars($this);
|
||||||
|
|
||||||
return Registry::get('db')->query('
|
return Registry::get('db')->query('
|
||||||
UPDATE tags
|
UPDATE tags
|
||||||
SET
|
SET
|
||||||
@@ -306,7 +339,7 @@ class Tag
|
|||||||
description = :description,
|
description = :description,
|
||||||
count = :count
|
count = :count
|
||||||
WHERE id_tag = :id_tag',
|
WHERE id_tag = :id_tag',
|
||||||
get_object_vars($this));
|
$vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
@@ -417,16 +450,17 @@ class Tag
|
|||||||
['tags' => $tags]);
|
['tags' => $tags]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getCount($only_used = true, $kind = '', $isAlbum = false)
|
public static function getCount($only_used = true, $kind = '', $invert = false)
|
||||||
{
|
{
|
||||||
$where = [];
|
$where = [];
|
||||||
if ($only_used)
|
|
||||||
$where[] = 'count > 0';
|
$where[] = 'kind ' . ($invert ? '!=' : '=' ) . ' :kind';
|
||||||
if (empty($kind))
|
if (empty($kind))
|
||||||
$kind = 'Album';
|
$kind = 'Album';
|
||||||
|
|
||||||
$operator = $isAlbum ? '=' : '!=';
|
if ($only_used)
|
||||||
$where[] = 'kind ' . $operator . ' :kind';
|
$where[] = 'count > 0';
|
||||||
|
|
||||||
$where = implode(' AND ', $where);
|
$where = implode(' AND ', $where);
|
||||||
|
|
||||||
return Registry::get('db')->queryValue('
|
return Registry::get('db')->queryValue('
|
||||||
@@ -438,19 +472,17 @@ class Tag
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getOffset($offset, $limit, $order, $direction, $isAlbum = false)
|
public static function getOffset($offset, $limit, $order, $direction, $invert = false)
|
||||||
{
|
{
|
||||||
assert(in_array($order, ['id_tag', 'tag', 'slug', 'count']));
|
assert(in_array($order, ['id_tag', 'tag', 'slug', 'count']));
|
||||||
$order = $order . ($direction === 'up' ? ' ASC' : ' DESC');
|
$order = $order . ($direction === 'up' ? ' ASC' : ' DESC');
|
||||||
|
|
||||||
$operator = $isAlbum ? '=' : '!=';
|
|
||||||
|
|
||||||
$db = Registry::get('db');
|
$db = Registry::get('db');
|
||||||
$res = $db->query('
|
$res = $db->query('
|
||||||
SELECT t.*, u.id_user, u.first_name, u.surname
|
SELECT t.*, u.id_user, u.first_name, u.surname
|
||||||
FROM tags AS t
|
FROM tags AS t
|
||||||
LEFT JOIN users AS u ON t.id_user_owner = u.id_user
|
LEFT JOIN users AS u ON t.id_user_owner = u.id_user
|
||||||
WHERE kind ' . $operator . ' :album
|
WHERE kind ' . ($invert ? '!=' : '=') . ' :album
|
||||||
ORDER BY id_parent, ' . $order . '
|
ORDER BY id_parent, ' . $order . '
|
||||||
LIMIT :offset, :limit',
|
LIMIT :offset, :limit',
|
||||||
[
|
[
|
||||||
@@ -462,10 +494,12 @@ class Tag
|
|||||||
$albums_by_parent = [];
|
$albums_by_parent = [];
|
||||||
while ($row = $db->fetchAssoc($res))
|
while ($row = $db->fetchAssoc($res))
|
||||||
{
|
{
|
||||||
if (!isset($albums_by_parent[$row['id_parent']]))
|
$parent = $row['id_parent'];
|
||||||
$albums_by_parent[$row['id_parent']] = [];
|
|
||||||
|
|
||||||
$albums_by_parent[$row['id_parent']][] = $row + ['children' => []];
|
if (!isset($albums_by_parent[$parent]))
|
||||||
|
$albums_by_parent[$parent] = [];
|
||||||
|
|
||||||
|
$albums_by_parent[$parent][] = $row + ['children' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);
|
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ abstract class User
|
|||||||
protected $is_admin;
|
protected $is_admin;
|
||||||
protected $reset_key;
|
protected $reset_key;
|
||||||
protected $reset_blocked_until;
|
protected $reset_blocked_until;
|
||||||
|
protected $oidc_sub;
|
||||||
|
|
||||||
protected bool $is_logged;
|
protected bool $is_logged;
|
||||||
protected bool $is_guest;
|
protected bool $is_guest;
|
||||||
|
|||||||
@@ -30,21 +30,38 @@ function disableKeyDownPropagation(obj) {
|
|||||||
function enableTouchNavigation() {
|
function enableTouchNavigation() {
|
||||||
var x_down = null;
|
var x_down = null;
|
||||||
var y_down = null;
|
var y_down = null;
|
||||||
|
var cancelled = false;
|
||||||
|
|
||||||
document.addEventListener('touchstart', function(event) {
|
document.addEventListener('touchstart', function(event) {
|
||||||
|
if (event.touches.length > 1) {
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
x_down = event.touches[0].clientX;
|
x_down = event.touches[0].clientX;
|
||||||
y_down = event.touches[0].clientY;
|
y_down = event.touches[0].clientY;
|
||||||
|
cancelled = false;
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
document.addEventListener('touchmove', function(event) {
|
document.addEventListener('touchmove', function(event) {
|
||||||
if (!x_down || !y_down) {
|
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;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var x_diff = x_down - event.touches[0].clientX;
|
var x_diff = x_down - event.changedTouches[0].clientX;
|
||||||
var y_diff = y_down - event.touches[0].clientY;
|
var y_diff = y_down - event.changedTouches[0].clientY;
|
||||||
|
|
||||||
if (Math.abs(y_diff) > 50) {
|
x_down = null;
|
||||||
|
y_down = null;
|
||||||
|
|
||||||
|
if (Math.abs(x_diff) < 40 || Math.abs(y_diff) > 50) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +69,11 @@ function enableTouchNavigation() {
|
|||||||
if (x_diff > 0) {
|
if (x_diff > 0) {
|
||||||
var target = document.getElementById("previous_photo").href;
|
var target = document.getElementById("previous_photo").href;
|
||||||
if (target) {
|
if (target) {
|
||||||
event.preventDefault();
|
|
||||||
document.location.href = target + '#photo_frame';
|
document.location.href = target + '#photo_frame';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var target = document.getElementById("next_photo").href;
|
var target = document.getElementById("next_photo").href;
|
||||||
if (target) {
|
if (target) {
|
||||||
event.preventDefault();
|
|
||||||
document.location.href = target + '#photo_frame';
|
document.location.href = target + '#photo_frame';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
schema.sqlite.sql
Normal file
104
schema.sqlite.sql
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
-- 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
Normal file
58
seed.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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";
|
||||||
@@ -63,5 +63,18 @@ class LogInForm extends SubTemplate
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>';
|
</form>';
|
||||||
|
|
||||||
|
if (!empty(OIDC_PROVIDER_URL))
|
||||||
|
{
|
||||||
|
$oidc_url = BASEURL . '/oidclogin/';
|
||||||
|
if (!empty($this->redirect_url))
|
||||||
|
$oidc_url .= '?redirect=' . base64_encode($this->redirect_url);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<hr>
|
||||||
|
<a class="btn btn-secondary" href="', htmlspecialchars($oidc_url), '">Login with ', htmlspecialchars(OIDC_PROVIDER_NAME), '</a>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user