Skip to content
7 changes: 7 additions & 0 deletions PersistentPageIdentifiers.alias.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
$specialPageAliases = [];

/** English (English) */
$specialPageAliases['en'] = [
'PersistentPageIdentifierResolver' => [ 'PersistentPageIdentifierResolver' ],
];
5 changes: 5 additions & 0 deletions extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@
}
],

"SpecialPages": {
"PersistentPageIdentifierResolver": "ProfessionalWiki\\PersistentPageIdentifiers\\EntryPoints\\SpecialPersistentPageIdentifierResolver"
},

"ExtensionMessagesFiles": {
"PersistentPageIdentifiersAlias": "PersistentPageIdentifiers.alias.php",
"PersistentPageIdentifiersMagic": "i18n/Magic/MagicWords.php"
},

Expand Down
4 changes: 3 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
},
"persistentpageidentifiers-name": "Persistent Page Identifiers",
"persistentpageidentifiers-description": "Stable unique identifiers for your wiki pages. UUID v7 or PURIs, accessible via parser function and REST API",
"persistentpageidentifiers-info-label": "Persistent page identifier"
"persistentpageidentifiers-info-label": "Persistent page identifier",
"persistentpageidentifierresolver": "Redirect by Persistent Page Identifier",
"persistentpageidentifierresolver-not-exists": "Persistent Page Identifier does not exist"
}
4 changes: 3 additions & 1 deletion i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
},
"persistentpageidentifiers-name": "{{Name}}",
"persistentpageidentifiers-description": "{{Desc|name=PersistentPageIdentifiers|url=https://github.com/ProfessionalWiki/PersistentPageIdentifiers}}",
"persistentpageidentifiers-info-label": "Persistent page identifier label on action=info page"
"persistentpageidentifiers-info-label": "Persistent page identifier label on action=info page",
"persistentpageidentifierresolver": "Special page name for redirect by Persistent Page Identifier",
"persistentpageidentifierresolver-not-exists": "Error message when Persistent Page Identifier does not exist"
}
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ parameters:
count: 1
path: src/EntryPoints/PersistentPageIdentifiersHooks.php

-
message: '#^Method ProfessionalWiki\\PersistentPageIdentifiers\\EntryPoints\\SpecialPersistentPageIdentifierResolver\:\:getFormFields\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/EntryPoints/SpecialPersistentPageIdentifierResolver.php

-
message: '#^Method ProfessionalWiki\\PersistentPageIdentifiers\\EntryPoints\\SpecialPersistentPageIdentifierResolver\:\:onSubmit\(\) has parameter \$data with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: src/EntryPoints/SpecialPersistentPageIdentifierResolver.php

-
message: '#^Method ProfessionalWiki\\PersistentPageIdentifiers\\PersistentPageIdentifiersExtension\:\:getDatabase\(\) should return Wikimedia\\Rdbms\\IDatabase but returns Wikimedia\\Rdbms\\IDatabase\|false\.$#'
identifier: return.type
Expand Down
10 changes: 10 additions & 0 deletions src/Adapters/DatabasePersistentPageIdentifiersRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,14 @@ private function persistentIdsResultToArray( IResultWrapper $result ): array {
return $rows;
}

public function getPageIdFromPersistentId( string $persistentId ): ?int {
$pageId = $this->database->newSelectQueryBuilder()
->select( 'page_id' )
->from( 'persistent_page_ids' )
->where( [ 'persistent_id' => $persistentId ] )
->fetchField();

return is_numeric( $pageId ) ? (int)$pageId : null;
}

}
4 changes: 4 additions & 0 deletions src/Adapters/StubPersistentPageIdentifiersRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public function getPersistentIds( array $pageIds ): array {
return array_combine( $pageIds, array_fill( 0, count( $pageIds ), $this->id ) );
}

public function getPageIdFromPersistentId( string $persistentId ): ?int {
return $this->id ? 1337 : null;
}

}
1 change: 1 addition & 0 deletions src/Application/PersistentPageIdentifiersRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ public function getPersistentId( int $pageId ): ?string;
*/
public function getPersistentIds( array $pageIds ): array;

public function getPageIdFromPersistentId( string $persistentId ): ?int;
}
77 changes: 77 additions & 0 deletions src/EntryPoints/SpecialPersistentPageIdentifierResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\PersistentPageIdentifiers\EntryPoints;

use FormSpecialPage;
use ProfessionalWiki\PersistentPageIdentifiers\PersistentPageIdentifiersExtension;
use Status;
use Title;

class SpecialPersistentPageIdentifierResolver extends FormSpecialPage {

public function __construct() {
parent::__construct( 'PersistentPageIdentifierResolver' );
}

public function execute( $subPage ): void {
if ( $subPage === null || $subPage === '' ) {
return;
}

$title = $this->getTitleFromPersistentId( $subPage );

if ( $title === null ) {
return;
}

$this->getOutput()->redirect( $title->getFullURL() );
}

protected function getFormFields(): array {
return [
'persistentpageidentifier' => [
'type' => 'text',
'label-message' => 'persistentpageidentifiers-info-label',
'required' => true,
]
];
}

public function onSubmit( array $data ): Status|bool {
$title = $this->getTitleFromPersistentId( $data['persistentpageidentifier'] );

if ( $title === null ) {
// Message: persistentpageidentifierresolver-not-exists
return Status::newFatal( $this->getMessagePrefix() . '-not-exists' );
}

$this->getOutput()->redirect( $title->getFullURL() );

return true;
}

protected function getDisplayFormat(): string {
return 'ooui';
}

public function getGroupName(): string {
return 'redirects';
}

private function getTitleFromPersistentId( string $persistentId ): ?Title {
$pageId = $this->getPageIdFromPersistentId( $persistentId );

if ( $pageId !== null ) {
return Title::newFromID( $pageId );
}

return null;
}

private function getPageIdFromPersistentId( string $persistentId ): ?int {
return PersistentPageIdentifiersExtension::getInstance()->getPersistentPageIdentifiersRepo()->getPageIdFromPersistentId( $persistentId );
}

}
12 changes: 12 additions & 0 deletions tests/Adapters/DatabasePersistentPageIdentifiersRepoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,16 @@ public function testCanSaveAndRetrieveMultiplePersistentIds(): void {
);
}

public function testGetPageIdFromPersistentId(): void {
$pageId = $this->createPageWithText()->getId();
$persistentId = '00000000-0000-0000-0000-000000000042';

$this->repo->savePersistentIds( [ $pageId => $persistentId ] );
$this->assertSame( $pageId, $this->repo->getPageIdFromPersistentId( $persistentId ) );
}

public function testGetPageIdFromNonExistentPersistentId(): void {
$this->assertNull( $this->repo->getPageIdFromPersistentId( 'non-existent' ) );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare( strict_types = 1 );

namespace ProfessionalWiki\PersistentPageIdentifiers\Tests\Integration;

use ProfessionalWiki\PersistentPageIdentifiers\Adapters\DatabasePersistentPageIdentifiersRepo;
use ProfessionalWiki\PersistentPageIdentifiers\Application\PersistentPageIdentifiersRepo;
use ProfessionalWiki\PersistentPageIdentifiers\EntryPoints\SpecialPersistentPageIdentifierResolver;
use ProfessionalWiki\PersistentPageIdentifiers\Tests\PersistentPageIdentifiersIntegrationTest;
use Status;

/**
* @covers \ProfessionalWiki\PersistentPageIdentifiers\EntryPoints\SpecialPersistentPageIdentifierResolver
* @group Database
*/
class SpecialPersistentPageIdentifierResolverIntegrationTest extends PersistentPageIdentifiersIntegrationTest {

private PersistentPageIdentifiersRepo $repo;

protected function setUp(): void {
parent::setUp();
$this->tablesUsed[] = 'persistent_page_ids';
$this->repo = new DatabasePersistentPageIdentifiersRepo( $this->db );
}

protected function newSpecialPage(): SpecialPersistentPageIdentifierResolver {
return new SpecialPersistentPageIdentifierResolver();
}

public function testExecuteWithValidIdRedirects(): void {
$page = $this->createPageWithText();

$resolver = $this->newSpecialPage();
$resolver->execute( $this->repo->getPersistentId( $page->getId() ) );

$this->assertSame(
$page->getTitle()->getFullURL(),
$resolver->getOutput()->getRedirect(),
'Should redirect to the page associated with the persistent ID.'
);
}

public function testExecuteWithNonExistentIdReturns(): void {
$resolver = $this->newSpecialPage();
$resolver->execute( 'non-existent-id' );

$this->assertSame(
'',
$resolver->getOutput()->getRedirect(),
'Should not redirect for a non-existent persistent ID.'
);
}

public function testExecuteWithEmptySubPageReturns(): void {
$resolver = $this->newSpecialPage();
$resolver->execute( '' );

$this->assertSame(
'',
$resolver->getOutput()->getRedirect(),
'Should not redirect for an empty subpage.'
);
}

public function testExecuteWithNullSubPageReturns(): void {
$resolver = $this->newSpecialPage();
$resolver->execute( null );

$this->assertSame(
'',
$resolver->getOutput()->getRedirect(),
'Should not redirect for a null subpage.'
);
}

public function testOnSubmitWithValidIdRedirects(): void {
$page = $this->createPageWithText();

$resolver = $this->newSpecialPage();
$status = $resolver->onSubmit( [ 'persistentpageidentifier' => $this->repo->getPersistentId( $page->getId() ) ] );

$this->assertTrue( $status, 'onSubmit should return true on success.' );
$this->assertSame(
$page->getTitle()->getFullURL(),
$resolver->getOutput()->getRedirect(),
'Should redirect to the page associated with the persistent ID on form submission.'
);
}

public function testOnSubmitWithNonExistentIdReturnsFatalStatus(): void {
$resolver = $this->newSpecialPage();
$status = $resolver->onSubmit( [ 'persistentpageidentifier' => 'non-existent-id' ] );

$this->assertInstanceOf(
Status::class,
$status,
'onSubmit should return a Status object for a non-existent ID.'
);
$this->assertFalse( $status->isGood(), 'Status should be fatal.' );

$errors = $status->getErrors();
$this->assertNotEmpty( $errors, 'Status should contain errors.' );
$this->assertEquals(
'persistentpageidentifierresolver-not-exists',
$errors[0]['message'],
'Status message should indicate the ID does not exist.'
);

$this->assertSame(
'',
$resolver->getOutput()->getRedirect(),
'Should not redirect when ID does not exist on form submission.'
);
}

}
5 changes: 5 additions & 0 deletions tests/TestDoubles/InMemoryPersistentPageIdentifiersRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public function getPersistentIds( array $pageIds ): array {
return array_intersect_key( $this->persistentIds, array_flip( $pageIds ) );
}

public function getPageIdFromPersistentId( string $persistentId ): ?int {
$pageId = array_search( $persistentId, $this->persistentIds, true );
return $pageId === false ? null : $pageId;
}

}
Loading