├── .gitmodules ├── .atomignore ├── .dockerignore ├── .gitignore ├── Makefile ├── LinkTitles.php ├── install-composer.sh ├── Dockerfile ├── i18n ├── qqq.json ├── en.json └── de.json ├── includes ├── Magic.php ├── Targets.php ├── Splitter.php ├── Extension.php ├── Config.php ├── Linker.php ├── Source.php ├── Target.php └── Special.php ├── tests └── phpunit │ ├── ConfigTest.php │ ├── TestCase.php │ ├── TargetsTest.php │ ├── TargetTest.php │ ├── ExtensionTest.php │ ├── SplitterTest.php │ └── LinkerTest.php ├── README_DOC.md ├── NEWS.md ├── extension.json ├── linktitles-cli.php ├── NEWS.old ├── COPYING └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.atomignore: -------------------------------------------------------------------------------- 1 | gh-pages/ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | *.swp 4 | *.bak 5 | gpl-2.0.txt 6 | Maintenance.php 7 | doxygen_sqlite3.db 8 | /LinkTitles.phpproj 9 | /LinkTitles.phpproj.user 10 | /LinkTitles.sln 11 | /LinkTitles.v12.suo 12 | release/ 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | # With MW version 1.34, there is one test that is expected to fail 5 | # because linking on page save no longer works with MediaWiki 1.32 6 | # and newer. The test that is expected to fail is: 7 | # ExtensionTest::testParseOnEdit with data set #0 8 | docker run -it -v `pwd`:/var/www/html/extensions/LinkTitles --rm bovender/linktitles 9 | 10 | build-test-container: 11 | docker build -t bovender/linktitles . 12 | -------------------------------------------------------------------------------- /LinkTitles.php: -------------------------------------------------------------------------------- 1 | &2 echo 'ERROR: Invalid installer checksum' 14 | rm composer-setup.php 15 | exit 1 16 | fi 17 | 18 | # May need to remove the explicit pinning of version 1 in the future 19 | php composer-setup.php --quiet 20 | RESULT=$? 21 | rm composer-setup.php 22 | exit $RESULT 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile can be used to create a Docker image/container 2 | # that runs the unit tests on the LinkTitles extension. 3 | FROM mediawiki:1.44 4 | LABEL "MAINTAINER" Daniel Kraus (https://www.bovender.de) 5 | 6 | RUN apt-get update -yqq && \ 7 | apt-get install -yqq \ 8 | sqlite3 \ 9 | unzip \ 10 | zip 11 | 12 | WORKDIR /var/www/html 13 | ADD install-composer.sh install-composer.sh 14 | RUN sed -i 's/\r$//' install-composer.sh 15 | RUN chmod +x install-composer.sh 16 | RUN ./install-composer.sh 17 | 18 | COPY . /var/www/html/extensions/LinkTitles/ 19 | RUN mkdir /data && chown www-data /data 20 | 21 | RUN php composer.phar update 22 | 23 | WORKDIR /var/www/html/maintenance 24 | RUN php install.php --pass linktitles --dbtype sqlite --extensions LinkTitles Tests admin 25 | 26 | WORKDIR /var/www/html/tests/phpunit 27 | CMD ["php", "phpunit.php", "--group", "bovender"] 28 | -------------------------------------------------------------------------------- /i18n/qqq.json: -------------------------------------------------------------------------------- 1 | { 2 | "linktitles": "Name of the LinkTitles extension; also used for the special page.", 3 | "linktitles-desc": "Description of the LinkTitles extension.", 4 | "linktitles-special-info": "Descriptive text that appears on the Special:LinkTitles page.", 5 | "linktitles-special-submit": "Label for submit button to start linking.", 6 | "linktitles-special-progress": "Text that is shown on the Special:LinkTitles page while batch processing is executing.", 7 | "linktitles-special-page-count": "Number of pages.", 8 | "linktitles-special-cancel-notice": "Informs the user how to cancel the LinkTitles batch processing.", 9 | "linktitles-special-completed-info": "Text and statistics table that appear when batch processing has completed.", 10 | "linktitles-bot-comment": "Comment to add when pages are processed by LinkTitles\\Extension::processPage. The parameter may take the URL the the extension's homepage.", 11 | "right-linktitles-batch": "Describes the right to perform a LinkTitles batch operation.", 12 | "action-linktitles-batch": "Describes the action of performing a LinkTitles batch operation." 13 | } 14 | -------------------------------------------------------------------------------- /includes/Magic.php: -------------------------------------------------------------------------------- 1 | ('bovender') 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 18 | * MA 02110-1301, USA. 19 | */ 20 | /*! @file LinkTitles_Magic.php 21 | */ 22 | 23 | /// Holds the two magic words that the extension provides. 24 | $magicWords = array(); 25 | 26 | /// Default magic words in English. 27 | $magicWords['en'] = array( 28 | 'MAG_LINKTITLES_NOAUTOLINKS' => array(0, '__NOAUTOLINKS__'), 29 | 'MAG_LINKTITLES_NOTARGET' => array(0, '__NOAUTOLINKTARGET__') 30 | ); 31 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "linktitles": "LinkTitles", 3 | "linktitles-desc": "Automatically adds links to existing pages whenever a page is saved.", 4 | "linktitles-special-info": "LinkTitles extension: $1\n== Batch Linking ==\nYou can start a batch linking process by clicking on the button below. This will go through every page in the normal namespace of your Wiki and insert links automatically. This page will repeatedly reload itself, in order to prevent blocking the server. To interrupt the process, simply close this page.", 5 | "linktitles-special-submit": "Start linking now", 6 | "linktitles-special-progress": "== Processing pages... ==\nThe [$1 LinkTitles] extension is currently going through every page of your wiki, adding links to existing pages as appropriate.\n=== Current page: $2 ===", 7 | "linktitles-special-page-count": "Page $1 of $2.\n", 8 | "linktitles-special-cancel-notice": "=== To abort, close this page, or hit the 'Stop' button in your browser ===\n[[Special:LinkTitles|Return to Special:LinkTitles.]]", 9 | "linktitles-special-completed-info": "== Batch processing completed! ==\n{| class=\"wikitable\"\n|-\n| total number of pages: || $1\n|-\n| timeout setting [s]: || $2\n|-\n| webpage reloads: || $3\n|-\n| pages scanned per reload interval: || $4\n|}", 10 | "linktitles-bot-comment": "The LinkTitles extension automatically added links to existing pages ($1).", 11 | "right-linktitles-batch": "Perform a LinkTitles batch operation", 12 | "action-linktitles-batch": "perform a LinkTitles batch operation" 13 | } 14 | -------------------------------------------------------------------------------- /tests/phpunit/ConfigTest.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | 24 | /** 25 | * Tests the LinkTitles\Config class. 26 | * 27 | * This single unit test basically serves to ensure the Config class is working. 28 | * @group bovender 29 | * @group Database 30 | */ 31 | class ConfigTest extends LinkTitles\TestCase { 32 | 33 | public function testParseOnEdit() { 34 | $this->setMwGlobals( [ 35 | 'wgLinkTitlesParseOnEdit' => true, 36 | 'wgLinkTitlesParseOnRender' => false 37 | ] ); 38 | $config = new LinkTitles\Config(); 39 | global $wgLinkTitlesParseOnEdit; 40 | $this->assertSame( $config->parseOnEdit, $wgLinkTitlesParseOnEdit ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "linktitles": "LinkTitles", 3 | "linktitles-desc": "Fügt beim Speichern von Seiten automatisch Querverweise zu vorhandenen Seiten ein.", 4 | "linktitles-special-info": "LinkTitles-Erweiterung: <$1>\n== Stapelverarbeitung ==\nKlicken Sie auf den Button, um die Stapelverarbeitung zu beginnen. Die Erweiterung wird nacheinander alle Seiten Ihres Wikis mit automatischen Links versehen. Um zu verhindern, daß der Server bei der Ausführung blockiert wird, wird diese Seite sich wiederholt selbst aufrufen. Um die Stapelverarbeitung abzubrechen, schließen Sie einfach diese Seite.", 5 | "linktitles-special-submit": "Stapelverarbeitung beginnen", 6 | "linktitles-special-progress": "== Verarbeite die Seiten... ==\nDie [$1 LinkTitles-Erweiterung] verarbeitet gerade alle Seiten Ihres Wikis.\n=== Aktuelle Seite: $2 ===", 7 | "linktitles-special-page-count": "Seite $1 von $2.\n", 8 | "linktitles-special-cancel-notice": "=== Um abzubrechen, schließen Sie diese Seite, oder klicken Sie den 'Stopp'-Knopf in Ihrem Browser ===\n[[Special:LinkTitles|Zu Special:LinkTitles zurückkehren.]]", 9 | "linktitles-special-completed-info": "== Stapelverarbeitung abgeschlossen! ==\n{| class=\"wikitable\"\n|-\n| Anzahl der verarbeiteten Seiten: || $1\n|-\n| Verarbeitungsintervall [s]: || $2\n|-\n| Reloads: || $3\n|-\n| Seiten pro Reload: || $4\n|}", 10 | "linktitles-bot-comment": "Die LinkTitles-Erweiterung hat automatisch Links zu anderen Seiten hinzugefügt ($1).", 11 | "right-linktitles-batch": "LinkTitles-Stapelverarbeitung ausführen", 12 | "action-linktitles-batch": "LinkTitles-Stapelverarbeitung auszuführen" 13 | } 14 | -------------------------------------------------------------------------------- /tests/phpunit/TestCase.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | namespace LinkTitles; 24 | use MediaWikiIntegrationTestCase; 25 | 26 | abstract class TestCase extends MediaWikiIntegrationTestCase { 27 | protected function setUp(): void 28 | { 29 | parent::setUp(); 30 | } 31 | 32 | protected function tearDown(): void 33 | { 34 | parent::tearDown(); 35 | } 36 | 37 | public function addDBDataOnce() { 38 | parent::addDBDataOnce(); 39 | $this->insertPage( 'link target', 'This page serves as a link target' ); 40 | Targets::invalidate(); // force re-querying the pages table 41 | } 42 | 43 | protected function getPageText( \WikiPage $page ) { 44 | $content = $page->getContent(); 45 | return $page->getContentHandler()->serializeContent( $content ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/phpunit/TargetsTest.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | 24 | /** 25 | * Tests the LinkTitles\Targets class. 26 | * 27 | * @group bovender 28 | * @group Database 29 | */ 30 | class TargetsTest extends LinkTitles\TestCase { 31 | 32 | public function testTargets() { 33 | $config = new LinkTitles\Config(); 34 | // Include the custom namespace with index 4000 in the count. This is a 35 | // very ugly hack. If the custom namespace index in 36 | // LinkTitlesLinkerTest::testLinkContentTargetNamespaces() is every changed, 37 | // this test will fail. 38 | $config->targetNamespaces = [ 4000 ]; 39 | $title = Title::newFromText( 'link target' ); 40 | $targets = LinkTitles\Targets::singleton( $title, $config ); 41 | 42 | // Count number of articles: Inspired by updateArticleCount.php maintenance 43 | // script: https://doc.wikimedia.org/mediawiki-core/master/php/updateArticleCount_8php_source.html 44 | $dbr = wfGetDB( DB_REPLICA ); 45 | $counter = new SiteStatsInit( $dbr ); 46 | $count = $counter->pages(); 47 | 48 | $this->assertEquals( $targets->queryResult->numRows(), $count ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/phpunit/TargetTest.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | 24 | /** 25 | * @group bovender 26 | */ 27 | 28 | class TargetTest extends LinkTitles\TestCase { 29 | 30 | /** 31 | * @dataProvider provideStartOnly 32 | */ 33 | public function testTargetWordStartOnly($enabled, $delimiter) 34 | { 35 | $config = new LinkTitles\Config(); 36 | $config->wordStartOnly = $enabled; 37 | $target = new LinKTitles\Target(NS_MAIN, 'test page', $config); 38 | $this->assertSame($delimiter, $target->wordStart); 39 | } 40 | 41 | public static function provideStartOnly() 42 | { 43 | return [ 44 | [true, '(?wordEndOnly = $enabled; 56 | $target = new LinKTitles\Target(NS_MAIN, 'test page', $config); 57 | $this->assertSame($delimiter, $target->wordEnd); 58 | } 59 | 60 | public static function provideEndOnly() 61 | { 62 | return [ 63 | [true, '(?!\pL|\pN)'], 64 | [false, ''] 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README_DOC.md: -------------------------------------------------------------------------------- 1 | @mainpage LinkTitles 2 | @author [Daniel Kraus (bovender)](http://www.mediawiki.org/wiki/User:Bovender) 3 | @date 2012-2017 4 | @copyright [GNU GPL v2+](http://www.gnu.org/licenses/gpl-2.0.html) 5 | 6 | %LinkTitles source code documentation 7 | ===================================== 8 | 9 | This is the [source code][] documentation for the [LinkTitles][] extension 10 | for [MediaWiki][]. 11 | 12 | With version 5.0, the code is more extensively commented than ever. Version 5 13 | brought a major refactoring, and the extension now consists of several classes 14 | with clearly defined concerns. Look at the class comments to find out what they 15 | do. 16 | 17 | @note The source code that is referenced in this documentation may not 18 | necessarily reflect the latest code in the repository! Make sure to check 19 | out the Git repository for the latest code. 20 | 21 | 22 | A note on the publication of the source code documentation 23 | ---------------------------------------------------------- 24 | 25 | The documentation is automatically generated from the source code by 26 | [Doxygen][]. A special branch of the Git repository, [`gh-pages`][gh-pages], 27 | was created that holds the [GitHub project pages][github-pages]. To make use 28 | of the project page facility provided by GitHub, the root directory of a 29 | repository needs to be [erased][gh-erase] first before content is added. 30 | Since this would remove the source code from which the documentation is 31 | generated, deleting all files in the root directory while on the `gh-pages` 32 | branch was not feasible. 33 | 34 | The solution was to pull down the `gh-pages` branch as a submodule into an 35 | empty `gh-pages\` subdirectory: 36 | 37 | ~~~~{.sh} 38 | git submodule add -b gh-pages git@github.com:bovender/LinkTitles.git gh-pages 39 | ~~~~ 40 | 41 | This command tells git-submodule to only pull down the `gh-pages` branch as 42 | a submodule -- we don't need to pull down the master branch into the same 43 | repository again. 44 | 45 | 46 | [source code]: http://github.com/bovender/LinkTitles 47 | [LinkTitles]: http://www.mediawiki.org/wiki/Extension:LinkTitles 48 | [MediaWiki]: http://www.mediawiki.org 49 | [Doxygen]: http://www.doxygen.org 50 | [github-pages]: https://pages.github.com/ 51 | [gh-pages]: https://github.com/bovender/LinkTitles/tree/gh-pages 52 | [gh-erase]: https://help.github.com/articles/creating-project-pages-manually#create-a-gh-pages-branch 53 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | For changes prior to version 6.0.0, please see [`NEWS.old`](news.old). 9 | 10 | ## [8.1.2][] - 2024-11-22 11 | 12 | ### Fixed 13 | 14 | - Fix handling of words with special characters (e.g. French accents, German umlauts). 15 | 16 | ## [8.1.1][] - 2024-01-05 17 | 18 | ### Fixed 19 | 20 | - Fixed "Call to undefined method WikiPage::factory()" error that arose from 21 | an incompatibility with recent MediaWiki versions. 22 | - Fixed string interpolation deprecation warnings. 23 | 24 | ## [8.1.0][] - 2022-03-13 25 | 26 | ### New 27 | 28 | - Added a `--verbose` (`-v`) option to the commandline script. 29 | - Added a 'shebang' line to the commandline script so that it can be invoked 30 | directly. 31 | 32 | ### Fixed 33 | 34 | - Ensure compatibility with the PageForms extension (#58) 35 | 36 | ## [8.0.1][] - 2021-05-08 37 | 38 | ### Fixed 39 | 40 | - Fixed a regression concerning `linktitles-cli` progress display that was 41 | introduced in v8.0.0. 42 | - Prevent a division-by-zero error that could occur on the LinkTitles special 43 | page (#55). 44 | - Fixed the message that is shown when linking that was triggered from the 45 | special page has been completed. 46 | 47 | ## [8.0.0][] - 2021-04-06 48 | 49 | ### Changed 50 | 51 | - The minimum required version of MediaWiki is now 1.35. 52 | - The values of `data-*` attributes in HTML tags are now being ignored. 53 | 54 | ### Fixed 55 | 56 | - Replace PageContentSave with MultiContentSave to fix compatibility with MediaWiki 1.35. 57 | - The default value for wgLinkTitlesParseOnRender is change back to `false` as support 58 | for MediaWiki 1.35+ is fixed. 59 | - Progress reporting by `linktitles-cli.php` is no longer incorrect when a start 60 | page is defined. 61 | 62 | ## [7.0.0][] - 2020-12-23 63 | 64 | ### Changed 65 | 66 | - The minimum required version of MediaWiki is now 1.32. 67 | 68 | ### Fixed 69 | 70 | - Fixed compatibility with MediaWiki version 1.35. 71 | 72 | ## [6.0.0][] - 2019-12-31 73 | 74 | ### Changed 75 | 76 | - Because automatic linking upon page save no longer works with MediaWiki 77 | versions 1.32 and newer, the default value of the `$wgLinkTitlesParseOnRender` 78 | is now `true`. Please see `README.md` for more information. 79 | 80 | ### Fixed 81 | 82 | - Prevent crash that occurred with MediaWiki version 1.34 due to a renamed 83 | constant (DB_SLAVE was renamed to DB_REPLICA). NOTE that the minimum 84 | required version of MediaWiki is now 1.28 (which is an obsolete version). 85 | 86 | [8.1.2]: https://github.com/bovender/LinkTitles/releases/tag/v8.1.2 87 | [8.1.1]: https://github.com/bovender/LinkTitles/releases/tag/v8.1.1 88 | [8.1.0]: https://github.com/bovender/LinkTitles/releases/tag/v8.1.0 89 | [8.0.1]: https://github.com/bovender/LinkTitles/releases/tag/v8.0.1 90 | [8.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v8.0.0 91 | [7.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v7.0.0 92 | [6.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v6.0.0 93 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LinkTitles", 3 | "author": [ 4 | "[https://www.mediawiki.org/wiki/User:Bovender Daniel Kraus (bovender)]", 5 | "Adrian (bluedreamer)", 6 | "j-zero", 7 | "Bertrand Gorge (bertrandgorge)", 8 | "Brent Laabs (labster)", 9 | "Caleb Mingle (dentafrice)", 10 | "paladox", 11 | "Ulrich Strauss (c0nnex)", 12 | "tetsuya-zama", 13 | "yoshida" 14 | ], 15 | "type": "parserhook", 16 | "url": "https://www.mediawiki.org/wiki/Extension:LinkTitles", 17 | "version": "8.1.1", 18 | "license-name": "GPL-2.0+", 19 | "descriptionmsg": "linktitles-desc", 20 | "requires": { 21 | "MediaWiki": ">= 1.35.0" 22 | }, 23 | "config": { 24 | "LinkTitlesParseOnEdit": true, 25 | "LinkTitlesParseOnRender": false, 26 | "LinkTitlesParseHeadings": false, 27 | "LinkTitlesSkipTemplates": true, 28 | "LinkTitlesPreferShortTitles": true, 29 | "LinkTitlesCheckRedirect": true, 30 | "LinkTitlesEnableNoTargetMagicWord": false, 31 | "LinkTitlesMinimumTitleLength": 4, 32 | "LinkTitlesBlackList": [], 33 | "LinkTitlesFirstOnly": true, 34 | "LinkTitlesSmartMode": true, 35 | "LinkTitlesWordStartOnly": true, 36 | "LinkTitlesWordEndOnly": true, 37 | "LinkTitlesSpecialPageReloadAfter": 1, 38 | "LinkTitlesSourceNamespaces": [], 39 | "LinkTitlesTargetNamespaces": [], 40 | "LinkTitlesSameNamespace": true 41 | }, 42 | "AutoloadNamespaces": { 43 | "LinkTitles\\": "includes/" 44 | }, 45 | "AutoloadClasses": { 46 | "LinkTitles\\Extension": "includes/Extension.php", 47 | "LinkTitles\\Linker": "includes/Linker.php", 48 | "LinkTitles\\Source": "includes/Source.php", 49 | "LinkTitles\\Target": "includes/Target.php", 50 | "LinkTitles\\Targets": "includes/Targets.php", 51 | "LinkTitles\\Splitter": "includes/Splitter.php", 52 | "LinkTitles\\Config": "includes/Config.php", 53 | "LinkTitles\\Special": "includes/Special.php", 54 | "LinkTitles\\TestCase": "tests/phpunit/TestCase.php" 55 | }, 56 | "SpecialPages": { 57 | "LinkTitles": "LinkTitles\\Special" 58 | }, 59 | "AvailableRights": [ 60 | "linktitles-batch" 61 | ], 62 | "GroupPermissions": { 63 | "sysop": { 64 | "linktitles-batch": true 65 | } 66 | }, 67 | "Hooks": { 68 | "MultiContentSave": [ 69 | "LinkTitles\\Extension::onMultiContentSave" 70 | ], 71 | "InternalParseBeforeLinks": [ 72 | "LinkTitles\\Extension::onInternalParseBeforeLinks" 73 | ], 74 | "GetDoubleUnderscoreIDs": [ 75 | "LinkTitles\\Extension::onGetDoubleUnderscoreIDs" 76 | ], 77 | "ParserFirstCallInit": [ 78 | "LinkTitles\\Extension::onParserFirstCallInit" 79 | ] 80 | }, 81 | "ExtensionMessagesFiles": { 82 | "LinkTitlesMagic": "includes/Magic.php" 83 | }, 84 | "MessagesDirs": { 85 | "LinkTitles": [ 86 | "i18n" 87 | ] 88 | }, 89 | "manifest_version": 1 90 | } 91 | -------------------------------------------------------------------------------- /tests/phpunit/ExtensionTest.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | 24 | /** 25 | * @group bovender 26 | * @group Database 27 | */ 28 | 29 | use MediaWiki\Title\Title; 30 | use MediaWiki\Page\WikiPageFactory; 31 | use MediaWiki\MediaWikiServices; 32 | 33 | class ExtensionTest extends LinkTitles\TestCase { 34 | 35 | /** 36 | * @dataProvider provideParseOnEditData 37 | */ 38 | public function testParseOnEdit( $parseOnEdit, $input, $expectedOutput) { 39 | $this->setMwGlobals( [ 40 | 'wgLinkTitlesParseOnEdit' => $parseOnEdit, 41 | 'wgLinkTitlesParseOnRender' => !$parseOnEdit 42 | ] ); 43 | $pageId = $this->insertPage( 'test page', $input )['id']; 44 | $title = Title::newFromID( $pageId ); 45 | $wpf = MediaWikiServices::getInstance()->getWikiPageFactory(); 46 | $page = $wpf->newFromTitle( $title ); 47 | $this->assertSame( $expectedOutput, self::getPageText( $page ) ); 48 | } 49 | 50 | public function provideParseOnEditData() { 51 | return [ 52 | [ 53 | true, // parseOnEdit 54 | 'This page should link to the link target but not to test page', 55 | 'This page should link to the [[link target]] but not to test page' 56 | ], 57 | [ 58 | false, // parseOnEdit 59 | 'This page should *not* link to the link target', 60 | 'This page should *not* link to the link target' 61 | ], 62 | [ 63 | true, // parseOnEdit 64 | 'With __NOAUTOLINKS__, this page should not link to the link target', 65 | 'With __NOAUTOLINKS__, this page should not link to the link target' 66 | ], 67 | ]; 68 | } 69 | 70 | 71 | /** 72 | * @dataProvider provideParseOnRenderData 73 | */ 74 | public function testParseOnRender( $parseOnRender, $input, $expectedOutput) { 75 | $this->setMwGlobals( [ 76 | 'wgLinkTitlesParseOnEdit' => false, // do not modify the page as we create it 77 | 'wgLinkTitlesParseOnRender' => $parseOnRender 78 | ] ); 79 | $title = $this->insertPage( 'test page', $input )['title']; 80 | $page = new WikiPage( $title ); 81 | $userFactory = MediaWikiServices::getInstance()->getUserFactory(); 82 | $user = $userFactory->newAnonymous(); 83 | $output = $page->getParserOutput( new ParserOptions( $user ), null, true ); 84 | $lines = explode( "\n", $output->getRawText() ); 85 | $this->assertRegexp( $expectedOutput, $lines[0] ); 86 | } 87 | 88 | public function provideParseOnRenderData() { 89 | return [ 90 | [ 91 | true, // parseOnRender 92 | 'This page should link to the link target but not to the test page', 93 | '_This page should link to the ]+>link target but not to the test page_' 94 | ], 95 | [ 96 | false, // parseOnRender 97 | 'This page should not link to the link target', 98 | '_This page should not link to the link target_' 99 | ], 100 | [ 101 | true, // parseOnRender 102 | '__NOAUTOLINKS__With noautolinks magic word, this page should not link to the link target', 103 | '_With noautolinks magic word, this page should not link to the link target_' 104 | ], 105 | [ 106 | true, // parseOnRender 107 | '__NOAUTOLINKS__With noautolinks magic word, link target in autolinks tag should be linked', 108 | '_With noautolinks magic word, ]+>link target in autolinks tag should be linked_' 109 | ], 110 | [ 111 | true, // parseOnRender 112 | 'In a noautolinks tag, link target should NOT be linked', 113 | '_In a noautolinks tag, link target should NOT be linked_' 114 | ], 115 | [ 116 | true, // parseOnRender 117 | 'In a noautolinks tag, link target in autolinks tag should be linked', 118 | '_In a noautolinks tag, ]+>link target in autolinks tag should be linked_' 119 | ], 120 | ]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/phpunit/SplitterTest.php: -------------------------------------------------------------------------------- 1 | ('bovender') 5 | * 6 | * This program is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 19 | * MA 02110-1301, USA. 20 | * 21 | * @author Daniel Kraus 22 | */ 23 | 24 | /** 25 | * Tests the LinKTitles\Splitter class. 26 | * 27 | * @group bovender 28 | */ 29 | 30 | class SplitterTest extends LinkTitles\TestCase { 31 | /** 32 | * @dataProvider provideSplitData 33 | */ 34 | public function testSplit($skipTemplates, $parseHeadings, $input, $expectedOutput) 35 | { 36 | $config = new LinkTitles\Config(); 37 | $config->skipTemplates = $skipTemplates; 38 | $config->parseHeadings = $parseHeadings; 39 | LinkTitles\Splitter::invalidate(); 40 | $splitter = LinkTitles\Splitter::singleton($config); 41 | $this->assertSame($skipTemplates, $splitter->config->skipTemplates, 'Splitter has incorrect skipTemplates config'); 42 | $this->assertSame($parseHeadings, $splitter->config->parseHeadings, 'Splitter has incorrect parseHeadings config'); 43 | $this->assertSame($expectedOutput, $splitter->split($input)); 44 | } 45 | 46 | // TODO: Add more examples. 47 | public static function provideSplitData() 48 | { 49 | return [ 50 | [ 51 | true, // skipTemplates 52 | false, // parseHeadings 53 | 'this may be linked [[this may not be linked]]', 54 | ['this may be linked ', '[[this may not be linked]]', ''] 55 | ], 56 | [ 57 | true, // skipTemplates 58 | false, // parseHeadings 59 | 'this may be linked this may not be linked', 60 | ['this may be linked ', 'this may not be linked', ''] 61 | ], 62 | [ 63 | true, // skipTemplates 64 | false, // parseHeadings 65 | 'With skipTemplates = true, this may be linked {{mytemplate|param=link target}}', 66 | ['With skipTemplates = true, this may be linked ', '{{mytemplate|param=link target}}', ''] 67 | ], 68 | [ 69 | false, // skipTemplates 70 | false, // parseHeadings 71 | 'With skipTemplates = false, this may be linked {{mytemplate|param=link target}}', 72 | ['With skipTemplates = false, this may be linked ', '{{mytemplate|param=', 'link target}}'] 73 | ], 74 | [ 75 | true, // skipTemplates 76 | false, // parseHeadings 77 | 'With skipTemplates = true, this may be linked {{mytemplate|param={{transcluded}}}}', 78 | ['With skipTemplates = true, this may be linked ', '{{mytemplate|param={{transcluded}}}}', ''] 79 | ], 80 | [ 81 | true, // skipTemplates 82 | true, // parseHeadings 83 | "With parseHeadings = true,\n==a heading may be linked==\n", 84 | ["With parseHeadings = true,\n==a heading may be linked==\n"] 85 | ], 86 | [ 87 | true, // skipTemplates 88 | false, // parseHeadings 89 | // no trailing newline in the following string because it would be swallowed 90 | "With parseHeadings = false,\n==a heading may not be linked==", 91 | ["With parseHeadings = false,\n", "==a heading may not be linked==", ''] 92 | ], 93 | [ 94 | true, // skipTemplates 95 | true, // parseHeadings 96 | "With parseHeadings = true,\n==a heading with spans may be linked==\n", 97 | ["With parseHeadings = true,\n==", "", "a heading with spans may be linked", "", "==\n"] 98 | ], 99 | [ 100 | true, // skipTemplates 101 | true, // parseHeadings 102 | "With parseHeadings = true,\n==
a heading with divs may be linked
==\n", 103 | ["With parseHeadings = true,\n==", "
", "a heading with divs may be linked", "
", "==\n"] 104 | ], 105 | // Improperly formatted headings cannot be dealt with appropriately for now 106 | // [ 107 | // true, // skipTemplates 108 | // false, // parseHeadings 109 | // "With parseHeadings = false,\n==an improperly formatted heading may be linked=\n", 110 | // [ "With parseHeadings = false,\n==an improperly formatted heading may be linked=\n" ] 111 | // ], 112 | [ 113 | true, // skipTemplates 114 | true, // parseHeadings 115 | "Text in noautolinks tagshould be excluded", 116 | ["Text ", "in noautolinks tag", "should be excluded"] 117 | ], 118 | ]; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /includes/Targets.php: -------------------------------------------------------------------------------- 1 | ('bovender') 7 | * 8 | * This program is free software; you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation; either version 2 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with this program; if not, write to the Free Software 20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | * MA 02110-1301, USA. 22 | * 23 | * @author Daniel Kraus 24 | */ 25 | namespace LinkTitles; 26 | 27 | use MediaWiki\Title\Title as MWTitle; 28 | use MediaWiki\MediaWikiServices; 29 | 30 | /** 31 | * Fetches potential target page titles from the database. 32 | */ 33 | class Targets { 34 | private static $instance; 35 | 36 | /** 37 | * Singleton factory that returns a (cached) database query results with 38 | * potential target page titles. 39 | * 40 | * The subset of pages that may serve as target pages depends on the namespace 41 | * of the source page. Therefore, if the $sourceNamespace differs from the 42 | * cached namespace, the database is queried again. 43 | * 44 | * @param String $sourceNamespace The namespace of the current page. 45 | * @param Config $config LinkTitles configuration. 46 | */ 47 | public static function singleton( MWTitle $title, Config $config ) { 48 | if ( ( self::$instance === null ) || ( self::$instance->sourceNamespace != $title->getNamespace() ) ) { 49 | self::$instance = new Targets( $title, $config ); 50 | } 51 | return self::$instance; 52 | } 53 | 54 | /** 55 | * Invalidates the cache; the next call of Targets::singleton() will trigger 56 | * a database query. 57 | * 58 | * Use this in unit tests which are performed in a single request cycle so that 59 | * changes to the pages list may not be picked up by the cached Targets instance. 60 | */ 61 | public static function invalidate() { 62 | self::$instance = null; 63 | } 64 | 65 | /** 66 | * Holds the results of a database query for target page titles, filtered 67 | * and sorted. 68 | * @var IResultWrapper $queryResult 69 | */ 70 | public $queryResult; 71 | 72 | /** 73 | * Holds the source page's namespace (integer) for which the list of target 74 | * pages was built. 75 | * @var Int $sourceNamespace 76 | */ 77 | public $sourceNamespace; 78 | 79 | private $config; 80 | 81 | /** 82 | * Stores the CHAR_LENGTH function to be used with the database connection. 83 | * @var string $charLengthFunction 84 | */ 85 | private $charLengthFunction; 86 | 87 | /** 88 | * The constructor is private to enforce using the singleton pattern. 89 | * @param MWTitle $title 90 | */ 91 | private function __construct( MWTitle $title, Config $config) { 92 | $this->config = $config; 93 | $this->sourceNamespace = $title->getNamespace(); 94 | $this->fetch(); 95 | } 96 | 97 | // 98 | /** 99 | * Fetches the page titles from the database. 100 | */ 101 | private function fetch() { 102 | ( $this->config->preferShortTitles ) ? $sortOrder = 'ASC' : $sortOrder = 'DESC'; 103 | 104 | // Build a blacklist of pages that are not supposed to be link 105 | // targets. This includes the current page. 106 | if ( $this->config->blackList ) { 107 | $blackList = 'page_title NOT IN ' . 108 | str_replace( ' ', '_', '("' . implode( '","', str_replace( '"', '\"', $this->config->blackList ) ) . '")' ); 109 | } else { 110 | $blackList = null; 111 | } 112 | 113 | if ( $this->config->sameNamespace ) { 114 | // Build our weight list. Make sure current namespace is first element 115 | $namespaces = array_diff( $this->config->targetNamespaces, [ $this->sourceNamespace ] ); 116 | array_unshift( $namespaces, $this->sourceNamespace ); 117 | } else { 118 | $namespaces = $this->config->targetNamespaces; 119 | } 120 | 121 | if ( !$namespaces) { 122 | // If there are absolutely no target namespaces (not even the one of the 123 | // source page), we can just return. 124 | return; 125 | } 126 | 127 | // No need for sanitiy check. we are sure that we have at least one element in the array 128 | $weightSelect = "CASE page_namespace "; 129 | $currentWeight = 0; 130 | foreach ($namespaces as &$namespaceValue) { 131 | $currentWeight = $currentWeight + 100; 132 | $weightSelect = $weightSelect . " WHEN " . $namespaceValue . " THEN " . $currentWeight . PHP_EOL; 133 | } 134 | $weightSelect = $weightSelect . " END "; 135 | $namespacesClause = '(' . implode( ', ', $namespaces ) . ')'; 136 | 137 | // Build an SQL query and fetch all page titles ordered by length from 138 | // shortest to longest. Only titles from 'normal' pages (namespace uid 139 | // = 0) are returned. Since the db may be sqlite, we need a try..catch 140 | // structure because sqlite does not support the CHAR_LENGTH function. 141 | $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA ); 142 | $this->queryResult = $dbr->select( 143 | 'page', 144 | array( 'page_title', 'page_namespace' , "weight" => $weightSelect), 145 | array_filter( 146 | array( 147 | 'page_namespace IN ' . $namespacesClause, 148 | $this->charLength() . '(page_title) >= ' . $this->config->minimumTitleLength, 149 | $blackList, 150 | ) 151 | ), 152 | __METHOD__, 153 | array( 'ORDER BY' => 'weight ASC, ' . $this->charLength() . '(page_title) ' . $sortOrder ) 154 | ); 155 | } 156 | 157 | private function charLength() { 158 | if ($this->charLengthFunction === null) { 159 | $this->charLengthFunction = $this->config->sqliteDatabase() ? 'LENGTH' : 'CHAR_LENGTH'; 160 | } 161 | return $this->charLengthFunction; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /includes/Splitter.php: -------------------------------------------------------------------------------- 1 | ('bovender') 7 | * 8 | * This program is free software; you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation; either version 2 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * This program is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with this program; if not, write to the Free Software 20 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | * MA 02110-1301, USA. 22 | * 23 | * @author Daniel Kraus 24 | */ 25 | namespace LinkTitles; 26 | 27 | /** 28 | * Caches a regular expression that delimits text to be parsed. 29 | */ 30 | class Splitter { 31 | /** 32 | * The splitting expression that separates text to be parsed from text that 33 | * must not be parsed. 34 | * @var String $splitter 35 | */ 36 | public $splitter; 37 | 38 | /** 39 | * The LinkTitles configuration for this Splitter instance. 40 | * @var Config $config 41 | */ 42 | public $config; 43 | 44 | private static $instance; 45 | 46 | /** 47 | * Gets the Splitter singleton; may build one with the given config or the 48 | * default config if none is given. 49 | * 50 | * If the instance was already created, it does not matter what Config this 51 | * method is called with. To re-create an instance with a different Config, 52 | * call Splitter::invalidate() first. 53 | * 54 | * @param Config|null $config LinkTitles configuration. 55 | */ 56 | public static function singleton( Config &$config = null ) { 57 | if ( self::$instance === null ) { 58 | if ( $config === null ) { 59 | $config = new Config(); 60 | } 61 | self::$instance = new Splitter( $config ); 62 | } 63 | return self::$instance; 64 | } 65 | 66 | /** 67 | * Invalidates the singleton instance. 68 | * 69 | * Used for unit testing. 70 | */ 71 | public static function invalidate() { 72 | self::$instance = null; 73 | } 74 | 75 | protected function __construct( Config $config) { 76 | $this->config = $config; 77 | $this->buildSplitter(); 78 | } 79 | 80 | /** 81 | * Splits a text into sections that may be linked and sections that may not 82 | * be linked (e.g., because they already are a link, or a template, etc.). 83 | * 84 | * @param String &$text Text to split. 85 | * @return Array of strings where even indexes point to linkable sections. 86 | */ 87 | public function split( &$text ) { 88 | return preg_split( $this->splitter, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); 89 | } 90 | 91 | /* 92 | * Builds the delimiter that is used in a regexp to separate 93 | * text that should be parsed from text that should not be 94 | * parsed (e.g. inside existing links etc.) 95 | */ 96 | private function buildSplitter() { 97 | if ( $this->config->skipTemplates ) 98 | { 99 | // Use recursive regex to balance curly braces; 100 | // see http://www.regular-expressions.info/recurse.html 101 | $templatesDelimiter = '{{(?>[^{}]|(?R))*}}|'; 102 | } else { 103 | // Match template names (ignoring any piped [[]] links in them) 104 | // along with the trailing pipe and parameter name or closing 105 | // braces; also match sequences of '|wordcharacters=' (without 106 | // spaces in them) that usually only occur as parameter names in 107 | // transclusions (but could also occur as wiki table cell contents). 108 | // TODO: Find a way to match parameter names in transclusions, but 109 | // not in table cells or other sequences involving a pipe character 110 | // and equal sign. 111 | $templatesDelimiter = '{{[^|]*?(?:(?:\[\[[^]]+]])?)[^|]*?(?:\|(?:\w+=)?|(?:}}))|\|\w+=|'; 112 | } 113 | 114 | // Build a regular expression that will capture existing wiki links ("[[...]]"), 115 | // wiki headings ("= ... =", "== ... ==" etc.), 116 | // urls ("http://example.com", "[http://example.com]", "[http://example.com Description]", 117 | // and email addresses ("mail@example.com"). 118 | 119 | // Match WikiText headings. 120 | // Since there is a user option to skip headings, we make this part of the 121 | // expression optional. Note that in order to use preg_split(), it is 122 | // important to have only one capturing subpattern (which precludes the use 123 | // of conditional subpatterns). 124 | // Caveat: This regex pattern should be improved to deal with balanced '='s 125 | // only. However, this would require grouping in the pattern which does not 126 | // agree with preg_split. 127 | $headingsDelimiter = $this->config->parseHeadings ? '' : '^=+[^=]+=+$|'; 128 | 129 | $urlPattern = '[a-z]+?\:\/\/(?:\S+\.)+\S+(?:\/.*)?'; 130 | $this->splitter = '/(' . // exclude from linking: 131 | '\[\[.*?\]\]|' . // links 132 | $headingsDelimiter . // headings (if requested) 133 | $templatesDelimiter . // templates (if requested) 134 | '^ .+?\n|\n .+?\n|\n .+?$|^ .+?$|' . // preformatted text 135 | '.*?<.nowiki>|.*?<\/code>|' . // nowiki/code 136 | '
.*?<\/pre>|.*?<\/html>|' .      // pre/html
137 | 			'
237 | EOF
238 | 		;
239 | 	}
240 | 
241 |   /**
242 | 	 * Adds statistics to the page when all processing is done.
243 | 	 * @param $output  Output object
244 | 	 * @param $start   Index of the first page that was processed.
245 | 	 * @param $end     Index of the last processed page.
246 | 	 * @param $reloads Number of reloads of the page.
247 | 	 * @return undefined
248 | 	 */
249 | 	private function addCompletedInfo( &$output, $start, $end, $reloads ) {
250 | 		if ( $reloads > 0 ) {
251 | 			$pagesPerReload = sprintf( '%0.1f', $end / $reloads );
252 | 		}
253 | 		else
254 | 		{
255 | 			$pagesPerReload = sprintf( '%0.1f', $end );
256 | 		}
257 | 
258 | 		$output->addWikiMsg( 'linktitles-special-completed-info', $end,
259 | 			$config->specialPageReloadAfter, $reloads, $pagesPerReload
260 | 		);
261 | 	}
262 | 
263 | 	/**
264 | 	 * Counts the number of pages in a read-access wiki database ($dbr).
265 | 	 * @param $dbr Read-only `Database` object.
266 | 	 * @return Number of pages in the default namespace (0) of the wiki.
267 | 	 */
268 | 	private function countPages( &$dbr, $namespacesClause ) {
269 | 		$res = $dbr->select(
270 | 			'page',
271 | 			array('pagecount' => "COUNT(page_id)"),
272 | 			array(
273 | 				'page_namespace IN ' . $namespacesClause,
274 | 			),
275 | 			__METHOD__
276 | 		);
277 | 
278 | 		return $res->current()->pagecount;
279 | 	}
280 | }
281 | 
282 | // vim: ts=2:sw=2:noet:comments^=\:///
283 | 


--------------------------------------------------------------------------------
/tests/phpunit/LinkerTest.php:
--------------------------------------------------------------------------------
  1 |  ('bovender')
  7 |  *
  8 |  * This program is free software; you can redistribute it and/or modify
  9 |  * it under the terms of the GNU General Public License as published by
 10 |  * the Free Software Foundation; either version 2 of the License, or
 11 |  * (at your option) any later version.
 12 |  *
 13 |  * This program is distributed in the hope that it will be useful,
 14 |  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 |  * GNU General Public License for more details.
 17 |  *
 18 |  * You should have received a copy of the GNU General Public License
 19 |  * along with this program; if not, write to the Free Software
 20 |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 21 |  * MA 02110-1301, USA.
 22 |  *
 23 |  * @author Daniel Kraus 
 24 |  */
 25 | 
 26 | /**
 27 |  * Unit tests for the LinkTitles\Linker class.
 28 |  *
 29 |  * The test class is prefixed with 'LinkTitles' to avoid a naming collision
 30 |  * with a class that exists in the MediaWiki core.
 31 |  *
 32 |  * (Ideally the test classes should be namespaced, but when you do that, they
 33 |  * will no longer be automatically discovered.)
 34 |  *
 35 |  * @group bovender
 36 |  * @group Database
 37 |  */
 38 | 
 39 | use MediaWiki\MediaWikiServices;
 40 | 
 41 | class LinkTitlesLinkerTest extends LinkTitles\TestCase {
 42 | 	protected $title;
 43 | 
 44 | 	protected function setUp(): void
 45 | 	{
 46 | 		parent::setUp(); // call last to have the Targets object invalidated after inserting the page
 47 | 	}
 48 | 
 49 | 	public function addDBData() {
 50 | 		$this->title = $this->insertPage( 'source page', 'This page is the test page' )['title'];
 51 | 		$this->insertPage( 'link target', 'This page serves as a link target' );
 52 | 		parent::addDBDataOnce(); // call parent after adding page to have targets invalidated
 53 | 	}
 54 | 
 55 | 	/**
 56 | 	 * @dataProvider provideTestTitleWithNumberData
 57 | 	 */
 58 | 	public function testTitleWithNumber( $input, $expectedOutput ) {
 59 | 		$config = new LinkTitles\Config();
 60 | 		$config->wordStartOnly = true;
 61 | 		$config->wordEndOnly = true;
 62 | 		$this->insertPage( 'numbered-1', 'This page serves as a link target with a numbered title' );
 63 | 		$this->insertPage( 'numbered-101', 'This page serves as a link target with a numbered title' );
 64 | 		parent::addDBDataOnce(); // call parent after adding page to have targets invalidated
 65 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
 66 | 		$linker = new LinkTitles\Linker( $config );
 67 | 		$result = $linker->linkContent( $source );
 68 | 		if ( !$result ) { $result = $input; }
 69 | 		$this->assertSame( $expectedOutput, $result );
 70 | 	}
 71 | 
 72 | 	public function provideTestTitleWithNumberData() {
 73 | 		return [
 74 | 			[
 75 | 				"Page text with numbered-1 in it.",
 76 | 				"Page text with [[numbered-1]] in it.",
 77 | 			],
 78 | 			[
 79 | 				"Page text with numbered-101 in it.",
 80 | 				"Page text with [[numbered-101]] in it.",
 81 | 			],
 82 | 			[
 83 | 				"Page text with numbered-1010 in it.",
 84 | 				"Page text with numbered-1010 in it.",
 85 | 			],
 86 | 			[
 87 | 				"Page text with link target1 in it.",
 88 | 				"Page text with link target1 in it.",
 89 | 			],
 90 | 		];
 91 | 	}
 92 | 
 93 | 	/**
 94 | 	 * Test issue #39, https://github.com/bovender/LinkTitles/issues/39
 95 | 	 */
 96 | 	public function testNoautolinks() {
 97 | 		$config = new LinkTitles\Config();
 98 | 		$config->firstOnly = false;
 99 | 		LinkTitles\Splitter::invalidate();
100 | 		$input = 'This is a text with link target';
101 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
102 | 		$linker = new LinkTitles\Linker( $config );
103 | 		$result = $linker->linkContent( $source );
104 | 		if ( !$result ) { $result = $input; }
105 | 		$this->assertSame( $input, $result );
106 | 	}
107 | 
108 | 	/**
109 | 	 * @dataProvider provideLinkContentTemplatesData
110 | 	 */
111 | 	public function testLinkContentTemplates( $skipTemplates, $input, $expectedOutput ) {
112 | 		$config = new LinkTitles\Config();
113 | 		$config->firstOnly = false;
114 | 		$config->skipTemplates = $skipTemplates;
115 | 		LinkTitles\Splitter::invalidate();
116 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
117 | 		$linker = new LinkTitles\Linker( $config );
118 | 		$result = $linker->linkContent( $source );
119 | 		if ( !$result ) { $result = $input; }
120 | 		$this->assertSame( $expectedOutput, $result );
121 | 	}
122 | 
123 | 	public function provideLinkContentTemplatesData() {
124 | 		return [
125 | 			[
126 | 				true, // skipTemplates
127 | 				'With skipTemplates = true, a {{template|with=link target}} in it should not be linked',
128 | 				'With skipTemplates = true, a {{template|with=link target}} in it should not be linked',
129 | 			],
130 | 			[
131 | 				false, // skipTemplates
132 | 				'With skipTemplates = false, a {{template|with=link target}} in it should be linked',
133 | 				'With skipTemplates = false, a {{template|with=[[link target]]}} in it should be linked',
134 | 			],
135 | 			[
136 | 				false, // skipTemplates
137 | 				'With skipTemplates = false, a {{template|with=already linked [[link target]]}} in it should not be linked again',
138 | 				'With skipTemplates = false, a {{template|with=already linked [[link target]]}} in it should not be linked again',
139 | 			]
140 | 		];
141 | 	}
142 | 
143 | 	/**
144 | 	 * @dataProvider provideLinkContentSmartModeData
145 | 	 */
146 | 	public function testLinkContentSmartMode( $capitalLinks, $smartMode, $input, $expectedOutput ) {
147 | 		$this->setMwGlobals( 'wgCapitalLinks', $capitalLinks );
148 | 		$config = new LinkTitles\Config();
149 | 		$config->firstOnly = false;
150 | 		$config->smartMode = $smartMode;
151 | 		$linker = new LinkTitles\Linker( $config );
152 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
153 | 		$result = $linker->linkContent( $source );
154 | 		if ( !$result ) { $result = $input; }
155 | 		$this->assertSame( $expectedOutput, $result );
156 | 	}
157 | 
158 | 	public function provideLinkContentSmartModeData() {
159 | 		return [
160 | 			[
161 | 				true, // wgCapitalLinks
162 | 				true, // smartMode
163 | 				'With smart mode on and $wgCapitalLinks = true, this page should link to link target',
164 | 				'With smart mode on and $wgCapitalLinks = true, this page should link to [[link target]]'
165 | 			],
166 | 			[
167 | 				true, // wgCapitalLinks
168 | 				false, // smartMode
169 | 				'With smart mode off and $wgCapitalLinks = true, this page should link to link target',
170 | 				'With smart mode off and $wgCapitalLinks = true, this page should link to [[link target]]'
171 | 			],
172 | 			[
173 | 				true, // wgCapitalLinks
174 | 				true, // smartMode
175 | 				'With smart mode on and $wgCapitalLinks = true, this page should link to Link target',
176 | 				'With smart mode on and $wgCapitalLinks = true, this page should link to [[Link target]]'
177 | 			],
178 | 			[
179 | 				true, // wgCapitalLinks
180 | 				false, // smartMode
181 | 				'With smart mode off and $wgCapitalLinks = true, this page should not link to Link Target',
182 | 				'With smart mode off and $wgCapitalLinks = true, this page should not link to Link Target'
183 | 			],
184 | 			[
185 | 				false, // wgCapitalLinks
186 | 				true, // smartMode
187 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to Link target',
188 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to [[Link target]]'
189 | 			],
190 | 			[
191 | 				false, // wgCapitalLinks
192 | 				true, // smartMode
193 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to link target',
194 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to [[Link target|link target]]'
195 | 			],
196 | 			[
197 | 				false, // wgCapitalLinks
198 | 				false, // smartMode
199 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to link target',
200 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to link target'
201 | 			],
202 | 			[
203 | 				false, // wgCapitalLinks
204 | 				false, // smartMode
205 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to Link target',
206 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to [[Link target]]'
207 | 			],
208 | 			[
209 | 				false, // wgCapitalLinks
210 | 				true, // smartMode
211 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to Link target',
212 | 				'With smart mode on and $wgCapitalLinks = false, this page should link to [[Link target]]'
213 | 			],
214 | 			[
215 | 				false, // wgCapitalLinks
216 | 				false, // smartMode
217 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to Link Target',
218 | 				'With smart mode off and $wgCapitalLinks = false, this page should not link to Link Target'
219 | 			],
220 | 		];
221 | 	}
222 | 
223 | 	/**
224 | 	 * @dataProvider provideLinkContentFirstOnlyData
225 | 	 */
226 | 	public function testLinkContentFirstOnly( $firstOnly, $input, $expectedOutput ) {
227 | 		$config = new LinkTitles\Config();
228 | 		$config->firstOnly = $firstOnly;
229 | 		$linker = new LinkTitles\Linker( $config );
230 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
231 | 		$result = $linker->linkContent( $source );
232 | 		if ( !$result ) { $result = $input; }
233 | 		$this->assertSame( $expectedOutput, $result );
234 | 	}
235 | 
236 | 	public function provideLinkContentFirstOnlyData() {
237 | 		return [
238 | 			[
239 | 				false, // firstOnly
240 | 				'With firstOnly = false, link target is a link target multiple times',
241 | 				'With firstOnly = false, [[link target]] is a [[link target]] multiple times'
242 | 			],
243 | 			[
244 | 				false, // firstOnly
245 | 				'With firstOnly = false, [[link target]] is a link target multiple times',
246 | 				'With firstOnly = false, [[link target]] is a [[link target]] multiple times'
247 | 			],
248 | 			[
249 | 				true, // firstOnly
250 | 				'With firstOnly = true, link target is a link target only once',
251 | 				'With firstOnly = true, [[link target]] is a link target only once'
252 | 			],
253 | 			[
254 | 				true, // firstOnly
255 | 				'With firstOnly = true, [[link target]] is a link target only once',
256 | 				'With firstOnly = true, [[link target]] is a link target only once'
257 | 			],
258 | 		];
259 | 	}
260 | 
261 | 	/**
262 | 	 * @dataProvider provideLinkContentHeadingsData
263 | 	 */
264 | 	public function testLinkContentHeadings( $parseHeadings, $input, $expectedOutput ) {
265 | 		$config = new LinkTitles\Config();
266 | 		$config->parseHeadings = $parseHeadings;
267 | 		LinkTitles\Splitter::invalidate();
268 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
269 | 		$linker = new LinkTitles\Linker( $config );
270 | 		$result = $linker->linkContent( $source );
271 | 		if ( !$result ) { $result = $input; }
272 | 		$this->assertSame( $expectedOutput, $result );
273 | 	}
274 | 
275 | 	public function provideLinkContentHeadingsData() {
276 | 		return [
277 | 			[
278 | 				true, // parseHeadings
279 | 				"With parseHeadings = true,\n== a heading with link target in it ==\n should be linked",
280 | 				"With parseHeadings = true,\n== a heading with [[link target]] in it ==\n should be linked",
281 | 			],
282 | 			[
283 | 				true, // parseHeadings
284 | 				"With parseHeadings = true,\n== a heading with link target in ity/span> ==\n should be linked",
285 | 				"With parseHeadings = true,\n== a heading with [[link target]] in ity/span> ==\n should be linked",
286 | 			],
287 | 			[
288 | 				false, // parseHeadings
289 | 				"With parseHeadings = false,\n== a heading with link target in it ==\n should not be linked",
290 | 				"With parseHeadings = false,\n== a heading with link target in it ==\n should not be linked",
291 | 			],
292 | 		];
293 | 	}
294 | 
295 | 	public function testLinkContentBlackList() {
296 | 		$config = new LinkTitles\Config();
297 | 		$config->blackList = [ 'Foo', 'Link target', 'Bar' ];
298 | 		LinkTitles\Targets::invalidate();
299 | 		$linker = new LinkTitles\Linker( $config );
300 | 		$text = 'If the link target is blacklisted, it should not be linked';
301 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $text, $config );
302 | 		$result = $linker->linkContent( $source );
303 | 		if ( !$result ) { $result = $text; }
304 | 		$this->assertSame( $text, $result );
305 | 	}
306 | 
307 | 	// Tests for namespace handling are commented out until I find a way to add
308 | 	// a custom namespace during testing. (The assertTrue assertion below fails.)
309 | 
310 | 	/**
311 | 	 * @dataProvider provideLinkContentNamespacesData
312 | 	 */
313 | 	public function testLinkContentTargetNamespaces( $namespaces, $input, $expectedOutput ) {
314 | 		$config = new LinkTitles\Config();
315 | 		$config->targetNamespaces = $namespaces;
316 | 
317 | 		$ns = 4000;
318 | 		$nsText = 'customnamespace';
319 | 		$this->mergeMwGlobalArrayValue( 'wgExtraNamespaces', [ $ns => $nsText ] );
320 | 
321 | 		// Reset namespace caches.
322 | 		// See https://stackoverflow.com/q/45974979/270712
323 | 		$namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
324 | 		$namespaceInfo->getCanonicalNamespaces( true );
325 | 		$wgContLang = \MediaWiki\MediaWikiServices::getInstance()->getContentLanguage();
326 | 		$wgContLang->resetNamespaces();
327 | 		$this->assertTrue( $namespaceInfo->exists( $ns ), "The namespace with id $ns should exist!" );
328 | 
329 | 		$this->insertPage( "in custom namespace", 'This is a page in a custom namespace', $ns );
330 | 		LinkTitles\Targets::invalidate();
331 | 		$linker = new LinkTitles\Linker( $config );
332 | 		$source = LinkTitles\Source::createFromTitleAndText( $this->title, $input, $config );
333 | 		$result = $linker->linkContent( $source );
334 | 		if ( !$result ) { $result = $input; }
335 | 		$this->assertSame( $expectedOutput, $result );
336 | 	}
337 | 
338 | 	public function provideLinkContentNamespacesData() {
339 | 		return [
340 | 			[
341 | 				[], // namespaces
342 | 				'With targetNamespaces = [], page in custom namespace should not be linked',
343 | 				'With targetNamespaces = [], page in custom namespace should not be linked'
344 | 			],
345 | 			[
346 | 				[ 4000 ], // namespaces
347 | 				'With targetNamespaces = [ 4000 ], page in custom namespace should be linked',
348 | 				'With targetNamespaces = [ 4000 ], page [[customnamespace:In custom namespace|in custom namespace]] should be linked'
349 | 			],
350 | 		];
351 | 	}
352 | }
353 | 


--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
  1 |                     GNU GENERAL PUBLIC LICENSE
  2 |                        Version 2, June 1991
  3 | 
  4 |  Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
  5 |  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  6 |  Everyone is permitted to copy and distribute verbatim copies
  7 |  of this license document, but changing it is not allowed.
  8 | 
  9 |                             Preamble
 10 | 
 11 |   The licenses for most software are designed to take away your
 12 | freedom to share and change it.  By contrast, the GNU General Public
 13 | License is intended to guarantee your freedom to share and change free
 14 | software--to make sure the software is free for all its users.  This
 15 | General Public License applies to most of the Free Software
 16 | Foundation's software and to any other program whose authors commit to
 17 | using it.  (Some other Free Software Foundation software is covered by
 18 | the GNU Lesser General Public License instead.)  You can apply it to
 19 | your programs, too.
 20 | 
 21 |   When we speak of free software, we are referring to freedom, not
 22 | price.  Our General Public Licenses are designed to make sure that you
 23 | have the freedom to distribute copies of free software (and charge for
 24 | this service if you wish), that you receive source code or can get it
 25 | if you want it, that you can change the software or use pieces of it
 26 | in new free programs; and that you know you can do these things.
 27 | 
 28 |   To protect your rights, we need to make restrictions that forbid
 29 | anyone to deny you these rights or to ask you to surrender the rights.
 30 | These restrictions translate to certain responsibilities for you if you
 31 | distribute copies of the software, or if you modify it.
 32 | 
 33 |   For example, if you distribute copies of such a program, whether
 34 | gratis or for a fee, you must give the recipients all the rights that
 35 | you have.  You must make sure that they, too, receive or can get the
 36 | source code.  And you must show them these terms so they know their
 37 | rights.
 38 | 
 39 |   We protect your rights with two steps: (1) copyright the software, and
 40 | (2) offer you this license which gives you legal permission to copy,
 41 | distribute and/or modify the software.
 42 | 
 43 |   Also, for each author's protection and ours, we want to make certain
 44 | that everyone understands that there is no warranty for this free
 45 | software.  If the software is modified by someone else and passed on, we
 46 | want its recipients to know that what they have is not the original, so
 47 | that any problems introduced by others will not reflect on the original
 48 | authors' reputations.
 49 | 
 50 |   Finally, any free program is threatened constantly by software
 51 | patents.  We wish to avoid the danger that redistributors of a free
 52 | program will individually obtain patent licenses, in effect making the
 53 | program proprietary.  To prevent this, we have made it clear that any
 54 | patent must be licensed for everyone's free use or not licensed at all.
 55 | 
 56 |   The precise terms and conditions for copying, distribution and
 57 | modification follow.
 58 | 
 59 |                     GNU GENERAL PUBLIC LICENSE
 60 |    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 61 | 
 62 |   0. This License applies to any program or other work which contains
 63 | a notice placed by the copyright holder saying it may be distributed
 64 | under the terms of this General Public License.  The "Program", below,
 65 | refers to any such program or work, and a "work based on the Program"
 66 | means either the Program or any derivative work under copyright law:
 67 | that is to say, a work containing the Program or a portion of it,
 68 | either verbatim or with modifications and/or translated into another
 69 | language.  (Hereinafter, translation is included without limitation in
 70 | the term "modification".)  Each licensee is addressed as "you".
 71 | 
 72 | Activities other than copying, distribution and modification are not
 73 | covered by this License; they are outside its scope.  The act of
 74 | running the Program is not restricted, and the output from the Program
 75 | is covered only if its contents constitute a work based on the
 76 | Program (independent of having been made by running the Program).
 77 | Whether that is true depends on what the Program does.
 78 | 
 79 |   1. You may copy and distribute verbatim copies of the Program's
 80 | source code as you receive it, in any medium, provided that you
 81 | conspicuously and appropriately publish on each copy an appropriate
 82 | copyright notice and disclaimer of warranty; keep intact all the
 83 | notices that refer to this License and to the absence of any warranty;
 84 | and give any other recipients of the Program a copy of this License
 85 | along with the Program.
 86 | 
 87 | You may charge a fee for the physical act of transferring a copy, and
 88 | you may at your option offer warranty protection in exchange for a fee.
 89 | 
 90 |   2. You may modify your copy or copies of the Program or any portion
 91 | of it, thus forming a work based on the Program, and copy and
 92 | distribute such modifications or work under the terms of Section 1
 93 | above, provided that you also meet all of these conditions:
 94 | 
 95 |     a) You must cause the modified files to carry prominent notices
 96 |     stating that you changed the files and the date of any change.
 97 | 
 98 |     b) You must cause any work that you distribute or publish, that in
 99 |     whole or in part contains or is derived from the Program or any
100 |     part thereof, to be licensed as a whole at no charge to all third
101 |     parties under the terms of this License.
102 | 
103 |     c) If the modified program normally reads commands interactively
104 |     when run, you must cause it, when started running for such
105 |     interactive use in the most ordinary way, to print or display an
106 |     announcement including an appropriate copyright notice and a
107 |     notice that there is no warranty (or else, saying that you provide
108 |     a warranty) and that users may redistribute the program under
109 |     these conditions, and telling the user how to view a copy of this
110 |     License.  (Exception: if the Program itself is interactive but
111 |     does not normally print such an announcement, your work based on
112 |     the Program is not required to print an announcement.)
113 | 
114 | These requirements apply to the modified work as a whole.  If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works.  But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 | 
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 | 
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 | 
134 |   3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 | 
138 |     a) Accompany it with the complete corresponding machine-readable
139 |     source code, which must be distributed under the terms of Sections
140 |     1 and 2 above on a medium customarily used for software interchange; or,
141 | 
142 |     b) Accompany it with a written offer, valid for at least three
143 |     years, to give any third party, for a charge no more than your
144 |     cost of physically performing source distribution, a complete
145 |     machine-readable copy of the corresponding source code, to be
146 |     distributed under the terms of Sections 1 and 2 above on a medium
147 |     customarily used for software interchange; or,
148 | 
149 |     c) Accompany it with the information you received as to the offer
150 |     to distribute corresponding source code.  (This alternative is
151 |     allowed only for noncommercial distribution and only if you
152 |     received the program in object code or executable form with such
153 |     an offer, in accord with Subsection b above.)
154 | 
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it.  For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable.  However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 | 
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 | 
172 |   4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License.  Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 | 
180 |   5. You are not required to accept this License, since you have not
181 | signed it.  However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works.  These actions are
183 | prohibited by law if you do not accept this License.  Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 | 
189 |   6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions.  You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 | 
197 |   7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License.  If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all.  For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 | 
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 | 
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices.  Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 | 
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 | 
229 |   8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded.  In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 | 
237 |   9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time.  Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 | 
242 | Each version is given a distinguishing version number.  If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation.  If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 | 
250 |   10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission.  For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this.  Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 | 
258 |                             NO WARRANTY
259 | 
260 |   11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 | 
270 |   12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 | 
280 |                      END OF TERMS AND CONDITIONS
281 | 
282 |             How to Apply These Terms to Your New Programs
283 | 
284 |   If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 | 
288 |   To do so, attach the following notices to the program.  It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 | 
293 |     
294 |     Copyright (C)   
295 | 
296 |     This program is free software; you can redistribute it and/or modify
297 |     it under the terms of the GNU General Public License as published by
298 |     the Free Software Foundation; either version 2 of the License, or
299 |     (at your option) any later version.
300 | 
301 |     This program is distributed in the hope that it will be useful,
302 |     but WITHOUT ANY WARRANTY; without even the implied warranty of
303 |     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
304 |     GNU General Public License for more details.
305 | 
306 |     You should have received a copy of the GNU General Public License along
307 |     with this program; if not, write to the Free Software Foundation, Inc.,
308 |     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 | 
310 | Also add information on how to contact you by electronic and paper mail.
311 | 
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 | 
315 |     Gnomovision version 69, Copyright (C) year name of author
316 |     Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 |     This is free software, and you are welcome to redistribute it
318 |     under certain conditions; type `show c' for details.
319 | 
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License.  Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 | 
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary.  Here is a sample; alter the names:
328 | 
329 |   Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 |   `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 | 
332 |   , 1 April 1989
333 |   Ty Coon, President of Vice
334 | 
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs.  If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library.  If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | > **HEADS UP!**  
  2 | > **THE MAIN BRANCH HAS BEEN RENAMED FROM "MASTER" TO "MAIN".**  
  3 | > **TO UPDATE AN EXISTING LOCAL COPY OF THIS REPOSITORY:**  
  4 | > **`git fetch`**  
  5 | > **`git checkout -t origin/main`**
  6 | 
  7 | ---
  8 | 
  9 | # LinkTitles
 10 | 
 11 | [MediaWiki extension](https://www.mediawiki.org/wiki/Extension:LinkTitles) that
 12 | automatically adds links to words that match titles of existing pages.
 13 | 
 14 | Minimum requirement: MediaWiki version **1.35**.
 15 | 
 16 | ## Table of contents
 17 | 
 18 | 1. [Overview](#overview)
 19 |    - [Versions](#versions)
 20 | 2. [Installation](#installation)
 21 | 3. [Usage](#usage)
 22 |    - [Editing a page](#editing-a-page)
 23 |    - [Preventing automatic linking after minor edits](#preventing-automatic-linking-after-minor-edits)
 24 |    - [Viewing a page](#viewing-a-page)
 25 |    - [Including and excluding pages with Magic Words](#including-and-excluding-pages-with-magic-words)
 26 |    - [Enable or disable automatic linking for sections](#enable-or-disable-automatic-linking-for-sections)
 27 |    - [Namespace support](#namespace-support)
 28 |    - [Batch processing](#batch-processing)
 29 |    - [Special:LinkTitles](#special-linktitles)
 30 |    - [Maintenance script](#maintenance-script)
 31 | 4. [Configuration](#configuration)
 32 |    - [Linking when a page is edited and saved](#linking-when-a-page-is-edited-and-saved)
 33 |    - [Linking when a page is rendered for display](#linking-when-a-page-is-rendered-for-display)
 34 |    - [Enabling case-insensitive linking (smart mode)](#enabling-case-insensitive-linking-(smart-mode))
 35 |    - [Dealing with custom namespaces](#dealing-with-custom-namespaces)
 36 |    - [Linking or skipping headings](#linking-or-skipping-headings)
 37 |    - [Prioritizing pages with short titles](#prioritizing-pages-with-short-titles)
 38 |    - [Filtering pages by title length](#filtering-pages-by-title-length)
 39 |    - [Excluding pages from being linked to](#excluding-pages-from-being-linked-to)
 40 |    - [Dealing with templates](#dealing-with-templates)
 41 |    - [Multiple links to the same page](#multiple-links-to-the-same-page)
 42 |    - [Partial words](#partial-words)
 43 |    - [Special page configuration](#special-page-configuration)
 44 | 5. [Development](#development)
 45 |    - [Contributors](#contributors)
 46 |    - [Testing](#testing)
 47 | 6. [License](#license)
 48 | 
 49 | ## Overview
 50 | 
 51 | The **LinkTitles** extension automatically adds links to existing page titles
 52 | that occur on a given page. This will automatically cross-reference your wiki
 53 | for you. The extension can operate in three ways that can be used independently:
 54 | 
 55 | 1. Whenever a page is edited and saved, the extension will look if any existing
 56 | page titles occur in the text, and automatically add links (`[[...]]]`) to the
 57 | corresponding pages.
 58 | 
 59 | 2. Links may also be added on the fly whenever a page is rendered for display.
 60 | Most of the time, MediaWiki will fetch previously rendered pages from cache upon
 61 | a page request, but whenever a page is refreshed, the LinkTitles extension can
 62 | add its page links. These links are not hard-coded in the Wiki text. The
 63 | original content will not be modified.
 64 | 
 65 | 3. Batch mode enables Wiki administrators to process all pages in a Wiki at
 66 | once. Batch processing can either be started from a special page, or from the
 67 | server's command line (see [below](#batch-processing)).
 68 | 
 69 | ### Versions
 70 | 
 71 | This extension is [semantically versioned](http://semver.org). In short, this
 72 | means that the first version number (the 'major') only changes on substantial
 73 | changes. The second number (the 'minor') changes when features are added or
 74 | significantly improved. The third number (the 'patch level') changes when bugs
 75 | are fixed.
 76 | 
 77 | Version | Date | Major changes ||
 78 | -|-|-|-
 79 | 8 | 04-2021 | Minimum required version is 1.35. | [Details][v8.0.0]
 80 | 7 | 12-2020 | Minimum required version is 1.32. |
 81 | 6 | 12-2019 | Renamed deprecated MW constant for compatibility with MW version 1.34, minimum required version is 1.28. | [Details][v6.0.0]
 82 | 5 | 09-2017 | Rewrote the entire extension; vastly improved namespace support; some breaking changes | [Details][v5.0.0]
 83 | 4 | 11-2016 | Changed format of the extension for MediaWiki version 1.25; added basic namespace support | [Details][v4.0.0]
 84 | 3 | 02-2015 | Added magic words; improved performance | [Details][3.0.0]
 85 | 2 | 11-2013 | Introduced smart mode | [Details][2.0.0]
 86 | 1 | 05-2012 | First stable release |
 87 | 
 88 | [v8.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v8.0.0
 89 | [v6.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v6.0.0
 90 | [v6.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v6.0.0
 91 | [v5.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v5.0.0
 92 | [v4.0.0]: https://github.com/bovender/LinkTitles/releases/tag/v4.0.0
 93 | [3.0.0]: https://github.com/bovender/LinkTitles/compare/2.4.1...3.0.0
 94 | [2.0.0]: https://github.com/bovender/LinkTitles/compare/1.8.1...2.0.0
 95 | 
 96 | For more details, click the 'Details' links, see the `NEWS.md` file in the
 97 | repository for a user-friendly changelog, or study the commit messages.
 98 | 
 99 | ## Installation
100 | 
101 | To obtain the extension, you can either download a compressed archive from the
102 | [Github releases page](https://github.com/bovender/LinkTitles/releases): Choose
103 | one of the 'Source code' archives and extract it in your Wiki's `extension`
104 | folder. Note that these archives contain a folder that is named after the
105 | release version, e.g. `LinkTitles-8.0.0`. You may want to rename the folder to
106 | `LinkTitles`.
107 | 
108 | Alternatively (and preferred by the author), if you have [Git](https://git-scm.com),
109 | you can pull the repository in the usual way into the `extensions` folder.
110 | 
111 | To activate the extension, add the following to your `LocalSettings.php` file:
112 | 
113 |     wfLoadExtension( 'LinkTitles' );
114 | 
115 | Do not forget to adjust the [configuration](#configuration) to your needs.
116 | 
117 | ## Usage
118 | 
119 | ### Editing a page
120 | 
121 | When linking-on-edit is enabled (with MW versions prior to 1.33), the extension
122 | will by default link whole words only, prefer longer target page titles over
123 | shorter ones, skip headings, and add multiple links if a page title appears
124 | more than once on the page. All of this is configurable; see the
125 | [Configuration](#configuration) section.
126 | 
127 | ### Preventing automatic linking after minor edits
128 | 
129 | If the 'minor edit' check box is marked when you save a page, the extension will
130 | not add links to the page.
131 | 
132 | ### Viewing a page
133 | 
134 | If you do not want the LinkTitles extension to modify the page sources, you can
135 | also have links added whenever a page is being viewed (or, technically, when it
136 | is being rendered). MediaWiki caches rendered pages. Therefore, links do not need
137 | to be added every time a page is being viewed. See the
138 | [`$wgLinkTitlesParseOnRender`](#linking-when-a-page-is-rendered-for-display)
139 | configuration variable.
140 | 
141 | ### Including and excluding pages with Magic Words
142 | 
143 | Add the magic word **`__NOAUTOLINKS__`** to a page to prevent automatic linking
144 | of page titles.
145 | 
146 | The presence of **`__NOAUTOLINKTARGET__`** prevents a page from being
147 | automatically linked to from other pages.
148 | 
149 | ### Enable or disable automatic linking for sections
150 | 
151 | To **exclude** a section on your page from automatic linking, wrap it in
152 | **`...`** tags.
153 | 
154 | To **include** a section on your page for automatic linking, wrap it in
155 | **`...`** tags. Of course this only makes sense if both
156 | `$wgLinkTitlesParseOnEdit` and `$wgLinkTitlesParseOnRender` are set to `false`
157 | *or* if the page contains the `__NOAUTOLINKS__` magic word.
158 | 
159 | ### Namespace support
160 | 
161 | By default, LinkTitles will only process pages in the `NS_MAIN` namespace (i.e.,
162 | 'normal' Wiki pages). You can  modify the configuration to process pages in
163 | other 'source' namespaces as well. By default, LinkTitles will only link to
164 | pages that are in the same namespace as the page being edited or viewed. Again,
165 | additional 'target' namespaces may be added in the
166 | [configuration](#dealing-with-custom-namespaces).
167 | 
168 | If a page contains another page's title that is prefixed with the namespace
169 | (e.g. `my_namspace:other page`), LinkTitles will _not_ add a link. It is assumed
170 | that if someone deliberately types a namespace-qualified page title, they might
171 | just as well add the link markup (`[[...]]`) as well. It is the LinkTitles
172 | extension's intention to facilitate writing non-technical texts and have links
173 | to existing pages added automatically.
174 | 
175 | ### Batch processing
176 | 
177 | The extension provides two methods to batch-process all pages in a Wiki: A
178 | special page (i.e., graphical user interface) and a command-line maintenance
179 | script.
180 | 
181 | #### Special:LinkTitles
182 | 
183 | The special page provides a simple web interface to trigger batch processing. To
184 | avoid blocking the web server for too long, the page will frequently reload
185 | itself (this can be controlled by the `$wgLinkTitlesSpecialPageReloadAfter`
186 | configuration variable that the administrator can set in the `LocalSettings.php`
187 | file).
188 | 
189 | For security reasons, by default only users in the 'sysop' group are allowed to
190 | view the special page (otherwise unauthorized people could trigger a parsing of
191 | your entire wiki). To allow other user groups to view the page as well, add a
192 | line
193 | 
194 |     $wgGroupPermissions ['']['linktitles-batch'] = true;
195 | 
196 | to `LocalSettings.php`.
197 | 
198 | #### Commandline
199 | 
200 | If you have access to a shell on the server that runs your wiki, and are allowed
201 | to execute `/bin/php` on the command line, you can use the extension's
202 | maintenance script. Unlike MediaWiki's built-in maintenance scripts, this
203 | resides not in the `maintenance/` subdirectory but in the extension's own
204 | directory (the one where you downloaded and extracted the files to).
205 | 
206 | To trigger parsing of all pages, issue:
207 | 
208 |     php linktitles-cli.php
209 | 
210 | You can interrupt the process by hitting `CTRL+C` at any time.
211 | 
212 | To continue parsing at a later time, make a note of the index number of the last
213 | page that was processed (e.g., 37), and use the maintenance script with the
214 | `--start` option (or short `-s`) to indicate the start index:
215 | 
216 |     php extensions/LinkTitles/linktitles-cli.php -s 37
217 | 
218 | For more verbose output that also includes the page title, use the `--verbose`
219 | option (or short `-v`):
220 | 
221 |     php extensions/LinkTitles/linktitles-cli.php -s 37 -v
222 | 
223 | In verbose mode, the script will output all page titles one by one.
224 | 
225 | See all available options with:
226 | 
227 |     php extensions/LinkTitles/linktitles-cli.php -h
228 | 
229 | Depending on your shell, you may omit the `php` and call the script directly:
230 | 
231 |     extensions/LinkTitles/linktitles-cli.php
232 | 
233 | ## Configuration
234 | 
235 | To change the configuration, set the variables in your `LocalSettings.php` file.
236 | The code lines below show the default values of the configuration variables.
237 | 
238 | ### Linking when a page is edited and saved
239 | 
240 |     $wgLinkTitlesParseOnEdit = true;
241 | 
242 | Parse page content whenever it is edited and saved, unless 'minor edit' box is
243 | checked. This is the default mode of operation. It has the disadvantage that
244 | newly created pages won't be linked to from existing pages until those existing
245 | pages are edited and saved.
246 | 
247 | ### Linking when a page is rendered for display
248 | 
249 |     $wgLinkTitlesParseOnRender = false;
250 | 
251 | Parse page content when it is rendered for viewing. Unlike the "parse on edit"
252 | mode of operation, this will *not* hard-code the links in the Wiki text. Thus,
253 | if you edit a page that had links added to it during rendering, you will not see
254 | the links in the Wiki markup.
255 | 
256 | Note that MediaWiki caches rendered pages in the database, so that pages rarely
257 | need to be rendered. Rendering is whenever a page is viewed and saved.
258 | Therefore, whether you want to enable both parse-on-edit and parse-on-render
259 | depends on whether you want to have links (`[[...]]`) added to the Wiki markup.
260 | 
261 | Please note that the extension will work on a fully built page when this mode is
262 | enabled; therefore, it *will* add links to text transcluded from templates,
263 | regardless of the configuration setting of `$wgLinkTitlesSkipTemplates`.
264 | 
265 | You can purge the page cache and trigger rendering by adding `?action=purge` to
266 | the URL.
267 | 
268 | ### Enabling case-insensitive linking (smart mode)
269 | 
270 |     $wgLinkTitlesSmartMode = true;
271 | 
272 | With smart mode enabled, the extension will first perform a case-sensitive
273 | search for page titles in the current page; then it will search for occurrences
274 | of the page titles in a case-insensitive way and add aliased ('piped') links.
275 | Thus, if you have a page `MediaWiki Extensions`, but write `Mediawiki
276 | extensions` (with a small 'e') in your text, LinkTitles would generate a link
277 | `[[MediaWiki Extensions|Mediawiki extensions]]`, obviating the need to add
278 | dummy pages for variants of page titles with different cases.
279 | 
280 | Smart mode is enabled by default. You can disable it to increase performance of
281 | the extension.
282 | 
283 | ### Dealing with custom namespaces
284 | 
285 |     $wgLinkTitlesSourceNamespaces = [];
286 | 
287 | Specifies additional namespaces for pages that should be processed by the
288 | LinkTitles extension. If this is an empty array (or anything else that PHP
289 | evaluates to `false`), the default namespace `NS_MAIN` will be assumed.
290 | 
291 | The values in this array must be numbers/namespace constants (`NS_xxx`).
292 | 
293 |     $wgLinkTitlesTargetNamespaces = [];
294 | 
295 | By default, only pages in the same namespace as the page being edited or viewed
296 | will be considered as link targets. If you want to link to pages in other
297 | namespaces, list them here. Note that the source page's own namespace will also
298 | be included, unless you change the `$wgLinkTitlesSamenamespace` option.
299 | 
300 | The values in this array must be numbers/namespace constants (`NS_xxx`).
301 | 
302 |     $wgLinkTitlesSamenamespace = true;
303 | 
304 | If you do not want to have a page's own namespace included in the possible
305 | target namespaces, set this to false. Of course, if `$wgLinkTitlesSameNamespace`
306 | is `false` and `$wgLinkTitlesTargetNamespaces` is empty, LinkTitle will add
307 | no links at all because there are no target namespaces at all.
308 | 
309 | #### Example: Default configuration
310 | 
311 |     $wgLinkTitlesSourceNamespaces = [];
312 |     $wgLinkTitlesTargetNamespaces = [];
313 |     $wgLinkTitlesSamenamespace = true;
314 | 
315 | Process pages in the `NS_MAIN` namespace only, and add links to the `NS_MAIN`
316 | namespace only (i.e., the same namespace that the source page is in).
317 | 
318 | #### Example: Custom namespace only
319 | 
320 |     $wgLinkTitlesSourceNamespaces = [ NS_MY_NAMESPACE];
321 |     $wgLinkTitlesTargetNamespaces = [];
322 |     $wgLinkTitlesSamenamespace = true;
323 | 
324 | Process pages in the `NS_MY_NAMESPACE` namespace only, and add links to the
325 | `NS_MY_NAMESPACE` namespace only (i.e., the same namespace that the source page
326 | is in).
327 | 
328 | #### Example: Link to `NS_MAIN` only
329 | 
330 |     $wgLinkTitlesSourceNamespaces = [ NS_MY_NAMESPACE];
331 |     $wgLinkTitlesTargetNamespaces = [ NS_MAIN ];
332 |     $wgLinkTitlesSamenamespace = false;
333 | 
334 | Process pages in the `NS_MY_NAMESPACE` namespace only, and add links to the
335 | `NS_MAIN` namespace only. Do not link to pages that are in the same namespace
336 | as the source namespace (i.e., `NS_MY_NAMESPACE`).
337 | 
338 | ### Linking or skipping headings
339 | 
340 |     $wgLinkTitlesParseHeadings = false;
341 | 
342 | Determines whether or not to add links to headings. By default, the extension
343 | will leave your (sub)headings untouched. Only applies to parse-on-edit!
344 | 
345 | There is a **known issue** that the extension regards incorrectly formatted
346 | headings as headings. Consider this line:
347 | 
348 |     ## incorrect heading #
349 | 
350 | This line is not recognized as a heading by MediaWiki because the pound signs
351 | (`#`) are not balanced. However, the LinkTitles extension will currently treat
352 | this line as a heading (if it starts and ends with pound signs).
353 | 
354 | ### Prioritizing pages with short titles
355 | 
356 |     $wgLinkTitlesPreferShortTitles = true;
357 | 
358 | If `$wgLinkTitlesPreferShortTitles` is set to `true`, parsing will begin with
359 | shorter page titles.
360 | 
361 | You may want to explicitly set this to `false`, so that the extension will
362 | attempt to link the longest page titles first, as these generally tend to be
363 | more specific.
364 | 
365 | (An earlier version of this document erroneously claimed that the default
366 | setting was `false`, but this was not the way it was defined in the code.)
367 | 
368 | ### Filtering pages by title length
369 | 
370 |     $wgLinkTitlesMinimumTitleLength = 4;
371 | 
372 | Only link to page titles that have a certain minimum length. In my experience,
373 | very short titles can be ambiguous. For example, "mg" may be "milligrams" on a
374 | page, but there may be a page title "Mg" which redirects to the page
375 | "Magnesium". This settings prevents erroneous linking to very short titles by
376 | setting a minimum length. You can adjust this setting to your liking.
377 | 
378 | ### Excluding pages from being linked to
379 | 
380 |     $wgLinkTitlesBlackList = [];
381 | 
382 | Exclude page titles in the array from automatic linking. You can populate this
383 | array with common words that happen to be page titles in your Wiki. For example,
384 | if for whatever reason you had a page "And" in your Wiki, every occurrence of
385 | the word "and" would be linked to this page.
386 | 
387 | To add page titles to the black list, you can use statements such as
388 | 
389 |     $wgLinkTitlesBlackList[] = 'Some special page title';
390 | 
391 | in your `LocalSettings.php` file. Use one of these for every page title that you want to
392 | put on the black list. Alternatively, you can specify the entire array:
393 | 
394 |     $wgLinkTitlesBlackList = [ 'Some special page title', 'Another one' ];
395 | 
396 | Keep in mind that a MediaWiki page title always starts with a capital letter
397 | unless you have `$wgCapitalLinks = false;` in your `LocalSettings.php`.
398 | **Therefore, if you have lowercase first letters in the black list array, they
399 | will have no effect.**
400 | 
401 | ### Dealing with templates
402 | 
403 |     $wgLinkTitlesSkipTemplates = false;
404 | 
405 | If set to true, do not parse the variable text of templates, i.e. in `{{my
406 | template|some variable=some content}}`, leave the entire text between the curly
407 | brackets untouched. If set to false (default setting), the text after the pipe
408 | symbol (`|`) will be parsed.
409 | 
410 | Note: This setting works only with parse-on-edit; it does not affect
411 | parse-on-render! This is because the templates have already been transcluded
412 | (expanded) when the links are added during rendering.
413 | 
414 | ### Multiple links to the same page
415 | 
416 |     $wgLinkTitlesFirstOnly = true;
417 | 
418 | If set to true, only link the first occurrence of a title on a given page. If
419 | a link is piped, i.e. hiding the title of the target page:
420 | 
421 |     [[target page|text that appears as link text]]
422 | 
423 | then the LinkTitles extension does not count that as an occurrence.
424 | 
425 | ### Partial words
426 | 
427 |     $wgLinkTitlesWordStartOnly = true;
428 |     $wgLinkTitlesWordEndOnly = true;
429 | 
430 | Restrict linking to occurrences of the page titles at the start of a word. If
431 | you want to have only the exact page titles linked, you need to set **both**
432 | options `$wgLinkTitlesWordStartOnly` and `$wgLinkTitlesWordEndOnly` to *true*.
433 | On the other hand, if you want to have all occurrences of a page title linked,
434 | even if they are in the middle of a word, you need to set both options to
435 | *false*.
436 | 
437 | Keep in mind that linking in MediaWiki is generally *case-sensitive*.
438 | 
439 | ### Special page configuration
440 | 
441 |     $wgLinkTitlesSpecialPageReloadAfter = 1; // seconds
442 | 
443 | The `LinkTitles:Special` page performs batch processing of pages by repeatedly
444 | calling itself. This happens to prevent timeouts on your server. The default
445 | reload interval is 1 second.
446 | 
447 | ## Development
448 | 
449 | As of December 2020, there is only one major branch where all development takes
450 | place. Formerly, I used to follow Vincent Driessen's advice on [A successful Git
451 | branching model](http://nvie.com/git-model), but this did not work out for me
452 | after all. Pull requests from other developers were usually issued against the
453 | `master` branch, and the constant switching between the `develop` and the `master`
454 | branches was prone to cause a mess.
455 | 
456 | ### Contributors
457 | 
458 | - Daniel Kraus (@bovender), main developer
459 | - Ulrich Strauss (@c0nnex), initial support for namespaces
460 | - Brent Laabs (@labster), code review and bug fixes
461 | - @tetsuya-zama, bug fix
462 | - @yoshida3669, namespace-related bug fixes
463 | - Caleb Mingle (@dentafrice), bug fix
464 | - @paladox, bug fixes and compatilibity fixes
465 | - bluedreamer
466 | - j-zero
467 | 
468 | ### Testing
469 | 
470 | Starting from version 5, LinkTitles finally comes with phpunit tests. The code
471 | is not 100% covered yet. If you find something does not work as expected, let me
472 | know and I will try to add unit tests and fix it.
473 | 
474 | #### Testing with Docker
475 | 
476 | If you have [Docker](https://www.docker.com) available, simply to this:
477 | 
478 |     docker build -t bovender/linktitles .
479 |     # repeat the following as necessary
480 |     docker run -it --rm -v `pwd`:/var/www/html/extensions/LinkTitles bovender/linktitles
481 | 
482 | Or:
483 | 
484 |     make build-test-container
485 |     # repeat the following as necessary
486 |     make test
487 | 
488 | #### The Olde Way
489 | 
490 | Here's how I set up the testing environment. This may not be the canonical way
491 | to do it. Basic information on testing MediaWiki can be found
492 | [here](https://www.mediawiki.org/wiki/Manual:PHP_unit_testing).
493 | 
494 | The following assumes that you have an instance of MediaWiki running locally on
495 | your development machine. This assumes that you are running Linux (I personally
496 | use Ubuntu).
497 | 
498 | 1. Pull the MediaWiki repository:
499 | 
500 |        cd ~/Code
501 |        git clone --depth 1 https://phabricator.wikimedia.org/source/mediawiki.git
502 | 
503 | 2. Install [composer](https://getcomposer.org) locally and fetch the
504 |    dependencies (including development dependencies):
505 | 
506 |    Follow the instructions on the [composer download page](https://getcomposer.org/download),
507 |    but instead of running `php composer-setup.php`, run:
508 | 
509 |        php composer-setup.php --install-dir=bin --filename=composer
510 |        bin/composer install
511 | 
512 | 3. Install phpunit (it was already installed on my Ubuntu system when I began
513 |    testing LinkTitles, so I leave it up to you to figure out how to do it).
514 | 
515 | 4. Copy your `LocalSettings.php` over from your local MediaWiki installation
516 |    and remove (or comment out) any lines that reference extensions or skins that
517 |    you are not going to install to your test environment. For the purposes of
518 |    testing the LinkTitles extension, leave the following line in place:
519 | 
520 |        wfLoadExtensions( 'LinkTitles' );
521 | 
522 |    And ensure the settings file contains the following:
523 | 
524 |        $wgShowDBErrorBacktrace = true;
525 | 
526 | 5. Create a symbolic link to your copy of the LinkTitles repository:
527 | 
528 |        cd ~/Code/mediawiki/extensions
529 |        ln -s ~/Code/LinkTitles
530 | 
531 | 6. Make sure your local MediaWiki instance is up to date. Otherwise phpunit may
532 |    fail and tell you about database problems.
533 | 
534 |    This is because the local database is used as a template for the unit tests.
535 |    For example, I initially had MW 1.26 installed on my laptop, but the cloned
536 |    repository was MW 1.29.1. It's probably also possible to clone the repository
537 |    with a specific version tag which matches your local installation.
538 | 
539 | 7. Run the tests:
540 | 
541 |        cd ~/Code/mediawiki/tests/phpunit
542 |        php phpunit.php --group bovender
543 | 
544 |    This will run all tests from the 'bovender' group, i.e. tests for my extensions.
545 |    If you linked just the LinkTitles extension in step 5, only this extension
546 |    will be tested.
547 | 
548 | ## License
549 | 
550 | Copyright 2012-2024 Daniel Kraus  (GitHub: @bovender)
551 | 
552 | This program is free software; you can redistribute it and/or modify
553 | it under the terms of the GNU General Public License as published by
554 | the Free Software Foundation; either version 2 of the License, or
555 | (at your option) any later version.
556 | 
557 | This program is distributed in the hope that it will be useful,
558 | but WITHOUT ANY WARRANTY; without even the implied warranty of
559 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
560 | GNU General Public License for more details.
561 | 
562 | You should have received a copy of the GNU General Public License
563 | along with this program; if not, write to the Free Software
564 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
565 | MA 02110-1301, USA.
566 | 


--------------------------------------------------------------------------------