forked from fa/breadcrumb-the-shire
feat: extract bookmarks as standalone module with MintyPHP\Module\Bookmarks namespace
Moves bookmarks from core hardcoding into modules/bookmarks/ as a
fully self-contained module, following the same pattern as addressbook.
Module contributions via platform slots:
- aside.tab_panel: bookmark sidebar panel
- topbar.right_item: bookmark toggle button
- layout.body_end_template: bookmark/group dialogs
- layout.head_style: bookmark CSS (form + sidebar)
- runtime.component: bookmark-save and bookmark-panel (phase: late)
- search.resource_item: bookmarks in global search (user-scoped via {{userId}})
Backend fully in module namespace (MintyPHP\Module\Bookmarks\*):
- Service: BookmarkService, BookmarkServicesFactory, BookmarkRepositoryFactory
- Repository: BookmarkRepository, BookmarkGroupRepository, BookmarkNavigationRepository
- Support: BookmarkUrlNormalizer
- Providers: BookmarksSessionProvider, BookmarksLayoutProvider, BookmarksSearchProvider
Core cleanup:
- Removed all bookmark-specific markup from core templates
- Removed core DI registrations for bookmark services
- Removed core bookmark pages, JS, CSS
- AuthSessionTenantContextService delegates to module SessionProvider
Architecture guards:
- NoBookmarksHardcodingTest: verifies zero bookmark references in core
- testModuleClassesUseModuleNamespace: prevents core namespace leakage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Bookmarks\Support;
|
||||
|
||||
use MintyPHP\Module\Bookmarks\Support\BookmarkUrlNormalizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BookmarkUrlNormalizerTest extends TestCase
|
||||
{
|
||||
public function testCanonicalizeRelativeNormalizesPathAndQueryOrder(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRelative('/admin//users/?b=2&a=1');
|
||||
|
||||
$this->assertSame('admin/users?a=1&b=2', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeSortsMultiValueQueryKeys(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRelative('admin/users?roles=2&roles=1&search=max');
|
||||
|
||||
$this->assertSame('admin/users?roles=1&roles=2&search=max', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsAbsoluteUrl(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRelative('https://example.com/admin/users');
|
||||
|
||||
$this->assertSame('', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRemovesFragment(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRelative('admin/users?a=1#section-2');
|
||||
|
||||
$this->assertSame('admin/users?a=1', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRequestUriStripsLocaleBaseAndSortsQuery(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRequestUri('/de/admin/users?b=2&a=1', '/de/');
|
||||
|
||||
$this->assertSame('admin/users?a=1&b=2', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRequestUriHandlesAppBaseAndLocale(): void
|
||||
{
|
||||
$actual = BookmarkUrlNormalizer::canonicalizeRequestUri('/core/de/address-book?tenants=b&tenants=a', '/core/de/');
|
||||
|
||||
$this->assertSame('address-book?tenants=a&tenants=b', $actual);
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsDataUri(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('data:text/html,<script>alert(1)</script>'));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsJavaScriptUri(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('javascript:alert(1)'));
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('JavaScript:alert(document.cookie)'));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsFileUri(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('file:///etc/passwd'));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsVbScriptUri(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('vbscript:MsgBox("xss")'));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsNullBytes(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative("admin/users\x00javascript:alert(1)"));
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative("\x00"));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeRejectsProtocolRelativeUrl(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('//evil.com/payload'));
|
||||
}
|
||||
|
||||
public function testCanonicalizeRelativeHandlesDeepPathTraversal(): void
|
||||
{
|
||||
$this->assertSame('', BookmarkUrlNormalizer::canonicalizeRelative('admin/users/../../../../etc/passwd'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user