From c2c6c1878c5e8eff4ec8603928fffb934a490c9c Mon Sep 17 00:00:00 2001 From: Darto KLoning <1+dartokloning@noreply.git.darto.id> Date: Tue, 15 Sep 2026 04:58:49 +0700 Subject: [PATCH] Upload files to "/" --- CleanUserUrlsPlugin.php | 319 ++++++++++++++++++++++++++++++++++++++++ README.md | 95 ++++++++++++ index.php | 8 + locale.po | 13 ++ version.xml | 10 ++ 5 files changed, 445 insertions(+) create mode 100644 CleanUserUrlsPlugin.php create mode 100644 README.md create mode 100644 index.php create mode 100644 locale.po create mode 100644 version.xml diff --git a/CleanUserUrlsPlugin.php b/CleanUserUrlsPlugin.php new file mode 100644 index 0000000..8cab077 --- /dev/null +++ b/CleanUserUrlsPlugin.php @@ -0,0 +1,319 @@ +getEnabled($mainContextId)) { + Hook::add('NavigationMenus::displaySettings', $this->rewriteAccountMenuUrl(...)); + Hook::add('TemplateManager::display', $this->registerOutputFilter(...)); + } + + return $success; + } + + public function getDisplayName() + { + return __('plugins.generic.cleanUserUrls.displayName'); + } + + public function getDescription() + { + return __('plugins.generic.cleanUserUrls.description'); + } + + /** + * This plugin is intended to be enabled from Site Settings and affect only + * the site-wide context. Journal-context navigation is left untouched. + */ + public function isSitePlugin(): bool + { + return true; + } + + + + /** + * Register one conservative output filter for site-level pages. + * + * It only changes: + * 1. the public site-name link /index//index -> / + * 2. the exact Administration breadcrumb link /index/id/admin[/index] -> /admin + * + * Child admin routes such as /index/id/admin/settings are deliberately + * left untouched. + */ + public function registerOutputFilter(string $hookName, array $args): bool + { + $templateMgr = $args[0] ?? null; + $request = Application::get()->getRequest(); + + if (!$templateMgr instanceof PKPTemplateManager || $request->getContext()) { + return false; + } + + $templateMgr->registerFilter('output', $this->rewriteSafeSiteLinks(...)); + + // The backend user menu is rendered by Vue after the server-side + // Smarty output filter has finished. Add a tiny, site-level backend + // helper that updates only the generated Edit Profile and Log Out links. + $baseUrl = rtrim($request->getBaseUrl(), '/'); + + $legacyProfileUrl = $baseUrl . '/index/id/user/profile'; + $cleanProfileUrl = $baseUrl . '/user/profile'; + + $legacyLogoutUrl = $baseUrl . '/index/id/login/signOut'; + $cleanLogoutUrl = $baseUrl . '/logout'; + + $script = <<<'JS' +(function () { + const replacements = new Map([ + [__LEGACY_PROFILE_URL__, __CLEAN_PROFILE_URL__], + [__LEGACY_LOGOUT_URL__, __CLEAN_LOGOUT_URL__], + ]); + + function cleanBackendUserLinks(root) { + const scope = root && root.querySelectorAll ? root : document; + + scope.querySelectorAll('a[href]').forEach(function (link) { + const replacement = replacements.get(link.href); + if (replacement) { + link.href = replacement; + } + }); + } + + function start() { + cleanBackendUserLinks(document); + + const observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if (node.nodeType !== Node.ELEMENT_NODE) { + return; + } + + if (node.matches && node.matches('a[href]')) { + const replacement = replacements.get(node.href); + if (replacement) { + node.href = replacement; + } + } + + cleanBackendUserLinks(node); + }); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', start, { once: true }); + } else { + start(); + } +})(); +JS; + + $script = str_replace( + [ + '__LEGACY_PROFILE_URL__', + '__CLEAN_PROFILE_URL__', + '__LEGACY_LOGOUT_URL__', + '__CLEAN_LOGOUT_URL__', + ], + [ + json_encode($legacyProfileUrl), + json_encode($cleanProfileUrl), + json_encode($legacyLogoutUrl), + json_encode($cleanLogoutUrl), + ], + $script + ); + + $templateMgr->addJavaScript( + 'cleanUserUrlsBackendUserMenu', + $script, + [ + 'contexts' => ['backend'], + 'inline' => true, + ] + ); + + return false; + } + + /** + * Rewrite known-safe site-level links. + * + * Normal site-level Administration page URLs are cleaned: + * /index/id/admin -> /admin + * /index/id/admin/index -> /admin + * /index/id/admin/contexts -> /admin/contexts + * /index/id/admin/settings -> /admin/settings + * /index/id/admin/systemInfo -> /admin/systemInfo + * /index/id/admin/jobs -> /admin/jobs + * /index/id/admin/failedJobs -> /admin/failedJobs + * + * AJAX/component ($$$call$$$), API, journal-context and other sensitive + * routes are deliberately left untouched. + */ + public function rewriteSafeSiteLinks(string $output, $template): string + { + $request = Application::get()->getRequest(); + + if ($request->getContext()) { + return $output; + } + + $baseUrl = rtrim($request->getBaseUrl(), '/'); + $escapedBase = preg_quote($baseUrl, '~'); + + // Public site-name link: /index//index -> / + $rootUrl = htmlspecialchars($baseUrl . '/', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $siteNamePattern = + '~(]*>\s*]*href=["\'])' . + $escapedBase . + '/index/[^/"\']+/index/?(["\'])~is'; + + $output = preg_replace_callback( + $siteNamePattern, + static fn (array $matches): string => $matches[1] . $rootUrl . $matches[2], + $output, + 1 + ) ?? $output; + + // Clean ordinary site-level user links/actions in the + // Indonesian/default locale. + // + // Examples: + // /index/id/user/profile -> /user/profile + // /index/id/user/register -> /user/register + // + // Component/AJAX routes are not matched by this pattern. + $userPattern = + '~\b(href|action)=(["\'])' . + $escapedBase . + '/index/id/user(?:/([^"\'?#]*))?([^"\']*)\2~i'; + + $output = preg_replace_callback( + $userPattern, + static function (array $matches) use ($baseUrl): string { + $attribute = $matches[1]; + $quote = $matches[2]; + $tail = isset($matches[3]) ? trim($matches[3], '/') : ''; + $suffix = $matches[4] ?? ''; + + $clean = $baseUrl . '/user'; + if ($tail !== '' && $tail !== 'index') { + $clean .= '/' . $tail; + } + + return $attribute . '=' . $quote . $clean . $suffix . $quote; + }, + $output + ) ?? $output; + + // Clean ordinary site-level Administration links/actions in the + // Indonesian/default locale. Query strings/fragments are preserved. + $adminPattern = + '~\b(href|action)=(["\'])' . + $escapedBase . + '/index/id/admin(?:/([^"\'?#]*))?([^"\']*)\2~i'; + + $output = preg_replace_callback( + $adminPattern, + static function (array $matches) use ($baseUrl): string { + $attribute = $matches[1]; + $quote = $matches[2]; + $tail = isset($matches[3]) ? trim($matches[3], '/') : ''; + $suffix = $matches[4] ?? ''; + + // /admin and /admin/index both become the clean admin root. + if ($tail === '' || $tail === 'index') { + $clean = $baseUrl . '/admin'; + } else { + $clean = $baseUrl . '/admin/' . $tail; + } + + return $attribute . '=' . $quote . $clean . $suffix . $quote; + }, + $output + ) ?? $output; + + return $output; + } + + /** + * Replace site-level account navigation URLs with clean aliases. + * + * @param string $hookName + * @param array $params [$navigationMenuItem, $navigationMenu] + */ + public function rewriteAccountMenuUrl($hookName, $params): bool + { + $navigationMenuItem = $params[0] ?? null; + if (!$navigationMenuItem instanceof NavigationMenuItem) { + return false; + } + + $request = Application::get()->getRequest(); + + // Do not alter account-menu routes while inside a journal context. + if ($request->getContext()) { + return false; + } + + $baseUrl = rtrim($request->getBaseUrl(), '/'); + + switch ($navigationMenuItem->getType()) { + case NavigationMenuItem::NMI_TYPE_USER_DASHBOARD: + $navigationMenuItem->setUrl($baseUrl . '/dashboard'); + break; + + case NavigationMenuItem::NMI_TYPE_USER_PROFILE: + $navigationMenuItem->setUrl($baseUrl . '/profile'); + break; + + case NavigationMenuItem::NMI_TYPE_ADMINISTRATION: + $navigationMenuItem->setUrl($baseUrl . '/admin'); + break; + + case NavigationMenuItem::NMI_TYPE_USER_LOGOUT: + // When a site administrator is impersonating another user, + // preserve OJS' special signOutAsUser URL and behavior. + if (!Validation::loggedInAs()) { + $navigationMenuItem->setUrl($baseUrl . '/logout'); + } + break; + } + + return false; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae1d677 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Clean User URLs for OJS 3.5 + +Targeted for the OJS 3.5.0-5 source tree supplied for testing. + +The plugin changes the **site-level** account menu links and also rewrites the site-name/header link to `/`: + +- Dashboard -> `/dashboard` +- Profile -> `/profile` +- Administration -> `/admin` +- Log Out -> `/logout` +- Site name/header -> `/` + +Journal-context navigation is intentionally not changed. + +## Required `.htaccess` aliases + +Place these rules **before** the generic OJS catch-all rewrite rule: + +```apache +RewriteRule ^dashboard/?$ index.php/index/id/index [L,QSA] +RewriteRule ^profile/?$ index.php/index/id/user/profile [L,QSA] +RewriteRule ^admin/?$ index.php/index/id/admin/index [L,QSA] +RewriteRule ^logout/?$ index.php/index/id/login/signOut [L,QSA] +``` + +Then keep the existing OJS rules, for example: + +```apache +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php/$1 [L,QSA] +``` + +If the configured site locale/path is changed from `id`, update the internal targets above accordingly. + +## Install + +Upload the ZIP from **Site Settings -> Plugins -> Upload A New Plugin**, then enable **Clean User URLs**. + +If the plugin cannot be uploaded from the UI, extract `cleanUserUrls` into `plugins/generic/` and enable it from the plugin list. + + +## 1.0.0.2 +Fix output-filter callback compatibility with Smarty in OJS 3.5.0-5. + + +## 1.0.0.4 +Adds a conservative clean URL for the exact site-level **Administration** breadcrumb/home link: + +- `/index/id/admin` -> `/admin` +- `/index/id/admin/index` -> `/admin` + +It deliberately does **not** rewrite child admin URLs such as `/index/id/admin/settings`, +component (`$$$call$$$`) routes, API routes, or AJAX endpoints. + + +## 1.0.0.5 +Fixes the Administration-link matching bug in v1.0.0.4 and cleans ordinary +site-level Administration URLs while leaving component/API/AJAX routes alone. + +Requires Apache aliases such as: +`/admin/contexts -> index.php/index/id/admin/contexts`. + + +## 1.0.0.6 +Adds clean ordinary site-level user URLs. + +Examples: +- `/index/id/user/profile` -> `/user/profile` +- `/index/id/user/register` -> `/user/register` + +Requires: +`RewriteRule ^user/(.*)$ index.php/index/id/user/$1 [L,QSA]` + +Component/AJAX (`$$$call$$$`) and API routes remain untouched. + + +## 1.0.0.7 +Fixes the backend **Edit Profile** link in OJS 3.5. + +The backend top navigation is rendered by Vue after Smarty output filters run, +so the plugin now adds a small backend-only MutationObserver that changes only: + +- `/index/id/user/profile` -> `/user/profile` + +No API, component (`$$$call$$$`), journal-context, or profile-tab routes are modified. + + +## 1.0.0.8 +Extends the backend Vue user-menu helper to clean both: + +- `/index/id/user/profile` -> `/user/profile` +- `/index/id/login/signOut` -> `/logout` + +No API, component (`$$$call$$$`), profile-tab, or journal-context routes are modified. diff --git a/index.php b/index.php new file mode 100644 index 0000000..06fe16a --- /dev/null +++ b/index.php @@ -0,0 +1,8 @@ + + + + cleanUserUrls + plugins.generic + 1.0.0.8 + 2026-08-16 + 1 + CleanUserUrlsPlugin +