Refactor, add Targets class.

This commit is contained in:
Daniel Kraus
2017-08-26 20:35:25 +02:00
parent a7bd7d19ef
commit 3f32077884
8 changed files with 615 additions and 263 deletions

445
Doxyfile

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,8 @@
},
"AutoloadClasses": {
"LinkTitles\\Extension": "includes/LinkTitles_Extension.php",
"LinkTitles\\Targets": "includes/Targets.php",
"LinkTitles\\Config": "includes/Config.php",
"LinkTitles\\Special": "includes/LinkTitles_Special.php",
"LinkTitles\\TestCase": "tests/phpunit/TestCase.php"
},

126
includes/Config.php Normal file
View File

@ -0,0 +1,126 @@
<?php
/**
* The LinkTitles\Config class holds configuration for the LinkTitles extension.
*
* Copyright 2012-2017 Daniel Kraus <bovender@bovender.de> ('bovender')
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* @author Daniel Kraus <bovender@bovender.de>
*/
namespace LinkTitles;
/**
* Holds LinkTitles configuration.
*
* This class encapsulates the global configuration variables so we do not have
* to pull those globals into scope in the individual LinkTitles classes.
*
* Using a dedicated configuration class also facilitates overriding certain
* options, i.e. in a maintenance script that is invoked with flags from the
* command line.
*
* @since 5.0.0
*/
class Config {
/**
* Whether to add links to a page when the page is edited/saved.
* @var bool $parseOnEdit
*/
public $parseOnEdit;
/**
* Whether to add links to a page when the page is rendered.
* @var bool $parseOnRender
*/
public $parseOnRender;
/**
* Indicates whether to prioritize short over long titles.
* @var bool $preferShortTitles
*/
public $preferShortTitles;
/**
* Minimum length of a page title for it to qualify as a potential link target.
* @var int $minimumTitleLength
*/
public $minimumTitleLength;
/**
* Array of page titles that must never be link targets.
*
* This may be useful to exclude common abbreviations or acronyms from
* automatic linking.
* @var Array $blackList
*/
public $blackList;
/**
* Array of those name spaces (integer constants) whose pages may be linked.
* @var Array $nameSpaces
*/
public $nameSpaces;
/**
* Indicates whether to add a link to the first occurrence of a page title
* only (true), or add links to all occurrences on the source page (false).
* @var bool $firstOnly;
*/
public $firstOnly;
/**
* Indicates whether to operate in smart mode, i.e. link to pages even if the
* case does not match. Without smart mode, pages are linked to only if the
* exact title appears on the source page.
* @var bool $smartMode;
*/
public $smartMode;
/**
* Mirrors the global MediaWiki variable $wgCapitalLinks that indicates
* whether or not page titles are fully case sensitive
* @var bool $capitalLinks;
*/
public $capitalLinks;
/**
* Constructs a new Config object.
*
* The object's member variables will automatically be set with the values
* from the corresponding global variables.
*/
public function __construct() {
global $wgLinkTitlesParseOnEdit;
global $wgLinkTitlesParseOnRender;
global $wgLinkTitlesPreferShortTitles;
global $wgLinkTitlesMinimumTitleLength;
global $wgLinkTitlesBlackList;
global $wgLinkTitlesNamespaces;
global $wgLinkTitlesFirstOnly;
global $wgLinkTitlesSmartMode;
global $wgCapitalLinks;
$this->parseOnEdit = $wgLinkTitlesParseOnEdit;
$this->parseOnRender = $wgLinkTitlesParseOnRender;
$this->preferShortTitles = $wgLinkTitlesPreferShortTitles;
$this->minimumTitleLength = $wgLinkTitlesMinimumTitleLength;
$this->blackList = $wgLinkTitlesBlackList;
$this->nameSpaces = $wgLinkTitlesNamespaces;
$this->firstOnly = $wgLinkTitlesFirstOnly;
$this->smartMode = $wgLinkTitlesSmartMode;
$this->capitalLinks = $wgCapitalLinks; // MediaWiki global variable
}
}

View File

@ -1,44 +1,32 @@
<?php
/*
* Copyright 2012-2017 Daniel Kraus <bovender@bovender.de> ('bovender')
/**
* The LinkTitles\Extension class provides entry points for the extension.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* Copyright 2012-2017 Daniel Kraus <bovender@bovender.de> ('bovender')
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* @author Daniel Kraus <bovender@bovender.de>
*/
/// @file
namespace LinkTitles;
/// Helper function for development and debugging.
/// @param $var Any variable. Raw content will be dumped to stderr.
/// @return undefined
function dump($var) {
error_log(print_r($var, TRUE) . "\n", 3, 'php://stderr');
};
/// Central class of the extension. Sets up parser hooks.
/// This class contains only static functions; do not instantiate.
/**
* Provides entry points for the extension.
*/
class Extension {
/// Caching variable for page titles that are fetched from the DB.
private static $pageTitles;
/// Caching variable for the current namespace.
/// This is needed because the sort order of the page titles that
/// are cached in self::$pageTitles depends on the namespace of
/// the page currently being processed.
private static $currentNamespace;
/// A Title object for the page that is being parsed.
private static $currentTitle;
@ -73,12 +61,6 @@ class Extension {
self::BuildDelimiters();
}
/// Helper method to be used in unit testing, were everything takes place
/// in one request.
public static function invalidateCache() {
self::fetchPageTitles();
}
/// Event handler that is hooked to the PageContentSave event.
public static function onPageContentSave( &$wikiPage, &$user, &$content, &$summary,
$isMinor, $isWatch, $section, &$flags, &$status ) {
@ -136,16 +118,12 @@ class Extension {
( $wgLinkTitlesFirstOnly ) ? $limit = 1 : $limit = -1;
$limitReached = false;
self::$currentTitle = $title;
$currentNamespace = $title->getNamespace();
$newText = $text;
if ( !isset( self::$pageTitles ) || ( $currentNamespace != self::$currentNamespace ) ) {
self::$currentNamespace = $currentNamespace;
self::fetchPageTitles();
}
$targets = Targets::default( $title, new Config() );
// Iterate through the page titles
foreach( self::$pageTitles as $row ) {
foreach( $targets->queryResult as $row ) {
self::newTarget( $row->page_namespace, $row->page_title );
// Don't link current page
@ -274,67 +252,6 @@ class Extension {
return $output;
}
// Fetches the page titles from the database.
private static function fetchPageTitles() {
global $wgLinkTitlesPreferShortTitles;
global $wgLinkTitlesMinimumTitleLength;
global $wgLinkTitlesBlackList;
global $wgLinkTitlesNamespaces;
( $wgLinkTitlesPreferShortTitles ) ? $sort_order = 'ASC' : $sort_order = 'DESC';
// Build a blacklist of pages that are not supposed to be link
// targets. This includes the current page.
$blackList = str_replace( ' ', '_', '("' . implode( '","',$wgLinkTitlesBlackList ) . '")' );
// Build our weight list. Make sure current namespace is first element
$namespaces = array_diff( $wgLinkTitlesNamespaces, [ self::$currentNamespace ] );
array_unshift( $namespaces, self::$currentNamespace );
// No need for sanitiy check. we are sure that we have at least one element in the array
$weightSelect = "CASE page_namespace ";
$currentWeight = 0;
foreach ($namespaces as &$namspacevalue) {
$currentWeight = $currentWeight + 100;
$weightSelect = $weightSelect . " WHEN " . $namspacevalue . " THEN " . $currentWeight . PHP_EOL;
}
$weightSelect = $weightSelect . " END ";
$namespacesClause = '(' . implode( ', ', $namespaces ) . ')';
// Build an SQL query and fetch all page titles ordered by length from
// shortest to longest. Only titles from 'normal' pages (namespace uid
// = 0) are returned. Since the db may be sqlite, we need a try..catch
// structure because sqlite does not support the CHAR_LENGTH function.
$dbr = wfGetDB( DB_SLAVE );
try {
$res = $dbr->select(
'page',
array( 'page_title', 'page_namespace' , "weight" => $weightSelect),
array(
'page_namespace IN ' . $namespacesClause,
'CHAR_LENGTH(page_title) >= ' . $wgLinkTitlesMinimumTitleLength,
'page_title NOT IN ' . $blackList,
),
__METHOD__,
array( 'ORDER BY' => 'weight ASC, CHAR_LENGTH(page_title) ' . $sort_order )
);
} catch (Exception $e) {
$res = $dbr->select(
'page',
array( 'page_title', 'page_namespace' , "weight" => $weightSelect ),
array(
'page_namespace IN ' . $namespacesClause,
'LENGTH(page_title) >= ' . $wgLinkTitlesMinimumTitleLength,
'page_title NOT IN ' . $blackList,
),
__METHOD__,
array( 'ORDER BY' => 'weight ASC, LENGTH(page_title) ' . $sort_order )
);
}
self::$pageTitles = $res;
return true;
}
// Build an anonymous callback function to be used in simple mode.
private static function simpleModeCallback( array $matches ) {
if ( self::checkTargetPage() ) {

142
includes/Targets.php Normal file
View File

@ -0,0 +1,142 @@
<?php
/**
* The LinkTitles\Targets class.
*
* Copyright 2012-2017 Daniel Kraus <bovender@bovender.de> ('bovender')
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* @author Daniel Kraus <bovender@bovender.de>
*/
namespace LinkTitles;
/**
* Fetches potential target page titles from the database.
*/
class Targets {
private static $instance;
/**
* Singleton factory that returns a (cached) database query results with
* potential target page titles.
*
* The subset of pages that may serve as target pages depends on the
* name space of the source page. Therefore, if the $nameSpace differs from
* the cached name space, the database is queried again.
*
* @param String $nameSpace The namespace of the current page.
* @param Config $config LinkTitles configuration.
*/
public static function default( \Title $title, Config $config ) {
if ( ( self::$instance === null ) || ( self::$instance->nameSpace != $title->getNamespace() ) ) {
self::$instance = new Targets( $title, $config );
}
return self::$instance;
}
/**
* Invalidates the cache; the next call of Targets::default() will trigger
* a database query.
*
* Use this in unit tests which are performed in a single request cycle so that
* changes to the pages list may not be picked up by the cached Targets instance.
*/
public static function invalidate() {
self::$instance = null;
}
/**
* Holds the results of a database query for target page titles, filtered
* and sorted.
* @var IResultWrapper $queryResult
*/
public $queryResult;
/**
* Holds the name space (integer) for which the list of target pages was built.
* @var Int $nameSpace
*/
public $nameSpace;
private $config;
/**
* The constructor is private to enforce using the singleton pattern.
* @param \Title $title
*/
private function __construct( \Title $title, Config $config) {
$this->config = $config;
$this->nameSpace = $title->getNameSpace();
$this->fetch();
}
//
/**
* Fetches the page titles from the database.
*/
private function fetch() {
( $this->config->preferShortTitles ) ? $sortOrder = 'ASC' : $sortOrder = 'DESC';
// Build a blacklist of pages that are not supposed to be link
// targets. This includes the current page.
$blackList = str_replace( ' ', '_', '("' . implode( '","',$this->config->blackList ) . '")' );
// Build our weight list. Make sure current namespace is first element
$nameSpaces = array_diff( $this->config->nameSpaces, [ $this->nameSpace ] );
array_unshift( $nameSpaces, $this->nameSpace );
// No need for sanitiy check. we are sure that we have at least one element in the array
$weightSelect = "CASE page_namespace ";
$currentWeight = 0;
foreach ($nameSpaces as &$nameSpaceValue) {
$currentWeight = $currentWeight + 100;
$weightSelect = $weightSelect . " WHEN " . $nameSpaceValue . " THEN " . $currentWeight . PHP_EOL;
}
$weightSelect = $weightSelect . " END ";
$nameSpacesClause = '(' . implode( ', ', $nameSpaces ) . ')';
// Build an SQL query and fetch all page titles ordered by length from
// shortest to longest. Only titles from 'normal' pages (namespace uid
// = 0) are returned. Since the db may be sqlite, we need a try..catch
// structure because sqlite does not support the CHAR_LENGTH function.
$dbr = wfGetDB( DB_SLAVE );
try {
$this->queryResult = $dbr->select(
'page',
array( 'page_title', 'page_namespace' , "weight" => $weightSelect),
array(
'page_namespace IN ' . $nameSpacesClause,
'CHAR_LENGTH(page_title) >= ' . $this->config->minimumTitleLength,
'page_title NOT IN ' . $blackList,
),
__METHOD__,
array( 'ORDER BY' => 'weight ASC, CHAR_LENGTH(page_title) ' . $sortOrder )
);
} catch (Exception $e) {
$this->queryResult = $dbr->select(
'page',
array( 'page_title', 'page_namespace' , "weight" => $weightSelect ),
array(
'page_namespace IN ' . $nameSpacesClause,
'LENGTH(page_title) >= ' . $this->config->minimumTitleLength,
'page_title NOT IN ' . $blackList,
),
__METHOD__,
array( 'ORDER BY' => 'weight ASC, LENGTH(page_title) ' . $sortOrder )
);
}
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* Tests the LinkTitles\Config class.
*
* This single unit test basically serves to ensure the Config class is working.
* @group bovender
* @group Database
*/
class ConfigTest extends LinkTitles\TestCase {
public function testParseOnEdit() {
$this->setMwGlobals( [
'wgLinkTitlesParseOnEdit' => true,
'wgLinkTitlesParseOnRender' => false
] );
$config = new LinkTitles\Config();
global $wgLinkTitlesParseOnEdit;
$this->assertSame( $config->parseOnEdit, $wgLinkTitlesParseOnEdit );
}
}

View File

@ -8,10 +8,20 @@ class ParseOnEditTest extends LinkTitles\TestCase {
public function testParseOnEdit() {
$this->setMwGlobals( [
'wgLinkTitlesParseOnEdit' => true,
'wgLinkTitlesParseOnRender' => true
'wgLinkTitlesParseOnRender' => false
] );
$pageId = $this->insertPage( 'test page', 'This page should link to the link target' )['id'];
$page = WikiPage::newFromId( $pageId );
$this->assertSame( 'This page should link to the [[link target]]', self::getPageText( $page ) );
}
public function testDoNotParseOnEdit() {
$this->setMwGlobals( [
'wgLinkTitlesParseOnEdit' => false,
'wgLinkTitlesParseOnRender' => false
] );
$pageId = $this->insertPage( 'test page', 'This page should not link to the link target' )['id'];
$page = WikiPage::newFromId( $pageId );
$this->assertSame( 'This page should not link to the link target', self::getPageText( $page ) );
}
}

View File

@ -5,7 +5,7 @@ abstract class TestCase extends \MediaWikiTestCase {
protected function setUp() {
parent::setUp();
$this->insertPage( 'link target', 'This page serves as a link target' );
Extension::invalidateCache();
Targets::invalidate(); // force re-querying the pages table
}
protected function tearDown() {