summaryrefslogtreecommitdiff
path: root/includes/specialpage
diff options
context:
space:
mode:
Diffstat (limited to 'includes/specialpage')
-rw-r--r--includes/specialpage/ChangesListSpecialPage.php468
-rw-r--r--includes/specialpage/FormSpecialPage.php193
-rw-r--r--includes/specialpage/ImageQueryPage.php79
-rw-r--r--includes/specialpage/IncludableSpecialPage.php39
-rw-r--r--includes/specialpage/PageQueryPage.php72
-rw-r--r--includes/specialpage/QueryPage.php754
-rw-r--r--includes/specialpage/RedirectSpecialPage.php209
-rw-r--r--includes/specialpage/SpecialPage.php663
-rw-r--r--includes/specialpage/SpecialPageFactory.php702
-rw-r--r--includes/specialpage/UnlistedSpecialPage.php37
-rw-r--r--includes/specialpage/WantedQueryPage.php130
11 files changed, 3346 insertions, 0 deletions
diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php
new file mode 100644
index 00000000..c28aa867
--- /dev/null
+++ b/includes/specialpage/ChangesListSpecialPage.php
@@ -0,0 +1,468 @@
+<?php
+/**
+ * Special page which uses a ChangesList to show query results.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page which uses a ChangesList to show query results.
+ * @todo Way too many public functions, most of them should be protected
+ *
+ * @ingroup SpecialPage
+ */
+abstract class ChangesListSpecialPage extends SpecialPage {
+ /** @var string */
+ protected $rcSubpage;
+
+ /** @var FormOptions */
+ protected $rcOptions;
+
+ /** @var array */
+ protected $customFilters;
+
+ /**
+ * Main execution point
+ *
+ * @param string $subpage
+ */
+ public function execute( $subpage ) {
+ $this->rcSubpage = $subpage;
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+
+ $rows = $this->getRows();
+ $opts = $this->getOptions();
+ if ( $rows === false ) {
+ if ( !$this->including() ) {
+ $this->doHeader( $opts, 0 );
+ $this->getOutput()->setStatusCode( 404 );
+ }
+
+ return;
+ }
+
+ $batch = new LinkBatch;
+ foreach ( $rows as $row ) {
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ $batch->add( $row->rc_namespace, $row->rc_title );
+ }
+ $batch->execute();
+
+ $this->webOutput( $rows, $opts );
+
+ $rows->free();
+ }
+
+ /**
+ * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
+ *
+ * @return bool|ResultWrapper Result or false
+ */
+ public function getRows() {
+ $opts = $this->getOptions();
+ $conds = $this->buildMainQueryConds( $opts );
+
+ return $this->doMainQuery( $conds, $opts );
+ }
+
+ /**
+ * Get the current FormOptions for this request
+ *
+ * @return FormOptions
+ */
+ public function getOptions() {
+ if ( $this->rcOptions === null ) {
+ $this->rcOptions = $this->setup( $this->rcSubpage );
+ }
+
+ return $this->rcOptions;
+ }
+
+ /**
+ * Create a FormOptions object with options as specified by the user
+ *
+ * @param array $parameters
+ *
+ * @return FormOptions
+ */
+ public function setup( $parameters ) {
+ $opts = $this->getDefaultOptions();
+ foreach ( $this->getCustomFilters() as $key => $params ) {
+ $opts->add( $key, $params['default'] );
+ }
+
+ $opts = $this->fetchOptionsFromRequest( $opts );
+
+ // Give precedence to subpage syntax
+ if ( $parameters !== null ) {
+ $this->parseParameters( $parameters, $opts );
+ }
+
+ $this->validateOptions( $opts );
+
+ return $opts;
+ }
+
+ /**
+ * Get a FormOptions object containing the default options. By default returns some basic options,
+ * you might want to not call parent method and discard them, or to override default values.
+ *
+ * @return FormOptions
+ */
+ public function getDefaultOptions() {
+ $opts = new FormOptions();
+
+ $opts->add( 'hideminor', false );
+ $opts->add( 'hidebots', false );
+ $opts->add( 'hideanons', false );
+ $opts->add( 'hideliu', false );
+ $opts->add( 'hidepatrolled', false );
+ $opts->add( 'hidemyself', false );
+
+ $opts->add( 'namespace', '', FormOptions::INTNULL );
+ $opts->add( 'invert', false );
+ $opts->add( 'associated', false );
+
+ return $opts;
+ }
+
+ /**
+ * Get custom show/hide filters
+ *
+ * @return array Map of filter URL param names to properties (msg/default)
+ */
+ protected function getCustomFilters() {
+ if ( $this->customFilters === null ) {
+ $this->customFilters = array();
+ wfRunHooks( 'ChangesListSpecialPageFilters', array( $this, &$this->customFilters ) );
+ }
+
+ return $this->customFilters;
+ }
+
+ /**
+ * Fetch values for a FormOptions object from the WebRequest associated with this instance.
+ *
+ * Intended for subclassing, e.g. to add a backwards-compatibility layer.
+ *
+ * @param FormOptions $opts
+ * @return FormOptions
+ */
+ protected function fetchOptionsFromRequest( $opts ) {
+ $opts->fetchValuesFromRequest( $this->getRequest() );
+
+ return $opts;
+ }
+
+ /**
+ * Process $par and put options found in $opts. Used when including the page.
+ *
+ * @param string $par
+ * @param FormOptions $opts
+ */
+ public function parseParameters( $par, FormOptions $opts ) {
+ // nothing by default
+ }
+
+ /**
+ * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
+ *
+ * @param FormOptions $opts
+ */
+ public function validateOptions( FormOptions $opts ) {
+ // nothing by default
+ }
+
+ /**
+ * Return an array of conditions depending of options set in $opts
+ *
+ * @param FormOptions $opts
+ * @return array
+ */
+ public function buildMainQueryConds( FormOptions $opts ) {
+ $dbr = $this->getDB();
+ $user = $this->getUser();
+ $conds = array();
+
+ // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
+ // what the user meant and either show only bots or force anons to be shown.
+ $botsonly = false;
+ $hideanons = $opts['hideanons'];
+ if ( $opts['hideanons'] && $opts['hideliu'] ) {
+ if ( $opts['hidebots'] ) {
+ $hideanons = false;
+ } else {
+ $botsonly = true;
+ }
+ }
+
+ // Toggles
+ if ( $opts['hideminor'] ) {
+ $conds['rc_minor'] = 0;
+ }
+ if ( $opts['hidebots'] ) {
+ $conds['rc_bot'] = 0;
+ }
+ if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
+ $conds['rc_patrolled'] = 0;
+ }
+ if ( $botsonly ) {
+ $conds['rc_bot'] = 1;
+ } else {
+ if ( $opts['hideliu'] ) {
+ $conds[] = 'rc_user = 0';
+ }
+ if ( $hideanons ) {
+ $conds[] = 'rc_user != 0';
+ }
+ }
+ if ( $opts['hidemyself'] ) {
+ if ( $user->getId() ) {
+ $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
+ } else {
+ $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
+ }
+ }
+
+ // Namespace filtering
+ if ( $opts['namespace'] !== '' ) {
+ $selectedNS = $dbr->addQuotes( $opts['namespace'] );
+ $operator = $opts['invert'] ? '!=' : '=';
+ $boolean = $opts['invert'] ? 'AND' : 'OR';
+
+ // Namespace association (bug 2429)
+ if ( !$opts['associated'] ) {
+ $condition = "rc_namespace $operator $selectedNS";
+ } else {
+ // Also add the associated namespace
+ $associatedNS = $dbr->addQuotes(
+ MWNamespace::getAssociated( $opts['namespace'] )
+ );
+ $condition = "(rc_namespace $operator $selectedNS "
+ . $boolean
+ . " rc_namespace $operator $associatedNS)";
+ }
+
+ $conds[] = $condition;
+ }
+
+ return $conds;
+ }
+
+ /**
+ * Process the query
+ *
+ * @param array $conds
+ * @param FormOptions $opts
+ * @return bool|ResultWrapper Result or false
+ */
+ public function doMainQuery( $conds, $opts ) {
+ $tables = array( 'recentchanges' );
+ $fields = RecentChange::selectFields();
+ $query_options = array();
+ $join_conds = array();
+
+ ChangeTags::modifyDisplayQuery(
+ $tables,
+ $fields,
+ $conds,
+ $join_conds,
+ $query_options,
+ ''
+ );
+
+ if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
+ $opts )
+ ) {
+ return false;
+ }
+
+ $dbr = $this->getDB();
+
+ return $dbr->select(
+ $tables,
+ $fields,
+ $conds,
+ __METHOD__,
+ $query_options,
+ $join_conds
+ );
+ }
+
+ protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) {
+ return wfRunHooks(
+ 'ChangesListSpecialPageQuery',
+ array( $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts )
+ );
+ }
+
+ /**
+ * Return a DatabaseBase object for reading
+ *
+ * @return DatabaseBase
+ */
+ protected function getDB() {
+ return wfGetDB( DB_SLAVE );
+ }
+
+ /**
+ * Send output to the OutputPage object, only called if not used feeds
+ *
+ * @param ResultWrapper $rows Database rows
+ * @param FormOptions $opts
+ */
+ public function webOutput( $rows, $opts ) {
+ if ( !$this->including() ) {
+ $this->outputFeedLinks();
+ $this->doHeader( $opts, $rows->numRows() );
+ }
+
+ $this->outputChangesList( $rows, $opts );
+ }
+
+ /**
+ * Output feed links.
+ */
+ public function outputFeedLinks() {
+ // nothing by default
+ }
+
+ /**
+ * Build and output the actual changes list.
+ *
+ * @param array $rows Database rows
+ * @param FormOptions $opts
+ */
+ abstract public function outputChangesList( $rows, $opts );
+
+ /**
+ * Set the text to be displayed above the changes
+ *
+ * @param FormOptions $opts
+ * @param int $numRows Number of rows in the result to show after this header
+ */
+ public function doHeader( $opts, $numRows ) {
+ $this->setTopText( $opts );
+
+ // @todo Lots of stuff should be done here.
+
+ $this->setBottomText( $opts );
+ }
+
+ /**
+ * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
+ * or similar methods to print the text.
+ *
+ * @param FormOptions $opts
+ */
+ function setTopText( FormOptions $opts ) {
+ // nothing by default
+ }
+
+ /**
+ * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
+ * or similar methods to print the text.
+ *
+ * @param FormOptions $opts
+ */
+ function setBottomText( FormOptions $opts ) {
+ // nothing by default
+ }
+
+ /**
+ * Get options to be displayed in a form
+ * @todo This should handle options returned by getDefaultOptions().
+ * @todo Not called by anything, should be called by something… doHeader() maybe?
+ *
+ * @param FormOptions $opts
+ * @return array
+ */
+ function getExtraOptions( $opts ) {
+ return array();
+ }
+
+ /**
+ * Return the legend displayed within the fieldset
+ * @todo This should not be static, then we can drop the parameter
+ * @todo Not called by anything, should be called by doHeader()
+ *
+ * @param IContextSource $context The object available as $this in non-static functions
+ * @return string
+ */
+ public static function makeLegend( IContextSource $context ) {
+ $user = $context->getUser();
+ # The legend showing what the letters and stuff mean
+ $legend = Html::openElement( 'dl' ) . "\n";
+ # Iterates through them and gets the messages for both letter and tooltip
+ $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
+ if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
+ unset( $legendItems['unpatrolled'] );
+ }
+ foreach ( $legendItems as $key => $item ) { # generate items of the legend
+ $label = isset( $item['legend'] ) ? $item['legend'] : $item['title'];
+ $letter = $item['letter'];
+ $cssClass = isset( $item['class'] ) ? $item['class'] : $key;
+
+ $legend .= Html::element( 'dt',
+ array( 'class' => $cssClass ), $context->msg( $letter )->text()
+ ) . "\n" .
+ Html::rawElement( 'dd', array(),
+ $context->msg( $label )->parse()
+ ) . "\n";
+ }
+ # (+-123)
+ $legend .= Html::rawElement( 'dt',
+ array( 'class' => 'mw-plusminus-pos' ),
+ $context->msg( 'recentchanges-legend-plusminus' )->parse()
+ ) . "\n";
+ $legend .= Html::element(
+ 'dd',
+ array( 'class' => 'mw-changeslist-legend-plusminus' ),
+ $context->msg( 'recentchanges-label-plusminus' )->text()
+ ) . "\n";
+ $legend .= Html::closeElement( 'dl' ) . "\n";
+
+ # Collapsibility
+ $legend =
+ '<div class="mw-changeslist-legend">' .
+ $context->msg( 'recentchanges-legend-heading' )->parse() .
+ '<div class="mw-collapsible-content">' . $legend . '</div>' .
+ '</div>';
+
+ return $legend;
+ }
+
+ /**
+ * Add page-specific modules.
+ */
+ protected function addModules() {
+ $out = $this->getOutput();
+ // Styles and behavior for the legend box (see makeLegend())
+ $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
+ $out->addModules( 'mediawiki.special.changeslist.legend.js' );
+ }
+
+ protected function getGroupName() {
+ return 'changes';
+ }
+}
diff --git a/includes/specialpage/FormSpecialPage.php b/includes/specialpage/FormSpecialPage.php
new file mode 100644
index 00000000..bf86ab2e
--- /dev/null
+++ b/includes/specialpage/FormSpecialPage.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * Special page which uses an HTMLForm to handle processing.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page which uses an HTMLForm to handle processing. This is mostly a
+ * clone of FormAction. More special pages should be built this way; maybe this could be
+ * a new structure for SpecialPages.
+ *
+ * @ingroup SpecialPage
+ */
+abstract class FormSpecialPage extends SpecialPage {
+ /**
+ * The sub-page of the special page.
+ * @var string
+ */
+ protected $par = null;
+
+ /**
+ * Get an HTMLForm descriptor array
+ * @return array
+ */
+ abstract protected function getFormFields();
+
+ /**
+ * Add pre-text to the form
+ * @return string HTML which will be sent to $form->addPreText()
+ */
+ protected function preText() {
+ return '';
+ }
+
+ /**
+ * Add post-text to the form
+ * @return string HTML which will be sent to $form->addPostText()
+ */
+ protected function postText() {
+ return '';
+ }
+
+ /**
+ * Play with the HTMLForm if you need to more substantially
+ * @param HTMLForm $form
+ */
+ protected function alterForm( HTMLForm $form ) {
+ }
+
+ /**
+ * Get message prefix for HTMLForm
+ *
+ * @since 1.21
+ * @return string
+ */
+ protected function getMessagePrefix() {
+ return strtolower( $this->getName() );
+ }
+
+ /**
+ * Get the HTMLForm to control behavior
+ * @return HTMLForm|null
+ */
+ protected function getForm() {
+ $this->fields = $this->getFormFields();
+
+ $form = new HTMLForm( $this->fields, $this->getContext(), $this->getMessagePrefix() );
+ $form->setSubmitCallback( array( $this, 'onSubmit' ) );
+ // If the form is a compact vertical form, then don't output this ugly
+ // fieldset surrounding it.
+ // XXX Special pages can setDisplayFormat to 'vform' in alterForm(), but that
+ // is called after this.
+ if ( !$form->isVForm() ) {
+ $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
+ }
+
+ $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' );
+ if ( !$headerMsg->isDisabled() ) {
+ $form->addHeaderText( $headerMsg->parseAsBlock() );
+ }
+
+ // Retain query parameters (uselang etc)
+ $params = array_diff_key(
+ $this->getRequest()->getQueryValues(), array( 'title' => null ) );
+ $form->addHiddenField( 'redirectparams', wfArrayToCgi( $params ) );
+
+ $form->addPreText( $this->preText() );
+ $form->addPostText( $this->postText() );
+ $this->alterForm( $form );
+
+ // Give hooks a chance to alter the form, adding extra fields or text etc
+ wfRunHooks( 'SpecialPageBeforeFormDisplay', array( $this->getName(), &$form ) );
+
+ return $form;
+ }
+
+ /**
+ * Process the form on POST submission.
+ * @param array $data
+ * @param HTMLForm $form
+ * @return bool|string|array|Status As documented for HTMLForm::trySubmit.
+ */
+ abstract public function onSubmit( array $data /* $form = null */ );
+
+ /**
+ * Do something exciting on successful processing of the form, most likely to show a
+ * confirmation message
+ * @since 1.22 Default is to do nothing
+ */
+ public function onSuccess() {
+ }
+
+ /**
+ * Basic SpecialPage workflow: get a form, send it to the user; get some data back,
+ *
+ * @param string $par Subpage string if one was specified
+ */
+ public function execute( $par ) {
+ $this->setParameter( $par );
+ $this->setHeaders();
+
+ // This will throw exceptions if there's a problem
+ $this->checkExecutePermissions( $this->getUser() );
+
+ $form = $this->getForm();
+ if ( $form->show() ) {
+ $this->onSuccess();
+ }
+ }
+
+ /**
+ * Maybe do something interesting with the subpage parameter
+ * @param string $par
+ */
+ protected function setParameter( $par ) {
+ $this->par = $par;
+ }
+
+ /**
+ * Called from execute() to check if the given user can perform this action.
+ * Failures here must throw subclasses of ErrorPageError.
+ * @param User $user
+ * @throws UserBlockedError
+ * @return bool True
+ */
+ protected function checkExecutePermissions( User $user ) {
+ $this->checkPermissions();
+
+ if ( $this->requiresUnblock() && $user->isBlocked() ) {
+ $block = $user->getBlock();
+ throw new UserBlockedError( $block );
+ }
+
+ if ( $this->requiresWrite() ) {
+ $this->checkReadOnly();
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether this action requires the wiki not to be locked
+ * @return bool
+ */
+ public function requiresWrite() {
+ return true;
+ }
+
+ /**
+ * Whether this action cannot be executed by a blocked user
+ * @return bool
+ */
+ public function requiresUnblock() {
+ return true;
+ }
+}
diff --git a/includes/specialpage/ImageQueryPage.php b/includes/specialpage/ImageQueryPage.php
new file mode 100644
index 00000000..272d5337
--- /dev/null
+++ b/includes/specialpage/ImageQueryPage.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Variant of QueryPage which uses a gallery to output results.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Variant of QueryPage which uses a gallery to output results, thus
+ * suited for reports generating images
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+abstract class ImageQueryPage extends QueryPage {
+ /**
+ * Format and output report results using the given information plus
+ * OutputPage
+ *
+ * @param OutputPage $out OutputPage to print to
+ * @param Skin $skin User skin to use [unused]
+ * @param DatabaseBase $dbr (read) connection to use
+ * @param ResultWrapper $res Result pointer
+ * @param int $num Number of available result rows
+ * @param int $offset Paging offset
+ */
+ protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
+ if ( $num > 0 ) {
+ $gallery = ImageGalleryBase::factory( false, $this->getContext() );
+
+ # $res might contain the whole 1,000 rows, so we read up to
+ # $num [should update this to use a Pager]
+ $i = 0;
+ foreach ( $res as $row ) {
+ $i++;
+ $namespace = isset( $row->namespace ) ? $row->namespace : NS_FILE;
+ $title = Title::makeTitleSafe( $namespace, $row->title );
+ if ( $title instanceof Title && $title->getNamespace() == NS_FILE ) {
+ $gallery->add( $title, $this->getCellHtml( $row ) );
+ }
+ if ( $i === $num ) {
+ break;
+ }
+ }
+
+ $out->addHTML( $gallery->toHtml() );
+ }
+ }
+
+ // Gotta override this since it's abstract
+ function formatResult( $skin, $result ) {
+ }
+
+ /**
+ * Get additional HTML to be shown in a results' cell
+ *
+ * @param object $row Result row
+ * @return string
+ */
+ protected function getCellHtml( $row ) {
+ return '';
+ }
+}
diff --git a/includes/specialpage/IncludableSpecialPage.php b/includes/specialpage/IncludableSpecialPage.php
new file mode 100644
index 00000000..2f7f69ce
--- /dev/null
+++ b/includes/specialpage/IncludableSpecialPage.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Shortcut to construct an includable special page.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Shortcut to construct an includable special page.
+ *
+ * @ingroup SpecialPage
+ */
+class IncludableSpecialPage extends SpecialPage {
+ function __construct(
+ $name, $restriction = '', $listed = true, $function = false, $file = 'default'
+ ) {
+ parent::__construct( $name, $restriction, $listed, $function, $file, true );
+ }
+
+ public function isIncludable() {
+ return true;
+ }
+}
diff --git a/includes/specialpage/PageQueryPage.php b/includes/specialpage/PageQueryPage.php
new file mode 100644
index 00000000..afc02271
--- /dev/null
+++ b/includes/specialpage/PageQueryPage.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Variant of QueryPage which formats the result as a simple link to the page.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Variant of QueryPage which formats the result as a simple link to the page
+ *
+ * @ingroup SpecialPage
+ */
+abstract class PageQueryPage extends QueryPage {
+ /**
+ * Run a LinkBatch to pre-cache LinkCache information,
+ * like page existence and information for stub color and redirect hints.
+ * This should be done for live data and cached data.
+ *
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
+ */
+ public function preprocessResults( $db, $res ) {
+ if ( !$res->numRows() ) {
+ return;
+ }
+
+ $batch = new LinkBatch();
+ foreach ( $res as $row ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+
+ $res->seek( 0 );
+ }
+
+ /**
+ * Format the result as a simple link to the page
+ *
+ * @param Skin $skin
+ * @param object $row Result row
+ * @return string
+ */
+ public function formatResult( $skin, $row ) {
+ global $wgContLang;
+
+ $title = Title::makeTitleSafe( $row->namespace, $row->title );
+
+ if ( $title instanceof Title ) {
+ $text = $wgContLang->convert( $title->getPrefixedText() );
+ return Linker::link( $title, htmlspecialchars( $text ) );
+ } else {
+ return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
+ Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) );
+ }
+ }
+}
diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php
new file mode 100644
index 00000000..b229e06e
--- /dev/null
+++ b/includes/specialpage/QueryPage.php
@@ -0,0 +1,754 @@
+<?php
+/**
+ * Base code for "query" special pages.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * This is a class for doing query pages; since they're almost all the same,
+ * we factor out some of the functionality into a superclass, and let
+ * subclasses derive from it.
+ * @ingroup SpecialPage
+ */
+abstract class QueryPage extends SpecialPage {
+ /** @var bool Whether or not we want plain listoutput rather than an ordered list */
+ protected $listoutput = false;
+
+ /** @var int The offset and limit in use, as passed to the query() function */
+ protected $offset = 0;
+
+ /** @var int */
+ protected $limit = 0;
+
+ /**
+ * The number of rows returned by the query. Reading this variable
+ * only makes sense in functions that are run after the query has been
+ * done, such as preprocessResults() and formatRow().
+ */
+ protected $numRows;
+
+ protected $cachedTimestamp = null;
+
+ /**
+ * Whether to show prev/next links
+ */
+ protected $shownavigation = true;
+
+ /**
+ * Get a list of query page classes and their associated special pages,
+ * for periodic updates.
+ *
+ * DO NOT CHANGE THIS LIST without testing that
+ * maintenance/updateSpecialPages.php still works.
+ * @return array
+ */
+ public static function getPages() {
+ global $wgDisableCounters;
+ static $qp = null;
+
+ if ( $qp === null ) {
+ // QueryPage subclass, Special page name
+ $qp = array(
+ array( 'AncientPagesPage', 'Ancientpages' ),
+ array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
+ array( 'DeadendPagesPage', 'Deadendpages' ),
+ array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
+ array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ),
+ array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'),
+ array( 'LinkSearchPage', 'LinkSearch' ),
+ array( 'ListredirectsPage', 'Listredirects' ),
+ array( 'LonelyPagesPage', 'Lonelypages' ),
+ array( 'LongPagesPage', 'Longpages' ),
+ array( 'MediaStatisticsPage', 'MediaStatistics' ),
+ array( 'MIMEsearchPage', 'MIMEsearch' ),
+ array( 'MostcategoriesPage', 'Mostcategories' ),
+ array( 'MostimagesPage', 'Mostimages' ),
+ array( 'MostinterwikisPage', 'Mostinterwikis' ),
+ array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
+ array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ),
+ array( 'MostlinkedPage', 'Mostlinked' ),
+ array( 'MostrevisionsPage', 'Mostrevisions' ),
+ array( 'FewestrevisionsPage', 'Fewestrevisions' ),
+ array( 'ShortPagesPage', 'Shortpages' ),
+ array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
+ array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
+ array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
+ array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
+ array( 'UnusedCategoriesPage', 'Unusedcategories' ),
+ array( 'UnusedimagesPage', 'Unusedimages' ),
+ array( 'WantedCategoriesPage', 'Wantedcategories' ),
+ array( 'WantedFilesPage', 'Wantedfiles' ),
+ array( 'WantedPagesPage', 'Wantedpages' ),
+ array( 'WantedTemplatesPage', 'Wantedtemplates' ),
+ array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
+ array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
+ array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
+ );
+ wfRunHooks( 'wgQueryPages', array( &$qp ) );
+
+ if ( !$wgDisableCounters ) {
+ $qp[] = array( 'PopularPagesPage', 'Popularpages' );
+ }
+ }
+
+ return $qp;
+ }
+
+ /**
+ * A mutator for $this->listoutput;
+ *
+ * @param bool $bool
+ */
+ function setListoutput( $bool ) {
+ $this->listoutput = $bool;
+ }
+
+ /**
+ * Subclasses return an SQL query here, formatted as an array with the
+ * following keys:
+ * tables => Table(s) for passing to Database::select()
+ * fields => Field(s) for passing to Database::select(), may be *
+ * conds => WHERE conditions
+ * options => options
+ * join_conds => JOIN conditions
+ *
+ * Note that the query itself should return the following three columns:
+ * 'namespace', 'title', and 'value'. 'value' is used for sorting.
+ *
+ * These may be stored in the querycache table for expensive queries,
+ * and that cached data will be returned sometimes, so the presence of
+ * extra fields can't be relied upon. The cached 'value' column will be
+ * an integer; non-numeric values are useful only for sorting the
+ * initial query (except if they're timestamps, see usesTimestamps()).
+ *
+ * Don't include an ORDER or LIMIT clause, they will be added.
+ *
+ * If this function is not overridden or returns something other than
+ * an array, getSQL() will be used instead. This is for backwards
+ * compatibility only and is strongly deprecated.
+ * @return array
+ * @since 1.18
+ */
+ function getQueryInfo() {
+ return null;
+ }
+
+ /**
+ * For back-compat, subclasses may return a raw SQL query here, as a string.
+ * This is strongly deprecated; getQueryInfo() should be overridden instead.
+ * @throws MWException
+ * @return string
+ */
+ function getSQL() {
+ /* Implement getQueryInfo() instead */
+ throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
+ . "getQuery() properly" );
+ }
+
+ /**
+ * Subclasses return an array of fields to order by here. Don't append
+ * DESC to the field names, that'll be done automatically if
+ * sortDescending() returns true.
+ * @return array
+ * @since 1.18
+ */
+ function getOrderFields() {
+ return array( 'value' );
+ }
+
+ /**
+ * Does this query return timestamps rather than integers in its
+ * 'value' field? If true, this class will convert 'value' to a
+ * UNIX timestamp for caching.
+ * NOTE: formatRow() may get timestamps in TS_MW (mysql), TS_DB (pgsql)
+ * or TS_UNIX (querycache) format, so be sure to always run them
+ * through wfTimestamp()
+ * @return bool
+ * @since 1.18
+ */
+ function usesTimestamps() {
+ return false;
+ }
+
+ /**
+ * Override to sort by increasing values
+ *
+ * @return bool
+ */
+ function sortDescending() {
+ return true;
+ }
+
+ /**
+ * Is this query expensive (for some definition of expensive)? Then we
+ * don't let it run in miser mode. $wgDisableQueryPages causes all query
+ * pages to be declared expensive. Some query pages are always expensive.
+ *
+ * @return bool
+ */
+ function isExpensive() {
+ return $this->getConfig()->get( 'DisableQueryPages' );
+ }
+
+ /**
+ * Is the output of this query cacheable? Non-cacheable expensive pages
+ * will be disabled in miser mode and will not have their results written
+ * to the querycache table.
+ * @return bool
+ * @since 1.18
+ */
+ public function isCacheable() {
+ return true;
+ }
+
+ /**
+ * Whether or not the output of the page in question is retrieved from
+ * the database cache.
+ *
+ * @return bool
+ */
+ function isCached() {
+ return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
+ }
+
+ /**
+ * Sometime we don't want to build rss / atom feeds.
+ *
+ * @return bool
+ */
+ function isSyndicated() {
+ return true;
+ }
+
+ /**
+ * Formats the results of the query for display. The skin is the current
+ * skin; you can use it for making links. The result is a single row of
+ * result data. You should be able to grab SQL results off of it.
+ * If the function returns false, the line output will be skipped.
+ * @param Skin $skin
+ * @param object $result Result row
+ * @return string|bool String or false to skip
+ */
+ abstract function formatResult( $skin, $result );
+
+ /**
+ * The content returned by this function will be output before any result
+ *
+ * @return string
+ */
+ function getPageHeader() {
+ return '';
+ }
+
+ /**
+ * If using extra form wheely-dealies, return a set of parameters here
+ * as an associative array. They will be encoded and added to the paging
+ * links (prev/next/lengths).
+ *
+ * @return array
+ */
+ function linkParameters() {
+ return array();
+ }
+
+ /**
+ * Some special pages (for example SpecialListusers) might not return the
+ * current object formatted, but return the previous one instead.
+ * Setting this to return true will ensure formatResult() is called
+ * one more time to make sure that the very last result is formatted
+ * as well.
+ * @return bool
+ */
+ function tryLastResult() {
+ return false;
+ }
+
+ /**
+ * Clear the cache and save new results
+ *
+ * @param int|bool $limit Limit for SQL statement
+ * @param bool $ignoreErrors Whether to ignore database errors
+ * @throws DBError|Exception
+ * @return bool|int
+ */
+ function recache( $limit, $ignoreErrors = true ) {
+ if ( !$this->isCacheable() ) {
+ return 0;
+ }
+
+ $fname = get_class( $this ) . '::recache';
+ $dbw = wfGetDB( DB_MASTER );
+ if ( !$dbw ) {
+ return false;
+ }
+
+ try {
+ # Do query
+ $res = $this->reallyDoQuery( $limit, false );
+ $num = false;
+ if ( $res ) {
+ $num = $res->numRows();
+ # Fetch results
+ $vals = array();
+ foreach ( $res as $row ) {
+ if ( isset( $row->value ) ) {
+ if ( $this->usesTimestamps() ) {
+ $value = wfTimestamp( TS_UNIX,
+ $row->value );
+ } else {
+ $value = intval( $row->value ); // @bug 14414
+ }
+ } else {
+ $value = 0;
+ }
+
+ $vals[] = array( 'qc_type' => $this->getName(),
+ 'qc_namespace' => $row->namespace,
+ 'qc_title' => $row->title,
+ 'qc_value' => $value );
+ }
+
+ $dbw->startAtomic( __METHOD__ );
+ # Clear out any old cached data
+ $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
+ # Save results into the querycache table on the master
+ if ( count( $vals ) ) {
+ $dbw->insert( 'querycache', $vals, __METHOD__ );
+ }
+ # Update the querycache_info record for the page
+ $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
+ $dbw->insert( 'querycache_info',
+ array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
+ $fname );
+ $dbw->endAtomic( __METHOD__ );
+ }
+ } catch ( DBError $e ) {
+ if ( !$ignoreErrors ) {
+ throw $e; // report query error
+ }
+ $num = false; // set result to false to indicate error
+ }
+
+ return $num;
+ }
+
+ /**
+ * Get a DB connection to be used for slow recache queries
+ * @return DatabaseBase
+ */
+ function getRecacheDB() {
+ return wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
+ }
+
+ /**
+ * Run the query and return the result
+ * @param int|bool $limit Numerical limit or false for no limit
+ * @param int|bool $offset Numerical offset or false for no offset
+ * @return ResultWrapper
+ * @since 1.18
+ */
+ function reallyDoQuery( $limit, $offset = false ) {
+ $fname = get_class( $this ) . "::reallyDoQuery";
+ $dbr = $this->getRecacheDB();
+ $query = $this->getQueryInfo();
+ $order = $this->getOrderFields();
+
+ if ( $this->sortDescending() ) {
+ foreach ( $order as &$field ) {
+ $field .= ' DESC';
+ }
+ }
+
+ if ( is_array( $query ) ) {
+ $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
+ $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
+ $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
+ $options = isset( $query['options'] ) ? (array)$query['options'] : array();
+ $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
+
+ if ( count( $order ) ) {
+ $options['ORDER BY'] = $order;
+ }
+
+ if ( $limit !== false ) {
+ $options['LIMIT'] = intval( $limit );
+ }
+
+ if ( $offset !== false ) {
+ $options['OFFSET'] = intval( $offset );
+ }
+
+ $res = $dbr->select( $tables, $fields, $conds, $fname,
+ $options, $join_conds
+ );
+ } else {
+ // Old-fashioned raw SQL style, deprecated
+ $sql = $this->getSQL();
+ $sql .= ' ORDER BY ' . implode( ', ', $order );
+ $sql = $dbr->limitResult( $sql, $limit, $offset );
+ $res = $dbr->query( $sql, $fname );
+ }
+
+ return $res;
+ }
+
+ /**
+ * Somewhat deprecated, you probably want to be using execute()
+ * @param int|bool $offset
+ * @param int|bool $limit
+ * @return ResultWrapper
+ */
+ function doQuery( $offset = false, $limit = false ) {
+ if ( $this->isCached() && $this->isCacheable() ) {
+ return $this->fetchFromCache( $limit, $offset );
+ } else {
+ return $this->reallyDoQuery( $limit, $offset );
+ }
+ }
+
+ /**
+ * Fetch the query results from the query cache
+ * @param int|bool $limit Numerical limit or false for no limit
+ * @param int|bool $offset Numerical offset or false for no offset
+ * @return ResultWrapper
+ * @since 1.18
+ */
+ function fetchFromCache( $limit, $offset = false ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $options = array();
+ if ( $limit !== false ) {
+ $options['LIMIT'] = intval( $limit );
+ }
+ if ( $offset !== false ) {
+ $options['OFFSET'] = intval( $offset );
+ }
+ if ( $this->sortDescending() ) {
+ $options['ORDER BY'] = 'qc_value DESC';
+ } else {
+ $options['ORDER BY'] = 'qc_value ASC';
+ }
+ $res = $dbr->select( 'querycache', array( 'qc_type',
+ 'namespace' => 'qc_namespace',
+ 'title' => 'qc_title',
+ 'value' => 'qc_value' ),
+ array( 'qc_type' => $this->getName() ),
+ __METHOD__, $options
+ );
+ return $dbr->resultObject( $res );
+ }
+
+ public function getCachedTimestamp() {
+ if ( is_null( $this->cachedTimestamp ) ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $fname = get_class( $this ) . '::getCachedTimestamp';
+ $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
+ array( 'qci_type' => $this->getName() ), $fname );
+ }
+ return $this->cachedTimestamp;
+ }
+
+ /**
+ * This is the actual workhorse. It does everything needed to make a
+ * real, honest-to-gosh query page.
+ * @param string $par
+ */
+ function execute( $par ) {
+ $user = $this->getUser();
+ if ( !$this->userCanExecute( $user ) ) {
+ $this->displayRestrictionError();
+ return;
+ }
+
+ $this->setHeaders();
+ $this->outputHeader();
+
+ $out = $this->getOutput();
+
+ if ( $this->isCached() && !$this->isCacheable() ) {
+ $out->addWikiMsg( 'querypage-disabled' );
+ return;
+ }
+
+ $out->setSyndicated( $this->isSyndicated() );
+
+ if ( $this->limit == 0 && $this->offset == 0 ) {
+ list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
+ }
+
+ // @todo Use doQuery()
+ if ( !$this->isCached() ) {
+ # select one extra row for navigation
+ $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
+ } else {
+ # Get the cached result, select one extra row for navigation
+ $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
+ if ( !$this->listoutput ) {
+
+ # Fetch the timestamp of this update
+ $ts = $this->getCachedTimestamp();
+ $lang = $this->getLanguage();
+ $maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
+
+ if ( $ts ) {
+ $updated = $lang->userTimeAndDate( $ts, $user );
+ $updateddate = $lang->userDate( $ts, $user );
+ $updatedtime = $lang->userTime( $ts, $user );
+ $out->addMeta( 'Data-Cache-Time', $ts );
+ $out->addJsConfigVars( 'dataCacheTime', $ts );
+ $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
+ } else {
+ $out->addWikiMsg( 'perfcached', $maxResults );
+ }
+
+ # If updates on this page have been disabled, let the user know
+ # that the data set won't be refreshed for now
+ if ( is_array( $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
+ && in_array( $this->getName(), $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
+ ) {
+ $out->wrapWikiMsg(
+ "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
+ 'querypage-no-updates'
+ );
+ }
+ }
+ }
+
+ $this->numRows = $res->numRows();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->preprocessResults( $dbr, $res );
+
+ $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
+
+ # Top header and navigation
+ if ( $this->shownavigation ) {
+ $out->addHTML( $this->getPageHeader() );
+ if ( $this->numRows > 0 ) {
+ $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
+ min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
+ $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
+ # Disable the "next" link when we reach the end
+ $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
+ $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
+ $out->addHTML( '<p>' . $paging . '</p>' );
+ } else {
+ # No results to show, so don't bother with "showing X of Y" etc.
+ # -- just let the user know and give up now
+ $out->addWikiMsg( 'specialpage-empty' );
+ $out->addHTML( Xml::closeElement( 'div' ) );
+ return;
+ }
+ }
+
+ # The actual results; specialist subclasses will want to handle this
+ # with more than a straight list, so we hand them the info, plus
+ # an OutputPage, and let them get on with it
+ $this->outputResults( $out,
+ $this->getSkin(),
+ $dbr, # Should use a ResultWrapper for this
+ $res,
+ min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
+ $this->offset );
+
+ # Repeat the paging links at the bottom
+ if ( $this->shownavigation ) {
+ $out->addHTML( '<p>' . $paging . '</p>' );
+ }
+
+ $out->addHTML( Xml::closeElement( 'div' ) );
+ }
+
+ /**
+ * Format and output report results using the given information plus
+ * OutputPage
+ *
+ * @param OutputPage $out OutputPage to print to
+ * @param Skin $skin User skin to use
+ * @param DatabaseBase $dbr Database (read) connection to use
+ * @param ResultWrapper $res Result pointer
+ * @param int $num Number of available result rows
+ * @param int $offset Paging offset
+ */
+ protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
+ global $wgContLang;
+
+ if ( $num > 0 ) {
+ $html = array();
+ if ( !$this->listoutput ) {
+ $html[] = $this->openList( $offset );
+ }
+
+ # $res might contain the whole 1,000 rows, so we read up to
+ # $num [should update this to use a Pager]
+ // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
+ // @codingStandardsIgnoreEnd
+ $line = $this->formatResult( $skin, $row );
+ if ( $line ) {
+ $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
+ ? ' class="not-patrolled"'
+ : '';
+ $html[] = $this->listoutput
+ ? $line
+ : "<li{$attr}>{$line}</li>\n";
+ }
+ }
+
+ # Flush the final result
+ if ( $this->tryLastResult() ) {
+ $row = null;
+ $line = $this->formatResult( $skin, $row );
+ if ( $line ) {
+ $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
+ ? ' class="not-patrolled"'
+ : '';
+ $html[] = $this->listoutput
+ ? $line
+ : "<li{$attr}>{$line}</li>\n";
+ }
+ }
+
+ if ( !$this->listoutput ) {
+ $html[] = $this->closeList();
+ }
+
+ $html = $this->listoutput
+ ? $wgContLang->listToText( $html )
+ : implode( '', $html );
+
+ $out->addHTML( $html );
+ }
+ }
+
+ /**
+ * @param int $offset
+ * @return string
+ */
+ function openList( $offset ) {
+ return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
+ }
+
+ /**
+ * @return string
+ */
+ function closeList() {
+ return "</ol>\n";
+ }
+
+ /**
+ * Do any necessary preprocessing of the result object.
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
+ */
+ function preprocessResults( $db, $res ) {
+ }
+
+ /**
+ * Similar to above, but packaging in a syndicated feed instead of a web page
+ * @param string $class
+ * @param int $limit
+ * @return bool
+ */
+ function doFeed( $class = '', $limit = 50 ) {
+ if ( !$this->getConfig()->get( 'Feed' ) ) {
+ $this->getOutput()->addWikiMsg( 'feed-unavailable' );
+ return false;
+ }
+
+ $limit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
+
+ $feedClasses = $this->getConfig()->get( 'FeedClasses' );
+ if ( isset( $feedClasses[$class] ) ) {
+ /** @var RSSFeed|AtomFeed $feed */
+ $feed = new $feedClasses[$class](
+ $this->feedTitle(),
+ $this->feedDesc(),
+ $this->feedUrl() );
+ $feed->outHeader();
+
+ $res = $this->reallyDoQuery( $limit, 0 );
+ foreach ( $res as $obj ) {
+ $item = $this->feedResult( $obj );
+ if ( $item ) {
+ $feed->outItem( $item );
+ }
+ }
+
+ $feed->outFooter();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Override for custom handling. If the titles/links are ok, just do
+ * feedItemDesc()
+ * @param object $row
+ * @return FeedItem|null
+ */
+ function feedResult( $row ) {
+ if ( !isset( $row->title ) ) {
+ return null;
+ }
+ $title = Title::makeTitle( intval( $row->namespace ), $row->title );
+ if ( $title ) {
+ $date = isset( $row->timestamp ) ? $row->timestamp : '';
+ $comments = '';
+ if ( $title ) {
+ $talkpage = $title->getTalkPage();
+ $comments = $talkpage->getFullURL();
+ }
+
+ return new FeedItem(
+ $title->getPrefixedText(),
+ $this->feedItemDesc( $row ),
+ $title->getFullURL(),
+ $date,
+ $this->feedItemAuthor( $row ),
+ $comments );
+ } else {
+ return null;
+ }
+ }
+
+ function feedItemDesc( $row ) {
+ return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
+ }
+
+ function feedItemAuthor( $row ) {
+ return isset( $row->user_text ) ? $row->user_text : '';
+ }
+
+ function feedTitle() {
+ $desc = $this->getDescription();
+ $code = $this->getConfig()->get( 'LanguageCode' );
+ $sitename = $this->getConfig()->get( 'Sitename' );
+ return "$sitename - $desc [$code]";
+ }
+
+ function feedDesc() {
+ return $this->msg( 'tagline' )->text();
+ }
+
+ function feedUrl() {
+ return $this->getPageTitle()->getFullURL();
+ }
+}
diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php
new file mode 100644
index 00000000..4226ee02
--- /dev/null
+++ b/includes/specialpage/RedirectSpecialPage.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Shortcuts to construct a special page alias.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Shortcut to construct a special page alias.
+ *
+ * @ingroup SpecialPage
+ */
+abstract class RedirectSpecialPage extends UnlistedSpecialPage {
+ // Query parameters that can be passed through redirects
+ protected $mAllowedRedirectParams = array();
+
+ // Query parameters added by redirects
+ protected $mAddedRedirectParams = array();
+
+ public function execute( $par ) {
+ $redirect = $this->getRedirect( $par );
+ $query = $this->getRedirectQuery();
+ // Redirect to a page title with possible query parameters
+ if ( $redirect instanceof Title ) {
+ $url = $redirect->getFullURL( $query );
+ $this->getOutput()->redirect( $url );
+
+ return $redirect;
+ } elseif ( $redirect === true ) {
+ // Redirect to index.php with query parameters
+ $url = wfAppendQuery( wfScript( 'index' ), $query );
+ $this->getOutput()->redirect( $url );
+
+ return $redirect;
+ } else {
+ $class = get_class( $this );
+ throw new MWException( "RedirectSpecialPage $class doesn't redirect!" );
+ }
+ }
+
+ /**
+ * If the special page is a redirect, then get the Title object it redirects to.
+ * False otherwise.
+ *
+ * @param string $par Subpage string
+ * @return Title|bool
+ */
+ abstract public function getRedirect( $par );
+
+ /**
+ * Return part of the request string for a special redirect page
+ * This allows passing, e.g. action=history to Special:Mypage, etc.
+ *
+ * @return string
+ */
+ public function getRedirectQuery() {
+ $params = array();
+ $request = $this->getRequest();
+
+ foreach ( $this->mAllowedRedirectParams as $arg ) {
+ if ( $request->getVal( $arg, null ) !== null ) {
+ $params[$arg] = $request->getVal( $arg );
+ } elseif ( $request->getArray( $arg, null ) !== null ) {
+ $params[$arg] = $request->getArray( $arg );
+ }
+ }
+
+ foreach ( $this->mAddedRedirectParams as $arg => $val ) {
+ $params[$arg] = $val;
+ }
+
+ return count( $params )
+ ? $params
+ : false;
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+abstract class SpecialRedirectToSpecial extends RedirectSpecialPage {
+ /** @var string Name of redirect target */
+ protected $redirName;
+
+ /** @var string Name of subpage of redirect target */
+ protected $redirSubpage;
+
+ function __construct(
+ $name, $redirName, $redirSubpage = false,
+ $allowedRedirectParams = array(), $addedRedirectParams = array()
+ ) {
+ parent::__construct( $name );
+ $this->redirName = $redirName;
+ $this->redirSubpage = $redirSubpage;
+ $this->mAllowedRedirectParams = $allowedRedirectParams;
+ $this->mAddedRedirectParams = $addedRedirectParams;
+ }
+
+ public function getRedirect( $subpage ) {
+ if ( $this->redirSubpage === false ) {
+ return SpecialPage::getTitleFor( $this->redirName, $subpage );
+ } else {
+ return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage );
+ }
+ }
+}
+
+/**
+ * Superclass for any RedirectSpecialPage which redirects the user
+ * to a particular article (as opposed to user contributions, logs, etc.).
+ *
+ * For security reasons these special pages are restricted to pass on
+ * the following subset of GET parameters to the target page while
+ * removing all others:
+ *
+ * - useskin, uselang, printable: to alter the appearance of the resulting page
+ *
+ * - redirect: allows viewing one's user page or talk page even if it is a
+ * redirect.
+ *
+ * - rdfrom: allows redirecting to one's user page or talk page from an
+ * external wiki with the "Redirect from..." notice.
+ *
+ * - limit, offset: Useful for linking to history of one's own user page or
+ * user talk page. For example, this would be a link to "the last edit to your
+ * user talk page in the year 2010":
+ * http://en.wikipedia.org/wiki/Special:MyPage?offset=20110000000000&limit=1&action=history
+ *
+ * - feed: would allow linking to the current user's RSS feed for their user
+ * talk page:
+ * http://en.wikipedia.org/w/index.php?title=Special:MyTalk&action=history&feed=rss
+ *
+ * - preloadtitle: Can be used to provide a default section title for a
+ * preloaded new comment on one's own talk page.
+ *
+ * - summary : Can be used to provide a default edit summary for a preloaded
+ * edit to one's own user page or talk page.
+ *
+ * - preview: Allows showing/hiding preview on first edit regardless of user
+ * preference, useful for preloaded edits where you know preview wouldn't be
+ * useful.
+ *
+ * - redlink: Affects the message the user sees if their talk page/user talk
+ * page does not currently exist. Avoids confusion for newbies with no user
+ * pages over why they got a "permission error" following this link:
+ * http://en.wikipedia.org/w/index.php?title=Special:MyPage&redlink=1
+ *
+ * - debug: determines whether the debug parameter is passed to load.php,
+ * which disables reformatting and allows scripts to be debugged. Useful
+ * when debugging scripts that manipulate one's own user page or talk page.
+ *
+ * @par Hook extension:
+ * Extensions can add to the redirect parameters list by using the hook
+ * RedirectSpecialArticleRedirectParams
+ *
+ * This hook allows extensions which add GET parameters like FlaggedRevs to
+ * retain those parameters when redirecting using special pages.
+ *
+ * @par Hook extension example:
+ * @code
+ * $wgHooks['RedirectSpecialArticleRedirectParams'][] =
+ * 'MyExtensionHooks::onRedirectSpecialArticleRedirectParams';
+ * public static function onRedirectSpecialArticleRedirectParams( &$redirectParams ) {
+ * $redirectParams[] = 'stable';
+ * return true;
+ * }
+ * @endcode
+ *
+ * @ingroup SpecialPage
+ */
+abstract class RedirectSpecialArticle extends RedirectSpecialPage {
+ function __construct( $name ) {
+ parent::__construct( $name );
+ $redirectParams = array(
+ 'action',
+ 'redirect', 'rdfrom',
+ # Options for preloaded edits
+ 'preload', 'preloadparams', 'editintro', 'preloadtitle', 'summary', 'nosummary',
+ # Options for overriding user settings
+ 'preview', 'minor', 'watchthis',
+ # Options for history/diffs
+ 'section', 'oldid', 'diff', 'dir',
+ 'limit', 'offset', 'feed',
+ # Misc options
+ 'redlink', 'debug',
+ # Options for action=raw; missing ctype can break JS or CSS in some browsers
+ 'ctype', 'maxage', 'smaxage',
+ );
+
+ wfRunHooks( "RedirectSpecialArticleRedirectParams", array( &$redirectParams ) );
+ $this->mAllowedRedirectParams = $redirectParams;
+ }
+}
diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php
new file mode 100644
index 00000000..c0a94af1
--- /dev/null
+++ b/includes/specialpage/SpecialPage.php
@@ -0,0 +1,663 @@
+<?php
+/**
+ * Parent class for all special pages.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Parent class for all special pages.
+ *
+ * Includes some static functions for handling the special page list deprecated
+ * in favor of SpecialPageFactory.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialPage {
+ // The canonical name of this special page
+ // Also used for the default <h1> heading, @see getDescription()
+ protected $mName;
+
+ // The local name of this special page
+ private $mLocalName;
+
+ // Minimum user level required to access this page, or "" for anyone.
+ // Also used to categorise the pages in Special:Specialpages
+ protected $mRestriction;
+
+ // Listed in Special:Specialpages?
+ private $mListed;
+
+ // Whether or not this special page is being included from an article
+ protected $mIncluding;
+
+ // Whether the special page can be included in an article
+ protected $mIncludable;
+
+ /**
+ * Current request context
+ * @var IContextSource
+ */
+ protected $mContext;
+
+ /**
+ * Get a localised Title object for a specified special page name
+ *
+ * @param string $name
+ * @param string|bool $subpage Subpage string, or false to not use a subpage
+ * @param string $fragment The link fragment (after the "#")
+ * @return Title
+ * @throws MWException
+ */
+ public static function getTitleFor( $name, $subpage = false, $fragment = '' ) {
+ $name = SpecialPageFactory::getLocalNameFor( $name, $subpage );
+
+ return Title::makeTitle( NS_SPECIAL, $name, $fragment );
+ }
+
+ /**
+ * Get a localised Title object for a page name with a possibly unvalidated subpage
+ *
+ * @param string $name
+ * @param string|bool $subpage Subpage string, or false to not use a subpage
+ * @return Title|null Title object or null if the page doesn't exist
+ */
+ public static function getSafeTitleFor( $name, $subpage = false ) {
+ $name = SpecialPageFactory::getLocalNameFor( $name, $subpage );
+ if ( $name ) {
+ return Title::makeTitleSafe( NS_SPECIAL, $name );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Default constructor for special pages
+ * Derivative classes should call this from their constructor
+ * Note that if the user does not have the required level, an error message will
+ * be displayed by the default execute() method, without the global function ever
+ * being called.
+ *
+ * If you override execute(), you can recover the default behavior with userCanExecute()
+ * and displayRestrictionError()
+ *
+ * @param string $name Name of the special page, as seen in links and URLs
+ * @param string $restriction User right required, e.g. "block" or "delete"
+ * @param bool $listed Whether the page is listed in Special:Specialpages
+ * @param callable|bool $function Unused
+ * @param string $file Unused
+ * @param bool $includable Whether the page can be included in normal pages
+ */
+ public function __construct(
+ $name = '', $restriction = '', $listed = true,
+ $function = false, $file = '', $includable = false
+ ) {
+ $this->mName = $name;
+ $this->mRestriction = $restriction;
+ $this->mListed = $listed;
+ $this->mIncludable = $includable;
+ }
+
+ /**
+ * Get the name of this Special Page.
+ * @return string
+ */
+ function getName() {
+ return $this->mName;
+ }
+
+ /**
+ * Get the permission that a user must have to execute this page
+ * @return string
+ */
+ function getRestriction() {
+ return $this->mRestriction;
+ }
+
+ // @todo FIXME: Decide which syntax to use for this, and stick to it
+ /**
+ * Whether this special page is listed in Special:SpecialPages
+ * @since 1.3 (r3583)
+ * @return bool
+ */
+ function isListed() {
+ return $this->mListed;
+ }
+
+ /**
+ * Set whether this page is listed in Special:Specialpages, at run-time
+ * @since 1.3
+ * @param bool $listed
+ * @return bool
+ */
+ function setListed( $listed ) {
+ return wfSetVar( $this->mListed, $listed );
+ }
+
+ /**
+ * Get or set whether this special page is listed in Special:SpecialPages
+ * @since 1.6
+ * @param bool $x
+ * @return bool
+ */
+ function listed( $x = null ) {
+ return wfSetVar( $this->mListed, $x );
+ }
+
+ /**
+ * Whether it's allowed to transclude the special page via {{Special:Foo/params}}
+ * @return bool
+ */
+ public function isIncludable() {
+ return $this->mIncludable;
+ }
+
+ /**
+ * Whether the special page is being evaluated via transclusion
+ * @param bool $x
+ * @return bool
+ */
+ function including( $x = null ) {
+ return wfSetVar( $this->mIncluding, $x );
+ }
+
+ /**
+ * Get the localised name of the special page
+ * @return string
+ */
+ function getLocalName() {
+ if ( !isset( $this->mLocalName ) ) {
+ $this->mLocalName = SpecialPageFactory::getLocalNameFor( $this->mName );
+ }
+
+ return $this->mLocalName;
+ }
+
+ /**
+ * Is this page expensive (for some definition of expensive)?
+ * Expensive pages are disabled or cached in miser mode. Originally used
+ * (and still overridden) by QueryPage and subclasses, moved here so that
+ * Special:SpecialPages can safely call it for all special pages.
+ *
+ * @return bool
+ */
+ public function isExpensive() {
+ return false;
+ }
+
+ /**
+ * Is this page cached?
+ * Expensive pages are cached or disabled in miser mode.
+ * Used by QueryPage and subclasses, moved here so that
+ * Special:SpecialPages can safely call it for all special pages.
+ *
+ * @return bool
+ * @since 1.21
+ */
+ public function isCached() {
+ return false;
+ }
+
+ /**
+ * Can be overridden by subclasses with more complicated permissions
+ * schemes.
+ *
+ * @return bool Should the page be displayed with the restricted-access
+ * pages?
+ */
+ public function isRestricted() {
+ // DWIM: If anons can do something, then it is not restricted
+ return $this->mRestriction != '' && !User::groupHasPermission( '*', $this->mRestriction );
+ }
+
+ /**
+ * Checks if the given user (identified by an object) can execute this
+ * special page (as defined by $mRestriction). Can be overridden by sub-
+ * classes with more complicated permissions schemes.
+ *
+ * @param User $user The user to check
+ * @return bool Does the user have permission to view the page?
+ */
+ public function userCanExecute( User $user ) {
+ return $user->isAllowed( $this->mRestriction );
+ }
+
+ /**
+ * Output an error message telling the user what access level they have to have
+ * @throws PermissionsError
+ */
+ function displayRestrictionError() {
+ throw new PermissionsError( $this->mRestriction );
+ }
+
+ /**
+ * Checks if userCanExecute, and if not throws a PermissionsError
+ *
+ * @since 1.19
+ * @return void
+ * @throws PermissionsError
+ */
+ public function checkPermissions() {
+ if ( !$this->userCanExecute( $this->getUser() ) ) {
+ $this->displayRestrictionError();
+ }
+ }
+
+ /**
+ * If the wiki is currently in readonly mode, throws a ReadOnlyError
+ *
+ * @since 1.19
+ * @return void
+ * @throws ReadOnlyError
+ */
+ public function checkReadOnly() {
+ if ( wfReadOnly() ) {
+ throw new ReadOnlyError;
+ }
+ }
+
+ /**
+ * If the user is not logged in, throws UserNotLoggedIn error
+ *
+ * The user will be redirected to Special:Userlogin with the given message as an error on
+ * the form.
+ *
+ * @since 1.23
+ * @param string $reasonMsg [optional] Message key to be displayed on login page
+ * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor
+ * @throws UserNotLoggedIn
+ */
+ public function requireLogin(
+ $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
+ ) {
+ if ( $this->getUser()->isAnon() ) {
+ throw new UserNotLoggedIn( $reasonMsg, $titleMsg );
+ }
+ }
+
+ /**
+ * Return an array of subpages beginning with $search that this special page will accept.
+ *
+ * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo,
+ * etc.):
+ *
+ * - `prefixSearchSubpages( "ba" )` should return `array( "bar", "baz" )`
+ * - `prefixSearchSubpages( "f" )` should return `array( "foo" )`
+ * - `prefixSearchSubpages( "z" )` should return `array()`
+ * - `prefixSearchSubpages( "" )` should return `array( foo", "bar", "baz" )`
+ *
+ * @param string $search Prefix to search for
+ * @param int $limit Maximum number of results to return
+ * @return string[] Matching subpages
+ */
+ public function prefixSearchSubpages( $search, $limit = 10 ) {
+ return array();
+ }
+
+ /**
+ * Helper function for implementations of prefixSearchSubpages() that
+ * filter the values in memory (as oppposed to making a query).
+ *
+ * @since 1.24
+ * @param string $search
+ * @param int $limit
+ * @param array $subpages
+ * @return string[]
+ */
+ protected static function prefixSearchArray( $search, $limit, array $subpages ) {
+ $escaped = preg_quote( $search, '/' );
+ return array_slice( preg_grep( "/^$escaped/i", $subpages ), 0, $limit );
+ }
+
+ /**
+ * Sets headers - this should be called from the execute() method of all derived classes!
+ */
+ function setHeaders() {
+ $out = $this->getOutput();
+ $out->setArticleRelated( false );
+ $out->setRobotPolicy( $this->getRobotPolicy() );
+ $out->setPageTitle( $this->getDescription() );
+ if ( $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
+ $out->addModuleStyles( array(
+ 'mediawiki.ui.input',
+ 'mediawiki.ui.checkbox',
+ ) );
+ }
+ }
+
+ /**
+ * Entry point.
+ *
+ * @since 1.20
+ *
+ * @param string|null $subPage
+ */
+ final public function run( $subPage ) {
+ /**
+ * Gets called before @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param SpecialPage $this
+ * @param string|null $subPage
+ */
+ wfRunHooks( 'SpecialPageBeforeExecute', array( $this, $subPage ) );
+
+ $this->beforeExecute( $subPage );
+ $this->execute( $subPage );
+ $this->afterExecute( $subPage );
+
+ /**
+ * Gets called after @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param SpecialPage $this
+ * @param string|null $subPage
+ */
+ wfRunHooks( 'SpecialPageAfterExecute', array( $this, $subPage ) );
+ }
+
+ /**
+ * Gets called before @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param string|null $subPage
+ */
+ protected function beforeExecute( $subPage ) {
+ // No-op
+ }
+
+ /**
+ * Gets called after @see SpecialPage::execute.
+ *
+ * @since 1.20
+ *
+ * @param string|null $subPage
+ */
+ protected function afterExecute( $subPage ) {
+ // No-op
+ }
+
+ /**
+ * Default execute method
+ * Checks user permissions
+ *
+ * This must be overridden by subclasses; it will be made abstract in a future version
+ *
+ * @param string|null $subPage
+ */
+ public function execute( $subPage ) {
+ $this->setHeaders();
+ $this->checkPermissions();
+ $this->outputHeader();
+ }
+
+ /**
+ * Outputs a summary message on top of special pages
+ * Per default the message key is the canonical name of the special page
+ * May be overridden, i.e. by extensions to stick with the naming conventions
+ * for message keys: 'extensionname-xxx'
+ *
+ * @param string $summaryMessageKey Message key of the summary
+ */
+ function outputHeader( $summaryMessageKey = '' ) {
+ global $wgContLang;
+
+ if ( $summaryMessageKey == '' ) {
+ $msg = $wgContLang->lc( $this->getName() ) . '-summary';
+ } else {
+ $msg = $summaryMessageKey;
+ }
+ if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
+ $this->getOutput()->wrapWikiMsg(
+ "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
+ }
+ }
+
+ /**
+ * Returns the name that goes in the \<h1\> in the special page itself, and
+ * also the name that will be listed in Special:Specialpages
+ *
+ * Derived classes can override this, but usually it is easier to keep the
+ * default behavior.
+ *
+ * @return string
+ */
+ function getDescription() {
+ return $this->msg( strtolower( $this->mName ) )->text();
+ }
+
+ /**
+ * Get a self-referential title object
+ *
+ * @param string|bool $subpage
+ * @return Title
+ * @deprecated since 1.23, use SpecialPage::getPageTitle
+ */
+ function getTitle( $subpage = false ) {
+ return $this->getPageTitle( $subpage );
+ }
+
+ /**
+ * Get a self-referential title object
+ *
+ * @param string|bool $subpage
+ * @return Title
+ * @since 1.23
+ */
+ function getPageTitle( $subpage = false ) {
+ return self::getTitleFor( $this->mName, $subpage );
+ }
+
+ /**
+ * Sets the context this SpecialPage is executed in
+ *
+ * @param IContextSource $context
+ * @since 1.18
+ */
+ public function setContext( $context ) {
+ $this->mContext = $context;
+ }
+
+ /**
+ * Gets the context this SpecialPage is executed in
+ *
+ * @return IContextSource|RequestContext
+ * @since 1.18
+ */
+ public function getContext() {
+ if ( $this->mContext instanceof IContextSource ) {
+ return $this->mContext;
+ } else {
+ wfDebug( __METHOD__ . " called and \$mContext is null. " .
+ "Return RequestContext::getMain(); for sanity\n" );
+
+ return RequestContext::getMain();
+ }
+ }
+
+ /**
+ * Get the WebRequest being used for this instance
+ *
+ * @return WebRequest
+ * @since 1.18
+ */
+ public function getRequest() {
+ return $this->getContext()->getRequest();
+ }
+
+ /**
+ * Get the OutputPage being used for this instance
+ *
+ * @return OutputPage
+ * @since 1.18
+ */
+ public function getOutput() {
+ return $this->getContext()->getOutput();
+ }
+
+ /**
+ * Shortcut to get the User executing this instance
+ *
+ * @return User
+ * @since 1.18
+ */
+ public function getUser() {
+ return $this->getContext()->getUser();
+ }
+
+ /**
+ * Shortcut to get the skin being used for this instance
+ *
+ * @return Skin
+ * @since 1.18
+ */
+ public function getSkin() {
+ return $this->getContext()->getSkin();
+ }
+
+ /**
+ * Shortcut to get user's language
+ *
+ * @return Language
+ * @since 1.19
+ */
+ public function getLanguage() {
+ return $this->getContext()->getLanguage();
+ }
+
+ /**
+ * Shortcut to get main config object
+ * @return Config
+ * @since 1.24
+ */
+ public function getConfig() {
+ return $this->getContext()->getConfig();
+ }
+
+ /**
+ * Return the full title, including $par
+ *
+ * @return Title
+ * @since 1.18
+ */
+ public function getFullTitle() {
+ return $this->getContext()->getTitle();
+ }
+
+ /**
+ * Return the robot policy. Derived classes that override this can change
+ * the robot policy set by setHeaders() from the default 'noindex,nofollow'.
+ *
+ * @return string
+ * @since 1.23
+ */
+ protected function getRobotPolicy() {
+ return 'noindex,nofollow';
+ }
+
+ /**
+ * Wrapper around wfMessage that sets the current context.
+ *
+ * @return Message
+ * @see wfMessage
+ */
+ public function msg( /* $args */ ) {
+ $message = call_user_func_array(
+ array( $this->getContext(), 'msg' ),
+ func_get_args()
+ );
+ // RequestContext passes context to wfMessage, and the language is set from
+ // the context, but setting the language for Message class removes the
+ // interface message status, which breaks for example usernameless gender
+ // invocations. Restore the flag when not including special page in content.
+ if ( $this->including() ) {
+ $message->setInterfaceMessageFlag( false );
+ }
+
+ return $message;
+ }
+
+ /**
+ * Adds RSS/atom links
+ *
+ * @param array $params
+ */
+ protected function addFeedLinks( $params ) {
+ $feedTemplate = wfScript( 'api' );
+
+ foreach ( $this->getConfig()->get( 'FeedClasses' ) as $format => $class ) {
+ $theseParams = $params + array( 'feedformat' => $format );
+ $url = wfAppendQuery( $feedTemplate, $theseParams );
+ $this->getOutput()->addFeedLink( $format, $url );
+ }
+ }
+
+ /**
+ * Get the group that the special page belongs in on Special:SpecialPage
+ * Use this method, instead of getGroupName to allow customization
+ * of the group name from the wiki side
+ *
+ * @return string Group of this special page
+ * @since 1.21
+ */
+ public function getFinalGroupName() {
+ $name = $this->getName();
+ $specialPageGroups = $this->getConfig()->get( 'SpecialPageGroups' );
+
+ // Allow overbidding the group from the wiki side
+ $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
+ if ( !$msg->isBlank() ) {
+ $group = $msg->text();
+ } else {
+ // Than use the group from this object
+ $group = $this->getGroupName();
+
+ // Group '-' is used as default to have the chance to determine,
+ // if the special pages overrides this method,
+ // if not overridden, $wgSpecialPageGroups is checked for b/c
+ if ( $group === '-' && isset( $specialPageGroups[$name] ) ) {
+ $group = $specialPageGroups[$name];
+ }
+ }
+
+ // never give '-' back, change to 'other'
+ if ( $group === '-' ) {
+ $group = 'other';
+ }
+
+ return $group;
+ }
+
+ /**
+ * Under which header this special page is listed in Special:SpecialPages
+ * See messages 'specialpages-group-*' for valid names
+ * This method defaults to group 'other'
+ *
+ * @return string
+ * @since 1.21
+ */
+ protected function getGroupName() {
+ // '-' used here to determine, if this group is overridden or has a hardcoded 'other'
+ // Needed for b/c in getFinalGroupName
+ return '-';
+ }
+}
diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php
new file mode 100644
index 00000000..679492ae
--- /dev/null
+++ b/includes/specialpage/SpecialPageFactory.php
@@ -0,0 +1,702 @@
+<?php
+/**
+ * Factory for handling the special page list and generating SpecialPage objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @defgroup SpecialPage SpecialPage
+ */
+
+/**
+ * Factory for handling the special page list and generating SpecialPage objects.
+ *
+ * To add a special page in an extension, add to $wgSpecialPages either
+ * an object instance or an array containing the name and constructor
+ * parameters. The latter is preferred for performance reasons.
+ *
+ * The object instantiated must be either an instance of SpecialPage or a
+ * sub-class thereof. It must have an execute() method, which sends the HTML
+ * for the special page to $wgOut. The parent class has an execute() method
+ * which distributes the call to the historical global functions. Additionally,
+ * execute() also checks if the user has the necessary access privileges
+ * and bails out if not.
+ *
+ * To add a core special page, use the similar static list in
+ * SpecialPageFactory::$list. To remove a core static special page at runtime, use
+ * a SpecialPage_initList hook.
+ *
+ * @ingroup SpecialPage
+ * @since 1.17
+ */
+class SpecialPageFactory {
+ /**
+ * List of special page names to the subclass of SpecialPage which handles them.
+ */
+ private static $list = array(
+ // Maintenance Reports
+ 'BrokenRedirects' => 'BrokenRedirectsPage',
+ 'Deadendpages' => 'DeadendPagesPage',
+ 'DoubleRedirects' => 'DoubleRedirectsPage',
+ 'Longpages' => 'LongPagesPage',
+ 'Ancientpages' => 'AncientPagesPage',
+ 'Lonelypages' => 'LonelyPagesPage',
+ 'Fewestrevisions' => 'FewestrevisionsPage',
+ 'Withoutinterwiki' => 'WithoutInterwikiPage',
+ 'Protectedpages' => 'SpecialProtectedpages',
+ 'Protectedtitles' => 'SpecialProtectedtitles',
+ 'Shortpages' => 'ShortpagesPage',
+ 'Uncategorizedcategories' => 'UncategorizedCategoriesPage',
+ 'Uncategorizedimages' => 'UncategorizedImagesPage',
+ 'Uncategorizedpages' => 'UncategorizedPagesPage',
+ 'Uncategorizedtemplates' => 'UncategorizedTemplatesPage',
+ 'Unusedcategories' => 'UnusedCategoriesPage',
+ 'Unusedimages' => 'UnusedimagesPage',
+ 'Unusedtemplates' => 'UnusedtemplatesPage',
+ 'Unwatchedpages' => 'UnwatchedpagesPage',
+ 'Wantedcategories' => 'WantedCategoriesPage',
+ 'Wantedfiles' => 'WantedFilesPage',
+ 'Wantedpages' => 'WantedPagesPage',
+ 'Wantedtemplates' => 'WantedTemplatesPage',
+
+ // List of pages
+ 'Allpages' => 'SpecialAllpages',
+ 'Prefixindex' => 'SpecialPrefixindex',
+ 'Categories' => 'SpecialCategories',
+ 'Listredirects' => 'ListredirectsPage',
+ 'PagesWithProp' => 'SpecialPagesWithProp',
+ 'TrackingCategories' => 'SpecialTrackingCategories',
+
+ // Login/create account
+ 'Userlogin' => 'LoginForm',
+ 'CreateAccount' => 'SpecialCreateAccount',
+
+ // Users and rights
+ 'Block' => 'SpecialBlock',
+ 'Unblock' => 'SpecialUnblock',
+ 'BlockList' => 'SpecialBlockList',
+ 'ChangePassword' => 'SpecialChangePassword',
+ 'PasswordReset' => 'SpecialPasswordReset',
+ 'DeletedContributions' => 'DeletedContributionsPage',
+ 'Preferences' => 'SpecialPreferences',
+ 'ResetTokens' => 'SpecialResetTokens',
+ 'Contributions' => 'SpecialContributions',
+ 'Listgrouprights' => 'SpecialListGroupRights',
+ 'Listusers' => 'SpecialListUsers',
+ 'Listadmins' => 'SpecialListAdmins',
+ 'Listbots' => 'SpecialListBots',
+ 'Userrights' => 'UserrightsPage',
+ 'EditWatchlist' => 'SpecialEditWatchlist',
+
+ // Recent changes and logs
+ 'Newimages' => 'SpecialNewFiles',
+ 'Log' => 'SpecialLog',
+ 'Watchlist' => 'SpecialWatchlist',
+ 'Newpages' => 'SpecialNewpages',
+ 'Recentchanges' => 'SpecialRecentChanges',
+ 'Recentchangeslinked' => 'SpecialRecentChangesLinked',
+ 'Tags' => 'SpecialTags',
+
+ // Media reports and uploads
+ 'Listfiles' => 'SpecialListFiles',
+ 'Filepath' => 'SpecialFilepath',
+ 'MediaStatistics' => 'MediaStatisticsPage',
+ 'MIMEsearch' => 'MIMEsearchPage',
+ 'FileDuplicateSearch' => 'FileDuplicateSearchPage',
+ 'Upload' => 'SpecialUpload',
+ 'UploadStash' => 'SpecialUploadStash',
+ 'ListDuplicatedFiles' => 'ListDuplicatedFilesPage',
+
+ // Data and tools
+ 'Statistics' => 'SpecialStatistics',
+ 'Allmessages' => 'SpecialAllmessages',
+ 'Version' => 'SpecialVersion',
+ 'Lockdb' => 'SpecialLockdb',
+ 'Unlockdb' => 'SpecialUnlockdb',
+
+ // Redirecting special pages
+ 'LinkSearch' => 'LinkSearchPage',
+ 'Randompage' => 'RandomPage',
+ 'RandomInCategory' => 'SpecialRandomInCategory',
+ 'Randomredirect' => 'SpecialRandomredirect',
+
+ // High use pages
+ 'Mostlinkedcategories' => 'MostlinkedCategoriesPage',
+ 'Mostimages' => 'MostimagesPage',
+ 'Mostinterwikis' => 'MostinterwikisPage',
+ 'Mostlinked' => 'MostlinkedPage',
+ 'Mostlinkedtemplates' => 'MostlinkedTemplatesPage',
+ 'Mostcategories' => 'MostcategoriesPage',
+ 'Mostrevisions' => 'MostrevisionsPage',
+
+ // Page tools
+ 'ComparePages' => 'SpecialComparePages',
+ 'Export' => 'SpecialExport',
+ 'Import' => 'SpecialImport',
+ 'Undelete' => 'SpecialUndelete',
+ 'Whatlinkshere' => 'SpecialWhatLinksHere',
+ 'MergeHistory' => 'SpecialMergeHistory',
+ 'ExpandTemplates' => 'SpecialExpandTemplates',
+
+ // Other
+ 'Booksources' => 'SpecialBookSources',
+
+ // Unlisted / redirects
+ 'Blankpage' => 'SpecialBlankpage',
+ 'Diff' => 'SpecialDiff',
+ 'Emailuser' => 'SpecialEmailUser',
+ 'Movepage' => 'MovePageForm',
+ 'Mycontributions' => 'SpecialMycontributions',
+ 'MyLanguage' => 'SpecialMyLanguage',
+ 'Mypage' => 'SpecialMypage',
+ 'Mytalk' => 'SpecialMytalk',
+ 'Myuploads' => 'SpecialMyuploads',
+ 'AllMyUploads' => 'SpecialAllMyUploads',
+ 'PermanentLink' => 'SpecialPermanentLink',
+ 'Redirect' => 'SpecialRedirect',
+ 'Revisiondelete' => 'SpecialRevisionDelete',
+ 'RunJobs' => 'SpecialRunJobs',
+ 'Specialpages' => 'SpecialSpecialpages',
+ 'Userlogout' => 'SpecialUserlogout',
+ );
+
+ private static $aliases;
+
+ /**
+ * Reset the internal list of special pages. Useful when changing $wgSpecialPages after
+ * the internal list has already been initialized, e.g. during testing.
+ */
+ public static function resetList() {
+ self::$list = null;
+ self::$aliases = null;
+ }
+
+ /**
+ * Returns a list of canonical special page names.
+ * May be used to iterate over all registered special pages.
+ *
+ * @return string[]
+ */
+ public static function getNames() {
+ return array_keys( self::getPageList() );
+ }
+
+ /**
+ * Get the special page list as an array
+ *
+ * @deprecated since 1.24, use getNames() instead.
+ * @return array
+ */
+ public static function getList() {
+ wfDeprecated( __FUNCTION__, '1.24' );
+ return self::getPageList();
+ }
+
+ /**
+ * Get the special page list as an array
+ *
+ * @return array
+ */
+ private static function getPageList() {
+ global $wgSpecialPages;
+ global $wgDisableCounters, $wgDisableInternalSearch, $wgEmailAuthentication;
+ global $wgEnableEmail, $wgEnableJavaScriptTest;
+ global $wgPageLanguageUseDB;
+
+ if ( !is_object( self::$list ) ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( !$wgDisableCounters ) {
+ self::$list['Popularpages'] = 'PopularPagesPage';
+ }
+
+ if ( !$wgDisableInternalSearch ) {
+ self::$list['Search'] = 'SpecialSearch';
+ }
+
+ if ( $wgEmailAuthentication ) {
+ self::$list['Confirmemail'] = 'EmailConfirmation';
+ self::$list['Invalidateemail'] = 'EmailInvalidation';
+ }
+
+ if ( $wgEnableEmail ) {
+ self::$list['ChangeEmail'] = 'SpecialChangeEmail';
+ }
+
+ if ( $wgEnableJavaScriptTest ) {
+ self::$list['JavaScriptTest'] = 'SpecialJavaScriptTest';
+ }
+
+ if ( $wgPageLanguageUseDB ) {
+ self::$list['PageLanguage'] = 'SpecialPageLanguage';
+ }
+
+ self::$list['Activeusers'] = 'SpecialActiveUsers';
+
+ // Add extension special pages
+ self::$list = array_merge( self::$list, $wgSpecialPages );
+
+ // Run hooks
+ // This hook can be used to remove undesired built-in special pages
+ wfRunHooks( 'SpecialPage_initList', array( &self::$list ) );
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ return self::$list;
+ }
+
+ /**
+ * Initialise and return the list of special page aliases. Returns an object with
+ * properties which can be accessed $obj->pagename - each property name is an
+ * alias, with the value being the canonical name of the special page. All
+ * registered special pages are guaranteed to map to themselves.
+ * @return object
+ */
+ private static function getAliasListObject() {
+ if ( !is_object( self::$aliases ) ) {
+ global $wgContLang;
+ $aliases = $wgContLang->getSpecialPageAliases();
+
+ self::$aliases = array();
+ $keepAlias = array();
+
+ // Force every canonical name to be an alias for itself.
+ foreach ( self::getPageList() as $name => $stuff ) {
+ $caseFoldedAlias = $wgContLang->caseFold( $name );
+ self::$aliases[$caseFoldedAlias] = $name;
+ $keepAlias[$caseFoldedAlias] = 'canonical';
+ }
+
+ // Check for $aliases being an array since Language::getSpecialPageAliases can return null
+ if ( is_array( $aliases ) ) {
+ foreach ( $aliases as $realName => $aliasList ) {
+ $aliasList = array_values( $aliasList );
+ foreach ( $aliasList as $i => $alias ) {
+ $caseFoldedAlias = $wgContLang->caseFold( $alias );
+
+ if ( isset( self::$aliases[$caseFoldedAlias] ) &&
+ $realName === self::$aliases[$caseFoldedAlias]
+ ) {
+ // Ignore same-realName conflicts
+ continue;
+ }
+
+ if ( !isset( $keepAlias[$caseFoldedAlias] ) ) {
+ self::$aliases[$caseFoldedAlias] = $realName;
+ if ( !$i ) {
+ $keepAlias[$caseFoldedAlias] = 'first';
+ }
+ } elseif ( !$i ) {
+ wfWarn( "First alias '$alias' for $realName conflicts with " .
+ "{$keepAlias[$caseFoldedAlias]} alias for " .
+ self::$aliases[$caseFoldedAlias]
+ );
+ }
+ }
+ }
+ }
+
+ // Cast to object: func()[$key] doesn't work, but func()->$key does
+ self::$aliases = (object)self::$aliases;
+ }
+
+ return self::$aliases;
+ }
+
+ /**
+ * Given a special page name with a possible subpage, return an array
+ * where the first element is the special page name and the second is the
+ * subpage.
+ *
+ * @param string $alias
+ * @return array Array( String, String|null ), or array( null, null ) if the page is invalid
+ */
+ public static function resolveAlias( $alias ) {
+ global $wgContLang;
+ $bits = explode( '/', $alias, 2 );
+
+ $caseFoldedAlias = $wgContLang->caseFold( $bits[0] );
+ $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias );
+ if ( isset( self::getAliasListObject()->$caseFoldedAlias ) ) {
+ $name = self::getAliasListObject()->$caseFoldedAlias;
+ } else {
+ return array( null, null );
+ }
+
+ if ( !isset( $bits[1] ) ) { // bug 2087
+ $par = null;
+ } else {
+ $par = $bits[1];
+ }
+
+ return array( $name, $par );
+ }
+
+ /**
+ * Add a page to a certain display group for Special:SpecialPages
+ *
+ * @param SpecialPage|string $page
+ * @param string $group
+ * @deprecated since 1.21 Override SpecialPage::getGroupName
+ */
+ public static function setGroup( $page, $group ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
+ global $wgSpecialPageGroups;
+ $name = is_object( $page ) ? $page->getName() : $page;
+ $wgSpecialPageGroups[$name] = $group;
+ }
+
+ /**
+ * Get the group that the special page belongs in on Special:SpecialPage
+ *
+ * @param SpecialPage $page
+ * @return string
+ * @deprecated since 1.21 Use SpecialPage::getFinalGroupName
+ */
+ public static function getGroup( &$page ) {
+ wfDeprecated( __METHOD__, '1.21' );
+
+ return $page->getFinalGroupName();
+ }
+
+ /**
+ * Check if a given name exist as a special page or as a special page alias
+ *
+ * @param string $name Name of a special page
+ * @return bool True if a special page exists with this name
+ */
+ public static function exists( $name ) {
+ list( $title, /*...*/ ) = self::resolveAlias( $name );
+
+ $specialPageList = self::getPageList();
+ return isset( $specialPageList[$title] );
+ }
+
+ /**
+ * Find the object with a given name and return it (or NULL)
+ *
+ * @param string $name Special page name, may be localised and/or an alias
+ * @return SpecialPage|null SpecialPage object or null if the page doesn't exist
+ */
+ public static function getPage( $name ) {
+ list( $realName, /*...*/ ) = self::resolveAlias( $name );
+
+ $specialPageList = self::getPageList();
+
+ if ( isset( $specialPageList[$realName] ) ) {
+ $rec = $specialPageList[$realName];
+
+ if ( is_callable( $rec ) ) {
+ // Use callback to instantiate the special page
+ $page = call_user_func( $rec );
+ } elseif ( is_string( $rec ) ) {
+ $className = $rec;
+ $page = new $className;
+ } elseif ( is_array( $rec ) ) {
+ $className = array_shift( $rec );
+ // @deprecated, officially since 1.18, unofficially since forever
+ wfDeprecated( "Array syntax for \$wgSpecialPages is deprecated ($className), " .
+ "define a subclass of SpecialPage instead.", '1.18' );
+ $page = MWFunction::newObj( $className, $rec );
+ } elseif ( $rec instanceof SpecialPage ) {
+ $page = $rec; //XXX: we should deep clone here
+ } else {
+ $page = null;
+ }
+
+ if ( $page instanceof SpecialPage ) {
+ return $page;
+ } else {
+ // It's not a classname, nor a callback, nor a legacy constructor array,
+ // nor a special page object. Give up.
+ wfLogWarning( "Cannot instantiate special page $realName: bad spec!" );
+ return null;
+ }
+
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return categorised listable special pages which are available
+ * for the current user, and everyone.
+ *
+ * @param User $user User object to check permissions, $wgUser will be used
+ * if not provided
+ * @return array ( string => Specialpage )
+ */
+ public static function getUsablePages( User $user = null ) {
+ $pages = array();
+ if ( $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+ foreach ( self::getPageList() as $name => $rec ) {
+ $page = self::getPage( $name );
+ if ( $page ) { // not null
+ $page->setContext( RequestContext::getMain() );
+ if ( $page->isListed()
+ && ( !$page->isRestricted() || $page->userCanExecute( $user ) )
+ ) {
+ $pages[$name] = $page;
+ }
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Return categorised listable special pages for all users
+ *
+ * @return array ( string => Specialpage )
+ */
+ public static function getRegularPages() {
+ $pages = array();
+ foreach ( self::getPageList() as $name => $rec ) {
+ $page = self::getPage( $name );
+ if ( $page->isListed() && !$page->isRestricted() ) {
+ $pages[$name] = $page;
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Return categorised listable special pages which are available
+ * for the current user, but not for everyone
+ *
+ * @param User|null $user User object to use or null for $wgUser
+ * @return array ( string => Specialpage )
+ */
+ public static function getRestrictedPages( User $user = null ) {
+ $pages = array();
+ if ( $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+ foreach ( self::getPageList() as $name => $rec ) {
+ $page = self::getPage( $name );
+ if (
+ $page->isListed()
+ && $page->isRestricted()
+ && $page->userCanExecute( $user )
+ ) {
+ $pages[$name] = $page;
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Execute a special page path.
+ * The path may contain parameters, e.g. Special:Name/Params
+ * Extracts the special page name and call the execute method, passing the parameters
+ *
+ * Returns a title object if the page is redirected, false if there was no such special
+ * page, and true if it was successful.
+ *
+ * @param Title $title
+ * @param IContextSource $context
+ * @param bool $including Bool output is being captured for use in {{special:whatever}}
+ *
+ * @return bool
+ */
+ public static function executePath( Title &$title, IContextSource &$context, $including = false ) {
+ wfProfileIn( __METHOD__ );
+
+ // @todo FIXME: Redirects broken due to this call
+ $bits = explode( '/', $title->getDBkey(), 2 );
+ $name = $bits[0];
+ if ( !isset( $bits[1] ) ) { // bug 2087
+ $par = null;
+ } else {
+ $par = $bits[1];
+ }
+ $page = self::getPage( $name );
+ // Nonexistent?
+ if ( !$page ) {
+ $context->getOutput()->setArticleRelated( false );
+ $context->getOutput()->setRobotPolicy( 'noindex,nofollow' );
+
+ global $wgSend404Code;
+ if ( $wgSend404Code ) {
+ $context->getOutput()->setStatusCode( 404 );
+ }
+
+ $context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
+ wfProfileOut( __METHOD__ );
+
+ return false;
+ }
+
+ // Page exists, set the context
+ $page->setContext( $context );
+
+ if ( !$including ) {
+ // Redirect to canonical alias for GET commands
+ // Not for POST, we'd lose the post data, so it's best to just distribute
+ // the request. Such POST requests are possible for old extensions that
+ // generate self-links without being aware that their default name has
+ // changed.
+ if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) {
+ $query = $context->getRequest()->getQueryValues();
+ unset( $query['title'] );
+ $title = $page->getPageTitle( $par );
+ $url = $title->getFullURL( $query );
+ $context->getOutput()->redirect( $url );
+ wfProfileOut( __METHOD__ );
+
+ return $title;
+ } else {
+ $context->setTitle( $page->getPageTitle( $par ) );
+ }
+ } elseif ( !$page->isIncludable() ) {
+ wfProfileOut( __METHOD__ );
+
+ return false;
+ }
+
+ $page->including( $including );
+
+ // Execute special page
+ $profName = 'Special:' . $page->getName();
+ wfProfileIn( $profName );
+ $page->run( $par );
+ wfProfileOut( $profName );
+ wfProfileOut( __METHOD__ );
+
+ return true;
+ }
+
+ /**
+ * Just like executePath() but will override global variables and execute
+ * the page in "inclusion" mode. Returns true if the execution was
+ * successful or false if there was no such special page, or a title object
+ * if it was a redirect.
+ *
+ * Also saves the current $wgTitle, $wgOut, $wgRequest, $wgUser and $wgLang
+ * variables so that the special page will get the context it'd expect on a
+ * normal request, and then restores them to their previous values after.
+ *
+ * @param Title $title
+ * @param IContextSource $context
+ * @return string HTML fragment
+ */
+ public static function capturePath( Title $title, IContextSource $context ) {
+ global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgLang;
+
+ // Save current globals
+ $oldTitle = $wgTitle;
+ $oldOut = $wgOut;
+ $oldRequest = $wgRequest;
+ $oldUser = $wgUser;
+ $oldLang = $wgLang;
+
+ // Set the globals to the current context
+ $wgTitle = $title;
+ $wgOut = $context->getOutput();
+ $wgRequest = $context->getRequest();
+ $wgUser = $context->getUser();
+ $wgLang = $context->getLanguage();
+
+ // The useful part
+ $ret = self::executePath( $title, $context, true );
+
+ // And restore the old globals
+ $wgTitle = $oldTitle;
+ $wgOut = $oldOut;
+ $wgRequest = $oldRequest;
+ $wgUser = $oldUser;
+ $wgLang = $oldLang;
+
+ return $ret;
+ }
+
+ /**
+ * Get the local name for a specified canonical name
+ *
+ * @param string $name
+ * @param string|bool $subpage
+ * @return string
+ */
+ public static function getLocalNameFor( $name, $subpage = false ) {
+ global $wgContLang;
+ $aliases = $wgContLang->getSpecialPageAliases();
+ $aliasList = self::getAliasListObject();
+
+ // Find the first alias that maps back to $name
+ if ( isset( $aliases[$name] ) ) {
+ $found = false;
+ foreach ( $aliases[$name] as $alias ) {
+ $caseFoldedAlias = $wgContLang->caseFold( $alias );
+ $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias );
+ if ( isset( $aliasList->$caseFoldedAlias ) &&
+ $aliasList->$caseFoldedAlias === $name
+ ) {
+ $name = $alias;
+ $found = true;
+ break;
+ }
+ }
+ if ( !$found ) {
+ wfWarn( "Did not find a usable alias for special page '$name'. " .
+ "It seems all defined aliases conflict?" );
+ }
+ } else {
+ // Check if someone misspelled the correct casing
+ if ( is_array( $aliases ) ) {
+ foreach ( $aliases as $n => $values ) {
+ if ( strcasecmp( $name, $n ) === 0 ) {
+ wfWarn( "Found alias defined for $n when searching for " .
+ "special page aliases for $name. Case mismatch?" );
+ return self::getLocalNameFor( $n, $subpage );
+ }
+ }
+ }
+
+ wfWarn( "Did not find alias for special page '$name'. " .
+ "Perhaps no aliases are defined for it?" );
+ }
+
+ if ( $subpage !== false && !is_null( $subpage ) ) {
+ $name = "$name/$subpage";
+ }
+
+ return $wgContLang->ucfirst( $name );
+ }
+
+ /**
+ * Get a title for a given alias
+ *
+ * @param string $alias
+ * @return Title|null Title or null if there is no such alias
+ */
+ public static function getTitleForAlias( $alias ) {
+ list( $name, $subpage ) = self::resolveAlias( $alias );
+ if ( $name != null ) {
+ return SpecialPage::getTitleFor( $name, $subpage );
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/includes/specialpage/UnlistedSpecialPage.php b/includes/specialpage/UnlistedSpecialPage.php
new file mode 100644
index 00000000..f5e2ccf7
--- /dev/null
+++ b/includes/specialpage/UnlistedSpecialPage.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Shortcut to construct a special page which is unlisted by default.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Shortcut to construct a special page which is unlisted by default.
+ *
+ * @ingroup SpecialPage
+ */
+class UnlistedSpecialPage extends SpecialPage {
+ function __construct( $name, $restriction = '', $function = false, $file = 'default' ) {
+ parent::__construct( $name, $restriction, false, $function, $file );
+ }
+
+ public function isListed() {
+ return false;
+ }
+}
diff --git a/includes/specialpage/WantedQueryPage.php b/includes/specialpage/WantedQueryPage.php
new file mode 100644
index 00000000..be2f1e8d
--- /dev/null
+++ b/includes/specialpage/WantedQueryPage.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Class definition for a wanted query page.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Class definition for a wanted query page like
+ * WantedPages, WantedTemplates, etc
+ * @ingroup SpecialPage
+ */
+abstract class WantedQueryPage extends QueryPage {
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * Cache page existence for performance
+ * @param DatabaseBase $db
+ * @param ResultWrapper $res
+ */
+ function preprocessResults( $db, $res ) {
+ if ( !$res->numRows() ) {
+ return;
+ }
+
+ $batch = new LinkBatch;
+ foreach ( $res as $row ) {
+ $batch->add( $row->namespace, $row->title );
+ }
+ $batch->execute();
+
+ // Back to start for display
+ $res->seek( 0 );
+ }
+
+ /**
+ * Should formatResult() always check page existence, even if
+ * the results are fresh? This is a (hopefully temporary)
+ * kluge for Special:WantedFiles, which may contain false
+ * positives for files that exist e.g. in a shared repo (bug
+ * 6220).
+ * @return bool
+ */
+ function forceExistenceCheck() {
+ return false;
+ }
+
+ /**
+ * Format an individual result
+ *
+ * @param Skin $skin Skin to use for UI elements
+ * @param object $result Result row
+ * @return string
+ */
+ public function formatResult( $skin, $result ) {
+ $title = Title::makeTitleSafe( $result->namespace, $result->title );
+ if ( $title instanceof Title ) {
+ if ( $this->isCached() || $this->forceExistenceCheck() ) {
+ $pageLink = $this->existenceCheck( $title )
+ ? '<del>' . Linker::link( $title ) . '</del>'
+ : Linker::link( $title );
+ } else {
+ $pageLink = Linker::link(
+ $title,
+ null,
+ array(),
+ array(),
+ array( 'broken' )
+ );
+ }
+ return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
+ } else {
+ return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
+ }
+ }
+
+ /**
+ * Does the Title currently exists
+ *
+ * This method allows a subclass to override this check
+ * (For example, wantedfiles, would want to check if the file exists
+ * not just that a page in the file namespace exists).
+ *
+ * This will only control if the link is crossed out. Whether or not the link
+ * is blue vs red is controlled by if the title exists.
+ *
+ * @note This will only be run if the page is cached (ie $wgMiserMode = true)
+ * unless forceExistenceCheck() is true.
+ * @since 1.24
+ * @return boolean
+ */
+ protected function existenceCheck( Title $title ) {
+ return $title->isKnown();
+ }
+
+ /**
+ * Make a "what links here" link for a given title
+ *
+ * @param Title $title Title to make the link for
+ * @param object $result Result row
+ * @return string
+ */
+ private function makeWlhLink( $title, $result ) {
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
+ $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
+ return Linker::link( $wlh, $label );
+ }
+}