From 88d1a190a0d9acfc8d5556e60c7955a3fe6011c3 Mon Sep 17 00:00:00 2001 From: Spine Date: Sun, 11 Feb 2024 06:17:11 +0000 Subject: [PATCH] appify custom navigation links --- app/Manager/User.php | 25 ----- app/Manager/UserNavigation.php | 78 +++++++++++++++ app/User.php | 10 +- app/UserNavigation.php | 99 +++++++++++++++++++ bin/migrate-user-nav-items | 24 +++++ classes/view.class.php | 2 +- .../20240129000000_user_navigation_json.php | 12 +++ sections/tools/managers/navigation_alter.php | 80 ++++++++------- sections/tools/managers/navigation_list.php | 4 +- sections/user/edit.php | 6 +- sections/user/edit_handle.php | 10 +- ...m-navigation.twig => user-navigation.twig} | 0 tests/phpunit/UserNavigationTest.php | 32 ++++++ 13 files changed, 302 insertions(+), 80 deletions(-) create mode 100644 app/Manager/UserNavigation.php create mode 100644 app/UserNavigation.php create mode 100755 bin/migrate-user-nav-items create mode 100644 misc/phinx/migrations/20240129000000_user_navigation_json.php rename templates/admin/{forum-navigation.twig => user-navigation.twig} (100%) create mode 100644 tests/phpunit/UserNavigationTest.php diff --git a/app/Manager/User.php b/app/Manager/User.php index 130779d55..69753b176 100644 --- a/app/Manager/User.php +++ b/app/Manager/User.php @@ -1629,31 +1629,6 @@ public function updateLastAccess(): int { return $affected; } - public function userNavList(\Gazelle\User $user): array { - $navList = $user->navigationList(); - $list = []; - foreach ($this->userNavFullList() as $n) { - if ($n['mandatory'] || in_array($n['id'], $navList) || (!$navList && $n['initial'])) { - $list[] = $n; - } - } - return $list; - } - - public function userNavFullList(): array { - $list = self::$cache->get_value("nav_items"); - if (!$list) { - $QueryID = self::$db->get_query_id(); - self::$db->prepared_query(" - SELECT id, tag, title, target, tests, test_user, mandatory, initial - FROM nav_items"); - $list = self::$db->to_array("id", MYSQLI_ASSOC, false); - self::$cache->cache_value("nav_items", $list, 0); - self::$db->set_query_id($QueryID); - } - return $list; - } - public function checkPassword(string $password): string { return $password === '' || (bool)self::$db->scalar(" diff --git a/app/Manager/UserNavigation.php b/app/Manager/UserNavigation.php new file mode 100644 index 000000000..4d0a76afc --- /dev/null +++ b/app/Manager/UserNavigation.php @@ -0,0 +1,78 @@ +prepared_query(" + INSERT INTO nav_items + (tag, title, target, tests, test_user, mandatory, initial) + VALUES (?, ?, ?, ?, ?, ?, ?) + ", $tag, $title, $target, $tests, $testUser, $mandatory, $initial + ); + $id = self::$db->inserted_id(); + self::$cache->delete_value(self::LIST_KEY); + return new \Gazelle\UserNavigation($id); + } + + public function findById(int $controlId): ?\Gazelle\UserNavigation { + $key = sprintf(self::ID_KEY, $controlId); + $id = self::$cache->get_value($key); + if ($id === false) { + $id = self::$db->scalar(" + SELECT ID FROM forums_nav WHERE ID = ? + ", $controlId + ); + if (!is_null($id)) { + self::$cache->cache_value($key, $id, 7200); + } + } + return $id ? new \Gazelle\UserNavigation($id) : null; + } + + public function userControlList(\Gazelle\User $user): array { + $navList = $user->navigationList(); + $list = []; + foreach ($this->fullList() as $n) { + if (($n['mandatory'] || in_array($n['id'], $navList)) || (!count($navList) && $n['initial'])) { + $list[] = $n; + } + } + return $list; + } + + public function fullList(): array { + $list = self::$cache->get_value(self::LIST_KEY); + if (!$list) { + self::$db->prepared_query(" + SELECT + id, + tag, + title, + target, + tests, + test_user, + mandatory, + initial + FROM nav_items + "); + $list = self::$db->to_array("id", MYSQLI_ASSOC, false); + self::$cache->cache_value(self::LIST_KEY, $list, 0); + } + return $list; + } +} diff --git a/app/User.php b/app/User.php index 1997e8d71..26dc7dfef 100644 --- a/app/User.php +++ b/app/User.php @@ -129,6 +129,7 @@ public function info(): array { um.Enabled, um.Invites, um.IRCKey, + um.nav_list, um.Paranoia, um.PassHash, um.PermissionID, @@ -171,6 +172,7 @@ public function info(): array { } $this->info['CommentHash'] = sha1($this->info['AdminComment']); + $this->info['nav_list'] = json_decode($this->info['nav_list'], true); $this->info['NavItems'] = empty($this->info['NavItems']) ? [] : explode(',', $this->info['NavItems']); $this->info['ParanoiaRaw'] = $this->info['Paranoia']; $this->info['Paranoia'] = $this->info['Paranoia'] ? unserialize($this->info['Paranoia']) : []; @@ -198,7 +200,7 @@ public function info(): array { } /** - * Get the custom navigation configuration. + * Get the custom user link navigation configuration. */ public function navigationList(): array { return $this->info()['NavItems']; @@ -1047,11 +1049,13 @@ public function modify(): bool { $userInfo = []; if ($this->field('nav_list') !== null) { - $userInfo['NavItems = ?'] = implode(',', $this->clearField('nav_list')); + $userInfo['NavItems = ?'] = implode(',', $this->field('nav_list')); + $this->setField('nav_list', json_encode($this->clearField('nav_list'))); } if ($this->field('option_list') !== null) { - $userInfo['SiteOptions = ?'] = serialize($this->clearField('option_list')); + $userInfo['SiteOptions = ?'] = serialize($this->clearField('option_list')); // remove field } + foreach (['AdminComment', 'BanDate', 'BanReason', 'PermittedForums', 'RestrictedForums', 'RatioWatchDownload'] as $field) { if ($this->field($field) !== null) { $userInfo["$field = ?"] = $this->clearField($field); diff --git a/app/UserNavigation.php b/app/UserNavigation.php new file mode 100644 index 000000000..4928708de --- /dev/null +++ b/app/UserNavigation.php @@ -0,0 +1,99 @@ +delete_value(sprintf(self::CACHE_KEY, $this->id)); + self::$cache->delete_value(Manager\UserNavigation::LIST_KEY); + unset($this->info); + return $this; + } + public function link(): string { + return "location()}\">User Link Editor"; + } + public function location(): string { + return "tools.php?action=navigation"; + } + + public function info(): array { + if (isset($this->info)) { + return $this->info; + } + $key = sprintf(self::CACHE_KEY, $this->id); + $info = self::$cache->get_value($key); + if ($info === false) { + $info = self::$db->rowAssoc(" + SELECT tag, + title, + target, + tests, + test_user, + mandatory, + initial + FROM nav_items + WHERE id = ? + ", $this->id + ); + self::$cache->cache_value($key, $info, 86400); + } + $this->info = $info; + return $this->info; + } + + public function isTestUser(): bool { + return $this->info()['test_user']; + } + + public function isMandatory(): bool { + return $this->info()['mandatory']; + } + + public function isInitial(): bool { + return $this->info()['initial']; + } + + public function tag(): string { + return $this->info()['tag']; + } + + public function target(): string { + return $this->info()['target']; + } + + public function tests(): string { + return $this->info()['tests']; + } + + public function title(): string { + return $this->info()['title']; + } + + public function modify(): bool { + $success = parent::modify(); + if ($success) { + self::$cache->delete_value(Manager\UserNavigation::LIST_KEY); + } + return $success; + } + + public function remove(): int { + $id = $this->id; + $this->flush(); + self::$db->prepared_query(" + DELETE FROM nav_items WHERE id = ? + ", $this->id + ); + $affected = self::$db->affected_rows(); + if ($affected) { + self::$cache->delete_multi([ + sprintf(Manager\UserNavigation::ID_KEY, $id), + Manager\UserNavigation::LIST_KEY, + ]); + } + return $affected; + } +} diff --git a/bin/migrate-user-nav-items b/bin/migrate-user-nav-items new file mode 100755 index 000000000..efc78fc54 --- /dev/null +++ b/bin/migrate-user-nav-items @@ -0,0 +1,24 @@ +#!/usr/bin/env php +prepared_query(" + SELECT UserID, NavItems FROM users_info WHERE NavItems != ''; +"); + +$manager = new Gazelle\Manager\User; + +$n = 0; +foreach ($db->to_array(false, MYSQLI_NUM, false) as [$id, $items]) { + $user = $manager->findById($id); + if (is_null($user)) { + continue; + } + $user->setField('nav_list', array_map('intval', explode(',', $items)))->modify(); + ++$n; +} + +echo "migrated $n users\n"; diff --git a/classes/view.class.php b/classes/view.class.php index 9bde44c48..f59374df7 100644 --- a/classes/view.class.php +++ b/classes/view.class.php @@ -91,7 +91,7 @@ public static function header(string $pageTitle, array $option = []): string { $PageID = [$Document, $_REQUEST['action'] ?? false, $_REQUEST['type'] ?? false]; $navLinks = []; - foreach ((new Gazelle\Manager\User)->userNavList($Viewer) as $n) { + foreach ((new Gazelle\Manager\UserNavigation)->userControlList($Viewer) as $n) { [$ID, $Key, $Title, $Target, $Tests, $TestUser, $Mandatory] = array_values($n); if (str_contains($Tests, ':')) { $testList = []; diff --git a/misc/phinx/migrations/20240129000000_user_navigation_json.php b/misc/phinx/migrations/20240129000000_user_navigation_json.php new file mode 100644 index 000000000..9a06937dc --- /dev/null +++ b/misc/phinx/migrations/20240129000000_user_navigation_json.php @@ -0,0 +1,12 @@ +table('users_main') + ->addColumn('nav_list', 'json', ['null' => true]) + ->save(); + } +} diff --git a/sections/tools/managers/navigation_alter.php b/sections/tools/managers/navigation_alter.php index 2f31fae97..a1a77e3e9 100644 --- a/sections/tools/managers/navigation_alter.php +++ b/sections/tools/managers/navigation_alter.php @@ -8,59 +8,57 @@ authorize(); -$P = array_map('trim', $_POST); -$db = Gazelle\DB::DB(); +$manager = new Gazelle\Manager\UserNavigation; if ($_POST['submit'] == 'Delete') { - if (!is_number($_POST['id']) || $_POST['id'] == '') { - error(0); + $id = (int)($_POST['id'] ?? 0); + $control = $manager->findById($id); + if (is_null($control)) { + error(404); } - - $db->prepared_query("DELETE FROM nav_items WHERE id = ?", $P['id']); + $control->remove(); } else { - $Val = new Gazelle\Util\Validator; - $Val->setFields([ - ['tag', true, 'string', 'The key must be set, and has a max length of 20 characters', ['maxlength' => 20]], - ['title', true, 'string', 'The title must be set, and has a max length of 50 characters', ['maxlength' => 50]], - ['target', true, 'string', 'The target must be set, and has a max length of 200 characters', ['maxlength' => 200]], - ['tests', false, 'string', 'The tests are optional, and have a max length of 200 characters', ['maxlength' => 200]], - ['testuser', true, 'checkbox', ''], + $validator = new Gazelle\Util\Validator; + $validator->setFields([ + ['tag', true, 'string', 'The key must be set, and has a max length of 20 characters', ['maxlength' => 20]], + ['title', true, 'string', 'The title must be set, and has a max length of 50 characters', ['maxlength' => 50]], + ['target', true, 'string', 'The target must be set, and has a max length of 200 characters', ['maxlength' => 200]], + ['tests', false, 'string', 'The tests are optional, and have a max length of 200 characters', ['maxlength' => 200]], + ['testuser', true, 'checkbox', ''], ['mandatory', true, 'checkbox', ''], - ['default', true, 'checkbox', ''], + ['default', true, 'checkbox', ''], ]); - if (!$Val->validate($_POST)) { - error($Val->errorMessage()); + if (!$validator->validate($_POST)) { + error($validator->errorMessage()); } if ($_POST['submit'] == 'Create') { - $db->prepared_query(" - INSERT INTO nav_items (tag, title, target, tests, test_user, mandatory, initial) - VALUES (?, ?, ?, ?, ?, ?, ?)", - $P['tag'], $P['title'], $P['target'], $P['tests'], - $P['testuser'] == 'on' ? 1 : 0, $P['mandatory'] == 'on' ? 1 : 0, - $P['default'] == 'on' ? 1 : 0 + $control = $manager->create( + trim($_POST['tag']), + trim($_POST['title']), + trim($_POST['target']), + trim($_POST['tests']), + $_POST['testuser'] == 'on', + $_POST['mandatory'] == 'on', + $_POST['default'] == 'on', ); } elseif ($_POST['submit'] == 'Edit') { - if (!is_number($_POST['id']) || $_POST['id'] == '') { - error(0); + $id = (int)($_POST['id'] ?? 0); + $control = $manager->findById($id); + if (is_null($control)) { + error(404); } - - $db->prepared_query(" - UPDATE nav_items - SET tag = ?, - title = ?, - target = ?, - tests = ?, - test_user = ?, - mandatory = ?, - initial = ? - WHERE id = ?", - $P['tag'], $P['title'], $P['target'], $P['tests'], - $P['testuser'] == 'on' ? 1 : 0, $P['mandatory'] == 'on' ? 1 : 0, - $P['default'] == 'on' ? 1 : 0, $P['id'] - ); + $control->setField('tag', trim($_POST['tag'])) + ->setField('target', trim($_POST['target'])) + ->setField('tests', trim($_POST['tests'])) + ->setField('title', trim($_POST['title'])) + ->setField('test_user', $_POST['testuser'] == 'on') + ->setField('mandatory', $_POST['mandatory'] == 'on') + ->setField('initial', $_POST['default'] == 'on') + ->modify(); + } else { + error(0); } } -$Cache->delete_value('nav_items'); -header('Location: tools.php?action=navigation'); +header("Location: {$control->location()}"); diff --git a/sections/tools/managers/navigation_list.php b/sections/tools/managers/navigation_list.php index 44a914fe0..4f49322a5 100644 --- a/sections/tools/managers/navigation_list.php +++ b/sections/tools/managers/navigation_list.php @@ -4,7 +4,7 @@ error(403); } -echo $Twig->render('admin/forum-navigation.twig', [ +echo $Twig->render('admin/user-navigation.twig', [ 'auth' => $Viewer->auth(), - 'list' => (new Gazelle\Manager\User)->userNavFullList(), + 'list' => (new Gazelle\Manager\UserNavigation)->fullList(), ]); diff --git a/sections/user/edit.php b/sections/user/edit.php index 75d91da9a..a646b5790 100644 --- a/sections/user/edit.php +++ b/sections/user/edit.php @@ -24,14 +24,14 @@ ]; } } -$navItems = $userMan->userNavFullList(); +$navList = (new Gazelle\Manager\UserNavigation)->fullList(); echo $Twig->render('user/setting.twig', [ 'donor' => $donor, 'js' => (new Gazelle\Util\Validator)->generateJS('userform'), 'lastfm_username' => (new Gazelle\Util\LastFM)->username($user), - 'nav_items' => $navItems, - 'nav_items_user' => $user->navigationList() ?: array_keys(array_filter($navItems, fn($item) => $item['initial'])), + 'nav_items' => $navList, + 'nav_items_user' => $user->navigationList(), 'notify_config' => (new Gazelle\User\Notification($user))->config(), 'profile' => $profile, 'release_order' => $user->releaseOrder((new Gazelle\ReleaseType)->extendedList()), diff --git a/sections/user/edit_handle.php b/sections/user/edit_handle.php index 62bc147ac..9b6cbc143 100644 --- a/sections/user/edit_handle.php +++ b/sections/user/edit_handle.php @@ -195,13 +195,13 @@ } $user->setField('option_list', $option); -$UserNavItems = []; -foreach ($userMan->userNavFullList() as $n) { - if ($n['mandatory'] || (!empty($_POST["n_{$n['id']}"]) && $_POST["n_{$n['id']}"] == 'on')) { - $UserNavItems[] = $n['id']; +$navList = []; +foreach ((new Gazelle\Manager\UserNavigation)->fullList() as $n) { + if ($n['mandatory'] || isset($_POST["n_{$n['id']}"])) { + $navList[] = (int)$n['id']; } } -$user->setField('nav_list', $UserNavItems); +$user->setField('nav_list', $navList); (new Gazelle\Util\LastFM)->modifyUsername($user, trim($_POST['lastfm_username'] ?? '')); diff --git a/templates/admin/forum-navigation.twig b/templates/admin/user-navigation.twig similarity index 100% rename from templates/admin/forum-navigation.twig rename to templates/admin/user-navigation.twig diff --git a/tests/phpunit/UserNavigationTest.php b/tests/phpunit/UserNavigationTest.php new file mode 100644 index 000000000..36ecb7059 --- /dev/null +++ b/tests/phpunit/UserNavigationTest.php @@ -0,0 +1,32 @@ +user = Helper::makeUser('user.' . randomString(10), 'forum'); + } + + public function tearDown(): void { + $this->user->remove(); + } + + public function testNavigationBasic(): void { + $manager = new Gazelle\Manager\UserNavigation; + $fullList = $manager->fullList(); + $this->assertCount(12, $fullList, 'user-nav-manager-full'); + + $this->assertTrue( + $this->user->setField('nav_list', [$fullList[3]["id"], $fullList[2]["id"], $fullList[1]["id"]])->modify(), + 'user-nav-modify' + ); + $userList = $manager->userControlList($this->user); + $this->assertCount(3, $userList, 'user-nav-count'); + $this->assertEquals($fullList[2]["id"], $userList[1]["id"], 'user-nav-order'); + } +}