Skip to content

Commit

Permalink
T12708: Add APIs for RottenLinks to allow fetching a link's status (#81)
Browse files Browse the repository at this point in the history
* T12708: Add {{#rl_status}}

* Add tests for {{#rl_status}}

* T12708: Add the ability to get the status code through Scribunto

If Scribunto is not installed, the hook is never activated and nothing
happens. Compatibility for non-Scribunto wikis is achieved by doing
nothing.

* CI: lint code to MediaWiki standards

Check commit and GitHub actions for more details

* Appease CI

* Appease CI, the proper way

* Remove strict type hint on RottenLinksLuaLibrary::onGetStatus()

If Lua passes us a non-string (and non-null), PHP blows up as type
hinting here is strict, unlike Python.

* Appease CI, config edition

* Make changes as requested

* Change namespace as requested in review

* Remove trailing newline

* Type hint RottenLinksLuaLibrary::onGetStatus() per request on IRC

Also require PHP >= 8 since apparently `mixed` only existed since then

* Bump versions and add changelog

* Update CHANGELOG.md

---------

Co-authored-by: github-actions <[email protected]>
  • Loading branch information
BlankEclair and github-actions authored Oct 6, 2024
1 parent 573f18a commit a201914
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/phan_dependencies
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Scribunto:
branch: auto
repo: auto
12 changes: 12 additions & 0 deletions .phan/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

$cfg = require __DIR__ . '/../vendor/mediawiki/mediawiki-phan-config/src/config.php';

$cfg['directory_list'] = array_merge(
$cfg['directory_list'], [
'../../extensions/Scribunto',
],
);

$cfg['exclude_analysis_directory_list'] = array_merge(
$cfg['exclude_analysis_directory_list'], [
'../../extensions/Scribunto',
],
);

$cfg['suppress_issue_types'] = [
'PhanAccessMethodInternal',
'SecurityCheck-LikelyFalsePositive',
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## ChangeLog for RottenLinks


### 2.1.0 (06-10-2024)
* Add parser function (`{{#rl_status: URL}}`) and Scribunto library
(`mw.ext.rottenLinks.getStatus(url)`) for getting the status code of a URL.

### 2.0.1 (02-01-2024)
* Add `requireExtension` to updateExternalLinks.
* Modernize extension:
Expand Down
7 changes: 7 additions & 0 deletions RottenLinksMagic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

$magicWords = [];

$magicWords['en'] = [
'rl_status' => [ 0, 'rl_status' ],
];
22 changes: 18 additions & 4 deletions extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "RottenLinks",
"version": "2.0.1",
"version": "2.1.0",
"author": [
"John Lewis",
"Paladox",
Expand All @@ -12,15 +12,19 @@
"license-name": "GPL-3.0-or-later",
"type": "specialpage",
"requires": {
"MediaWiki": ">= 1.40.0"
"MediaWiki": ">= 1.42.0",
"platform": {
"php": ">= 8.0"
}
},
"MessagesDirs": {
"RottenLinks": [
"i18n"
]
},
"ExtensionMessagesFiles": {
"RottenLinksAliases": "RottenLinksAliases.php"
"RottenLinksAliases": "RottenLinksAliases.php",
"RottenLinksMagic": "RottenLinksMagic.php"
},
"AutoloadNamespaces": {
"Miraheze\\RottenLinks\\": "includes/"
Expand All @@ -43,6 +47,12 @@
},
"LoadExtensionSchemaUpdates": {
"handler": "Installer"
},
"ParserFirstCallInit": {
"handler": "Main"
},
"ScribuntoExternalLibraries": {
"handler": "Scribunto"
}
},
"HookHandlers": {
Expand All @@ -52,8 +62,12 @@
"Main": {
"class": "Miraheze\\RottenLinks\\HookHandlers\\Main",
"services": [
"JobQueueGroup"
"JobQueueGroup",
"ConnectionProvider"
]
},
"Scribunto": {
"class": "Miraheze\\RottenLinks\\HookHandlers\\Scribunto"
}
},
"config": {
Expand Down
1 change: 1 addition & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"rottenlinks-extensionname": "RottenLinks",
"rottenlinks-header": "Refine external link results display",
"rottenlinks-header-info": "You may select what statistics you want displayed or how many results you want displayed per page.",
"rottenlinks-rlstatus-no-url": "No URL provided for {{#rl_status}}",
"rottenlinks-table-external": "External Link",
"rottenlinks-table-response": "HTTP Response",
"rottenlinks-table-usage": "Page Usage",
Expand Down
1 change: 1 addition & 0 deletions i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"rottenlinks": "{{doc-special}}",
"rottenlinks-desc": "{{desc|name=RottenLinks|url=https://github.com/miraheze/RottenLinks}}",
"rottenlinks-extensionname": "{{name}}",
"rottenlinks-rlstatus-no-url": "This is the error message that is shown when the parser function {{#rl_status}} is called with no URL",
"rottenlinks-table-external": "This is the label of a tabel header on Special:RottenLinks",
"rottenlinks-table-response": "This is the label of a tabel header on Special:RottenLinks",
"rottenlinks-table-usage": "This is the label of a tabel header on Special:RottenLinks",
Expand Down
23 changes: 19 additions & 4 deletions includes/HookHandlers/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
use JobQueueGroup;
use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
use MediaWiki\Hook\LinksUpdateCompleteHook;
use MediaWiki\Hook\ParserFirstCallInitHook;
use MediaWiki\Parser\Parser;
use Miraheze\RottenLinks\RottenLinksJob;
use Miraheze\RottenLinks\RottenLinksParserFunctions;
use Wikimedia\Rdbms\IConnectionProvider;

class Main implements LinksUpdateCompleteHook {
class Main implements LinksUpdateCompleteHook, ParserFirstCallInitHook {

/** @var JobQueueGroup */
private $jobQueueGroup;
private JobQueueGroup $jobQueueGroup;
private RottenLinksParserFunctions $parserFunctions;

/**
* @param JobQueueGroup $jobQueueGroup
* @param IConnectionProvider $connectionProvider
*/
public function __construct( JobQueueGroup $jobQueueGroup ) {
public function __construct( JobQueueGroup $jobQueueGroup, IConnectionProvider $connectionProvider ) {
$this->jobQueueGroup = $jobQueueGroup;
$this->parserFunctions = new RottenLinksParserFunctions( $connectionProvider );
}

/**
Expand All @@ -38,4 +44,13 @@ public function onLinksUpdateComplete( $linksUpdate, $ticket ) {
$this->jobQueueGroup->push( new RottenLinksJob( $params ) );
}
}

/**
* Handler for ParserFirstCallInit hook.
* @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
* @param Parser $parser
*/
public function onParserFirstCallInit( $parser ) {
$parser->setFunctionHook( 'rl_status', [ $this->parserFunctions, 'onRLStatus' ], Parser::SFH_OBJECT_ARGS );
}
}
23 changes: 23 additions & 0 deletions includes/HookHandlers/Scribunto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Miraheze\RottenLinks\HookHandlers;

use MediaWiki\Extension\Scribunto\Hooks\ScribuntoExternalLibrariesHook;
use Miraheze\RottenLinks\RottenLinksLuaLibrary;

class Scribunto implements ScribuntoExternalLibrariesHook {

/**
* Handler for ScribuntoExternalLibraries hook.
* @param string $engine
* @param array &$externalLibraries
* @return bool
*/
public function onScribuntoExternalLibraries( $engine, &$externalLibraries ) {
if ( $engine === 'lua' ) {
$externalLibraries['mw.ext.rottenLinks'] = RottenLinksLuaLibrary::class;
}

return true;
}
}
24 changes: 24 additions & 0 deletions includes/RottenLinks.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Config;
use MediaWiki\MediaWikiServices;
use WikiMedia\Rdbms\IReadableDatabase;

class RottenLinks {
/**
Expand Down Expand Up @@ -69,4 +70,27 @@ private static function getHttpStatus(

return (int)$request['code'];
}

/**
* Get the HTTP response status for a given URL from the database.
*
* @param IReadableDatabase $dbr
* @param string $url
*
* @return ?int null if the URL is not in the database, 0 if there was no response, or the response code
*/
public static function getResponseFromDatabase( IReadableDatabase $dbr, string $url ): ?int {
$statusCode = $dbr->newSelectQueryBuilder()
->select( 'rl_respcode' )
->from( 'rottenlinks' )
->where( [
'rl_externallink' => $url,
] )
->caller( __METHOD__ )
->fetchField();

return $statusCode !== false
? $statusCode
: null;
}
}
57 changes: 57 additions & 0 deletions includes/RottenLinksLuaLibrary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
namespace Miraheze\RottenLinks;

use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LibraryBase;
use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaEngine;
use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaError;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IConnectionProvider;

class RottenLinksLuaLibrary extends LibraryBase {

private IConnectionProvider $connectionProvider;

/**
* @param LuaEngine $engine
*/
public function __construct( LuaEngine $engine ) {
parent::__construct( $engine );
// Unfortunately, Scribunto currently does not offer us any options to do
// dependency injection, so we have to pretend that we do. Luckily, there
// is already an upstream task: https://phabricator.wikimedia.org/T375835
$this->connectionProvider = MediaWikiServices::getInstance()->getConnectionProvider();
}

/**
* @param mixed $url
* @return array
* @internal
*/
public function onGetStatus( mixed $url = null ): array {
$name = 'mw.ext.rottenLinks.getStatus';
$this->checkType( $name, 1, $url, 'string' );
// $this->checkType() validates that $url is a string, therefore...
'@phan-var string $url';

// I think Lua errors are untranslated? LibraryBase::checkType() returns
// a plain ol' English string too.
if ( $url === '' ) {
throw new LuaError( "bad argument #1 to '{$name}' (url is empty)" );
}

$dbr = $this->connectionProvider->getReplicaDatabase();
return [ RottenLinks::getResponseFromDatabase( $dbr, $url ) ];
}

/**
* @return array
*/
public function register(): array {
$functions = [
'getStatus' => [ $this, 'onGetStatus' ],
];
$arguments = [];

return $this->getEngine()->registerInterface( __DIR__ . '/mw.ext.rottenLinks.lua', $functions, $arguments );
}
}
41 changes: 41 additions & 0 deletions includes/RottenLinksParserFunctions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Miraheze\RottenLinks;

use MediaWiki\Html\Html;
use MediaWiki\Parser\Parser;
use PPFrame;
use PPNode;
use Wikimedia\Rdbms\IConnectionProvider;

class RottenLinksParserFunctions {

private IConnectionProvider $connectionProvider;

/**
* @param IConnectionProvider $connectionProvider
*/
public function __construct( IConnectionProvider $connectionProvider ) {
$this->connectionProvider = $connectionProvider;
}

/**
* The function responsible for handling {{#rl_status}}.
*
* @param Parser $parser
* @param PPFrame $frame
* @param PPNode[] $args
* @return string
*/
public function onRLStatus( Parser $parser, PPFrame $frame, array $args ): string {
$url = trim( $frame->expand( $args[0] ?? '' ) );
if ( $url === '' ) {
return Html::element( 'strong', [
'class' => 'error',
], $parser->msg( 'rottenlinks-rlstatus-no-url' ) );
}

$dbr = $this->connectionProvider->getReplicaDatabase();
return (string)RottenLinks::getResponseFromDatabase( $dbr, $url );
}
}
18 changes: 18 additions & 0 deletions includes/mw.ext.rottenLinks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
local p = {}

function p.setupInterface(arguments)
p.setupInterface = nil

local php = mw_interface
mw_interface = nil

mw = mw or {}
mw.ext = mw.ext or {}

p.getStatus = php.getStatus

mw.ext.rottenLinks = p
package.loaded['mw.ext.rottenLinks'] = p
end

return p
Loading

0 comments on commit a201914

Please sign in to comment.