From 7f86de91468ae1cb718088c422e47c75c2485bc4 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Thu, 9 May 2024 19:16:43 +0200 Subject: [PATCH] refactor: moved search statistics page to Twig (#2791) --- phpmyfaq/admin/assets/src/api/statistics.js | 21 +++ phpmyfaq/admin/assets/src/index.js | 2 + .../admin/assets/src/statistics/search.js | 41 +++++ phpmyfaq/admin/index.php | 4 +- phpmyfaq/admin/stat.search.php | 163 ------------------ ...tat.ratings.php => statistics.ratings.php} | 0 phpmyfaq/admin/statistics.search.php | 97 +++++++++++ phpmyfaq/admin/statistics.show.php | 5 + .../templates/admin/statistics/search.twig | 54 ++++++ phpmyfaq/src/admin-routes.php | 8 + .../Administration/StatisticsController.php | 36 +++- .../Template/LanguageCodeTwigExtension.php | 47 +++++ 12 files changed, 305 insertions(+), 173 deletions(-) create mode 100644 phpmyfaq/admin/assets/src/statistics/search.js delete mode 100644 phpmyfaq/admin/stat.search.php rename phpmyfaq/admin/{stat.ratings.php => statistics.ratings.php} (100%) create mode 100644 phpmyfaq/admin/statistics.search.php create mode 100644 phpmyfaq/assets/templates/admin/statistics/search.twig create mode 100644 phpmyfaq/src/phpMyFAQ/Template/LanguageCodeTwigExtension.php diff --git a/phpmyfaq/admin/assets/src/api/statistics.js b/phpmyfaq/admin/assets/src/api/statistics.js index 2a6463a6fc..b14657f932 100644 --- a/phpmyfaq/admin/assets/src/api/statistics.js +++ b/phpmyfaq/admin/assets/src/api/statistics.js @@ -33,3 +33,24 @@ export const deleteAdminLog = async (csrfToken) => { console.error(error); } }; + +export const truncateSearchTerms = async (csrfToken) => { + try { + const response = await fetch(`./api/statistics/search-terms`, { + method: 'DELETE', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + csrfToken: csrfToken, + }), + redirect: 'follow', + referrerPolicy: 'no-referrer', + }); + + return await response.json(); + } catch (error) { + console.error(error); + } +}; diff --git a/phpmyfaq/admin/assets/src/index.js b/phpmyfaq/admin/assets/src/index.js index 95f8aa0067..0eeaa44dd9 100644 --- a/phpmyfaq/admin/assets/src/index.js +++ b/phpmyfaq/admin/assets/src/index.js @@ -53,6 +53,7 @@ import { handleUserList, handleUsers } from './user'; import { handleGroups } from './group'; import { handlePasswordStrength, handlePasswordToggle } from '../../../assets/src/utils'; import { sidebarToggle } from './utils'; +import { handleTruncateSearchTerms } from './statistics/search'; document.addEventListener('DOMContentLoaded', async () => { 'use strict'; @@ -114,6 +115,7 @@ document.addEventListener('DOMContentLoaded', async () => { handleDeleteAdminLog(); handleStatistics(); handleCreateReport(); + handleTruncateSearchTerms(); // Configuration → FAQ configuration await handleConfiguration(); diff --git a/phpmyfaq/admin/assets/src/statistics/search.js b/phpmyfaq/admin/assets/src/statistics/search.js new file mode 100644 index 0000000000..cf7bd88e40 --- /dev/null +++ b/phpmyfaq/admin/assets/src/statistics/search.js @@ -0,0 +1,41 @@ +/** + * Search Term Handling + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at https://mozilla.org/MPL/2.0/. + * + * @package phpMyFAQ + * @author Jan Harms + * @copyright 2024 phpMyFAQ Team + * @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2024-05-09 + */ + +import { truncateSearchTerms } from '../api'; +import { pushErrorNotification, pushNotification } from '../utils'; + +export const handleTruncateSearchTerms = () => { + const buttonTruncateSearchTerms = document.getElementById('pmf-button-truncate-search-terms'); + + if (buttonTruncateSearchTerms) { + buttonTruncateSearchTerms.addEventListener('click', async (event) => { + event.preventDefault(); + + const csrf = event.target.getAttribute('data-pmf-csrf-token'); + + if (confirm('Are you sure?')) { + const response = await truncateSearchTerms(csrf); + + if (response.success) { + const tableToDelete = document.getElementById('pmf-table-search-terms'); + tableToDelete.remove(); + pushNotification(response.success); + } else { + pushErrorNotification(response.error); + } + } + }); + } +}; diff --git a/phpmyfaq/admin/index.php b/phpmyfaq/admin/index.php index d7bc4dd0dd..18bf2d79a2 100755 --- a/phpmyfaq/admin/index.php +++ b/phpmyfaq/admin/index.php @@ -326,11 +326,11 @@ break; case 'clear-statistics': case 'statistics': - require 'stat.ratings.php'; + require 'statistics.ratings.php'; break; case 'truncatesearchterms': case 'searchstats': - require 'stat.search.php'; + require 'statistics.search.php'; break; // Reports case 'reports': diff --git a/phpmyfaq/admin/stat.search.php b/phpmyfaq/admin/stat.search.php deleted file mode 100644 index 9f410b5548..0000000000 --- a/phpmyfaq/admin/stat.search.php +++ /dev/null @@ -1,163 +0,0 @@ - - * @author Thorsten Rinne - * @copyright 2003-2024 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2003-03-30 - */ - -use phpMyFAQ\Component\Alert; -use phpMyFAQ\Enums\PermissionType; -use phpMyFAQ\Filter; -use phpMyFAQ\Language\LanguageCodes; -use phpMyFAQ\Pagination; -use phpMyFAQ\Search; -use phpMyFAQ\Session\Token; -use phpMyFAQ\Strings; -use phpMyFAQ\Translation; -use Symfony\Component\HttpFoundation\Request; - -if (!defined('IS_VALID_PHPMYFAQ')) { - http_response_code(400); - exit(); -} - -$request = Request::createFromGlobals(); - -?> -
-

- - -

-
-
- - - -
-
-
- -
-
-perm->hasPermission($user->getUserId(), PermissionType::STATISTICS_VIEWLOGS->value)) { - $perPage = 10; - $pages = Filter::filterVar($request->query->get('pages'), FILTER_VALIDATE_INT); - $page = Filter::filterVar($request->query->get('page'), FILTER_VALIDATE_INT, 1); - $csrfToken = Filter::filterVar($request->query->get('csrf'), FILTER_SANITIZE_SPECIAL_CHARS); - - $search = new Search($faqConfig); - - if ( - $csrfToken && - Token::getInstance()->verifyToken('truncate-seaerchterms', $csrfToken) && - 'truncatesearchterms' === $action - ) { - if ($search->deleteAllSearchTerms()) { - echo Alert::success('ad_searchterm_del_suc'); - } else { - echo Alert::danger('ad_searchterm_del_err'); - } - } - - $searchesCount = $search->getSearchesCount(); - $searchesList = $search->getMostPopularSearches($searchesCount + 1, true); - - if (is_null($pages)) { - $pages = round(((is_countable($searchesList) ? count($searchesList) : 0) + ($perPage / 3)) / $perPage, 0); - } - - $start = ($page - 1) * $perPage; - $end = $start + $perPage; - - $baseUrl = sprintf( - '%sadmin/?action=searchstats&page=%d', - $faqConfig->getDefaultUrl(), - $page - ); - - // Pagination options - $options = [ - 'baseUrl' => $request->getUri(), - 'total' => is_countable($searchesList) ? count($searchesList) : 0, - 'perPage' => $perPage, - 'pageParamName' => 'page', - ]; - $pagination = new Pagination($options); - ?> -
- - - - - - - - - - - - - - - - - = $perPage) { - ++$displayedCounter; - continue; - } - - ++$counter; - if ($counter <= $start) { - continue; - } - ++$displayedCounter; - - $num = round(($searchItem['number'] * 100 / $searchesCount), 2); - $csrfToken = Token::getInstance()->getTokenString('delete-searchterm'); - ?> - - - - - - - - - - -
 
render() ?>
% - -
- -
-
diff --git a/phpmyfaq/admin/stat.ratings.php b/phpmyfaq/admin/statistics.ratings.php similarity index 100% rename from phpmyfaq/admin/stat.ratings.php rename to phpmyfaq/admin/statistics.ratings.php diff --git a/phpmyfaq/admin/statistics.search.php b/phpmyfaq/admin/statistics.search.php new file mode 100644 index 0000000000..96a9707f14 --- /dev/null +++ b/phpmyfaq/admin/statistics.search.php @@ -0,0 +1,97 @@ + + * @author Thorsten Rinne + * @copyright 2003-2024 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2003-03-30 + */ + +use phpMyFAQ\Configuration; +use phpMyFAQ\Enums\PermissionType; +use phpMyFAQ\Filter; +use phpMyFAQ\Pagination; +use phpMyFAQ\Search; +use phpMyFAQ\Session\Token; +use phpMyFAQ\Template\LanguageCodeTwigExtension; +use phpMyFAQ\Template\TwigWrapper; +use phpMyFAQ\Translation; +use phpMyFAQ\User\CurrentUser; +use Symfony\Component\HttpFoundation\Request; +use Twig\Extension\DebugExtension; + +if (!defined('IS_VALID_PHPMYFAQ')) { + http_response_code(400); + exit(); +} + +$faqConfig = Configuration::getConfigurationInstance(); +$user = CurrentUser::getCurrentUser($faqConfig); + +$request = Request::createFromGlobals(); + +if ($user->perm->hasPermission($user->getUserId(), PermissionType::STATISTICS_VIEWLOGS->value)) { + $perPage = 10; + $pages = Filter::filterVar($request->query->get('pages'), FILTER_VALIDATE_INT); + $page = Filter::filterVar($request->query->get('page'), FILTER_VALIDATE_INT, 1); + + $search = new Search($faqConfig); + + $twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates'); + $twig->addExtension(new DebugExtension()); + $twig->addExtension(new LanguageCodeTwigExtension()); + $template = $twig->loadTemplate('./admin/statistics/search.twig'); + + $searchesCount = $search->getSearchesCount(); + $searchesList = $search->getMostPopularSearches($searchesCount + 1, true); + + if (is_null($pages)) { + $pages = round(((is_countable($searchesList) ? count($searchesList) : 0) + ($perPage / 3)) / $perPage, 0); + } + + $start = ($page - 1) * $perPage; + $end = $start + $perPage; + + $baseUrl = sprintf( + '%sadmin/?action=searchstats&page=%d', + $faqConfig->getDefaultUrl(), + $page + ); + + // Pagination options + $options = [ + 'baseUrl' => $request->getUri(), + 'total' => is_countable($searchesList) ? count($searchesList) : 0, + 'perPage' => $perPage, + 'pageParamName' => 'page', + ]; + $pagination = new Pagination($options); + + $templateVars = [ + 'ad_menu_searchstats' => Translation::get('ad_menu_searchstats'), + 'csrfToken' => Token::getInstance()->getTokenString('truncate-search-terms'), + 'ad_searchterm_del' => Translation::get('ad_searchterm_del'), + 'ad_searchstats_search_term' => Translation::get('ad_searchstats_search_term'), + 'ad_searchstats_search_term_count' => Translation::get('ad_searchstats_search_term_count'), + 'ad_searchstats_search_term_lang' => Translation::get('ad_searchstats_search_term_lang'), + 'ad_searchstats_search_term_percentage' => Translation::get('ad_searchstats_search_term_percentage'), + 'pagination' => $pagination->render(), + 'searchesCount' => $searchesCount, + 'searchesList' => $searchesList, + 'csrfTokenDelete' => Token::getInstance()->getTokenString('delete-searchterm'), + 'ad_news_delete' => Translation::get('ad_news_delete'), + ]; + + echo $template->render($templateVars); +} else { + require __DIR__ . '/no-permission.php'; +} diff --git a/phpmyfaq/admin/statistics.show.php b/phpmyfaq/admin/statistics.show.php index 7f41b48e7b..b5202808ec 100644 --- a/phpmyfaq/admin/statistics.show.php +++ b/phpmyfaq/admin/statistics.show.php @@ -15,11 +15,13 @@ * @since 2003-02-24 */ +use phpMyFAQ\Configuration; use phpMyFAQ\Enums\PermissionType; use phpMyFAQ\Filter; use phpMyFAQ\Session; use phpMyFAQ\Template\TwigWrapper; use phpMyFAQ\Translation; +use phpMyFAQ\User\CurrentUser; use Twig\Extension\DebugExtension; if (!defined('IS_VALID_PHPMYFAQ')) { @@ -27,6 +29,9 @@ exit(); } +$faqConfig = Configuration::getConfigurationInstance(); +$user = CurrentUser::getCurrentUser($faqConfig); + $sessionId = Filter::filterInput(INPUT_GET, 'id', FILTER_VALIDATE_INT); if ($user->perm->hasPermission($user->getUserId(), PermissionType::STATISTICS_VIEWLOGS->value)) { diff --git a/phpmyfaq/assets/templates/admin/statistics/search.twig b/phpmyfaq/assets/templates/admin/statistics/search.twig new file mode 100644 index 0000000000..b7f32bba81 --- /dev/null +++ b/phpmyfaq/assets/templates/admin/statistics/search.twig @@ -0,0 +1,54 @@ +
+

+ + {{ ad_menu_searchstats }} +

+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + {% for searchItem in searchesList %} + {% set num = searchItem.number / searchesCount * 100 %} + + + + + + + + + {% endfor %} + +
{{ ad_searchstats_search_term }}{{ ad_searchstats_search_term_count }}{{ ad_searchstats_search_term_lang }}{{ ad_searchstats_search_term_percentage }} 
{{ pagination | raw }}
{{ searchItem.searchterm }}{{ searchItem.number }}{{ searchItem.lang | getFromLanguageCode }}{{ num }}% + +
+
+
diff --git a/phpmyfaq/src/admin-routes.php b/phpmyfaq/src/admin-routes.php index 71b5e90214..5fb4764874 100644 --- a/phpmyfaq/src/admin-routes.php +++ b/phpmyfaq/src/admin-routes.php @@ -608,6 +608,14 @@ ) ); +$routes->add( + 'admin.api.statistics.search-terms.truncate', + new Route( + '/statistics/search-terms', + ['_controller' => [StatisticsController::class, 'truncateSearchTerms'], '_methods' => 'DELETE'] + ) +); + // // Forms API // diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/StatisticsController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/StatisticsController.php index 1243595dfe..334d825a79 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/StatisticsController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/StatisticsController.php @@ -21,6 +21,8 @@ use phpMyFAQ\Administration\AdminLog; use phpMyFAQ\Controller\AbstractController; use phpMyFAQ\Core\Exception; +use phpMyFAQ\Enums\PermissionType; +use phpMyFAQ\Search; use phpMyFAQ\Session\Token; use phpMyFAQ\Translation; use Symfony\Component\HttpFoundation\JsonResponse; @@ -33,12 +35,10 @@ class StatisticsController extends AbstractController /** * @throws Exception|JsonException */ - #[Route('./admin/api/statistics/admin-log')] + #[Route('./admin/api/statistics/admin-log', methods: ['DELETE'])] public function deleteAdminLog(Request $request): JsonResponse { - $this->userIsAuthenticated(); - - $logging = new AdminLog($this->configuration); + $this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS); $data = json_decode($request->getContent(), false, 512, JSON_THROW_ON_ERROR); @@ -46,13 +46,33 @@ public function deleteAdminLog(Request $request): JsonResponse return $this->json(['error' => Translation::get('err_NotAuth')], Response::HTTP_UNAUTHORIZED); } + $logging = new AdminLog($this->configuration); if ($logging->delete()) { - return $this->json( - ['success' => Translation::get('ad_adminlog_delete_success')], - Response::HTTP_OK - ); + return $this->json(['success' => Translation::get('ad_adminlog_delete_success')], Response::HTTP_OK); } return $this->json(['error' => Translation::get('ad_adminlog_delete_failure')], Response::HTTP_BAD_REQUEST); } + + /** + * @throws Exception|JsonException + */ + #[Route('./admin/api/statistics/search-terms', methods: ['DELETE'])] + public function truncateSearchTerms(Request $request): JsonResponse + { + $this->userHasPermission(PermissionType::STATISTICS_VIEWLOGS); + + $data = json_decode($request->getContent(), false, 512, JSON_THROW_ON_ERROR); + + if (!Token::getInstance()->verifyToken('truncate-search-terms', $data->csrfToken)) { + return $this->json(['error' => Translation::get('err_NotAuth')], Response::HTTP_UNAUTHORIZED); + } + + $search = new Search($this->configuration); + if ($search->deleteAllSearchTerms()) { + return $this->json(['success' => Translation::get('ad_searchterm_del_suc')], Response::HTTP_OK); + } + + return $this->json(['error' => Translation::get('ad_searchterm_del_err')], Response::HTTP_BAD_REQUEST); + } } diff --git a/phpmyfaq/src/phpMyFAQ/Template/LanguageCodeTwigExtension.php b/phpmyfaq/src/phpMyFAQ/Template/LanguageCodeTwigExtension.php new file mode 100644 index 0000000000..3df4d53ece --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Template/LanguageCodeTwigExtension.php @@ -0,0 +1,47 @@ + + * @copyright 2024 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2024-05-09 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Template; + +use phpMyFAQ\Language\LanguageCodes; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; +use Twig\TwigFilter; + +class LanguageCodeTwigExtension extends AbstractExtension +{ + public function getFunctions(): array + { + return [ + new TwigFunction('getFromLanguageCode', $this->getFromLanguageCode(...)), + ]; + } + + public function getFilters(): array + { + return [ + new TwigFilter('getFromLanguageCode', $this->getFromLanguageCode(...)), + ]; + } + + public function getFromLanguageCode(string $languageCode): string + { + return LanguageCodes::get($languageCode); + } +}