From a1789ddde42033f1b05cc4929491214ee6e79383 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 17 Dec 2015 09:15:42 +0100 Subject: Update to MediaWiki 1.26.0 --- includes/specialpage/ChangesListSpecialPage.php | 3 +- includes/specialpage/FormSpecialPage.php | 6 ++- includes/specialpage/QueryPage.php | 49 ++++++++++++++------ includes/specialpage/RedirectSpecialPage.php | 33 ++++++++----- includes/specialpage/SpecialPage.php | 27 +++++------ includes/specialpage/SpecialPageFactory.php | 61 +++++++------------------ 6 files changed, 91 insertions(+), 88 deletions(-) (limited to 'includes/specialpage') diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index b9132358..23bd394c 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -434,7 +434,8 @@ abstract class ChangesListSpecialPage extends SpecialPage { $legend .= Html::element( 'dt', array( 'class' => $cssClass ), $context->msg( $letter )->text() ) . "\n" . - Html::rawElement( 'dd', array(), + Html::rawElement( 'dd', + array( 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ), $context->msg( $label )->parse() ) . "\n"; } diff --git a/includes/specialpage/FormSpecialPage.php b/includes/specialpage/FormSpecialPage.php index 90567617..42c59806 100644 --- a/includes/specialpage/FormSpecialPage.php +++ b/includes/specialpage/FormSpecialPage.php @@ -96,7 +96,11 @@ abstract class FormSpecialPage extends SpecialPage { $this->getMessagePrefix() ); $form->setSubmitCallback( array( $this, 'onSubmit' ) ); - $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + if ( $this->getDisplayFormat() !== 'ooui' ) { + // No legend and wrapper by default in OOUI forms, but can be set manually + // from alterForm() + $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' ); + } $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' ); if ( !$headerMsg->isDisabled() ) { diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index 1ff7e3fb..3c8b7420 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -70,7 +70,7 @@ abstract class QueryPage extends SpecialPage { array( 'DeadendPagesPage', 'Deadendpages' ), array( 'DoubleRedirectsPage', 'DoubleRedirects' ), array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ), - array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'), + array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles' ), array( 'LinkSearchPage', 'LinkSearch' ), array( 'ListredirectsPage', 'Listredirects' ), array( 'LonelyPagesPage', 'Lonelypages' ), @@ -141,7 +141,7 @@ abstract class QueryPage extends SpecialPage { * @return array * @since 1.18 */ - function getQueryInfo() { + public function getQueryInfo() { return null; } @@ -178,7 +178,7 @@ abstract class QueryPage extends SpecialPage { * @return bool * @since 1.18 */ - function usesTimestamps() { + public function usesTimestamps() { return false; } @@ -198,7 +198,7 @@ abstract class QueryPage extends SpecialPage { * * @return bool */ - function isExpensive() { + public function isExpensive() { return $this->getConfig()->get( 'DisableQueryPages' ); } @@ -219,7 +219,7 @@ abstract class QueryPage extends SpecialPage { * * @return bool */ - function isCached() { + public function isCached() { return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' ); } @@ -252,6 +252,17 @@ abstract class QueryPage extends SpecialPage { return ''; } + /** + * Outputs some kind of an informative message (via OutputPage) to let the + * user know that the query returned nothing and thus there's nothing to + * show. + * + * @since 1.26 + */ + protected function showEmptyText() { + $this->getOutput()->addWikiMsg( 'specialpage-empty' ); + } + /** * 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 @@ -283,7 +294,7 @@ abstract class QueryPage extends SpecialPage { * @throws DBError|Exception * @return bool|int */ - function recache( $limit, $ignoreErrors = true ) { + public function recache( $limit, $ignoreErrors = true ) { if ( !$this->isCacheable() ) { return 0; } @@ -359,7 +370,7 @@ abstract class QueryPage extends SpecialPage { * @return ResultWrapper * @since 1.18 */ - function reallyDoQuery( $limit, $offset = false ) { + public function reallyDoQuery( $limit, $offset = false ) { $fname = get_class( $this ) . "::reallyDoQuery"; $dbr = $this->getRecacheDB(); $query = $this->getQueryInfo(); @@ -410,7 +421,7 @@ abstract class QueryPage extends SpecialPage { * @param int|bool $limit * @return ResultWrapper */ - function doQuery( $offset = false, $limit = false ) { + public function doQuery( $offset = false, $limit = false ) { if ( $this->isCached() && $this->isCacheable() ) { return $this->fetchFromCache( $limit, $offset ); } else { @@ -425,7 +436,7 @@ abstract class QueryPage extends SpecialPage { * @return ResultWrapper * @since 1.18 */ - function fetchFromCache( $limit, $offset = false ) { + public function fetchFromCache( $limit, $offset = false ) { $dbr = wfGetDB( DB_SLAVE ); $options = array(); if ( $limit !== false ) { @@ -459,12 +470,24 @@ abstract class QueryPage extends SpecialPage { return $this->cachedTimestamp; } + /** + * Returns limit and offset, as returned by $this->getRequest()->getLimitOffset(). + * Subclasses may override this to further restrict or modify limit and offset. + * + * @since 1.26 + * + * @return int[] list( $limit, $offset ) + */ + protected function getLimitOffset() { + return $this->getRequest()->getLimitOffset(); + } + /** * This is the actual workhorse. It does everything needed to make a * real, honest-to-gosh query page. * @param string $par */ - function execute( $par ) { + public function execute( $par ) { $user = $this->getUser(); if ( !$this->userCanExecute( $user ) ) { $this->displayRestrictionError(); @@ -484,7 +507,7 @@ abstract class QueryPage extends SpecialPage { $out->setSyndicated( $this->isSyndicated() ); if ( $this->limit == 0 && $this->offset == 0 ) { - list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset(); + list( $this->limit, $this->offset ) = $this->getLimitOffset(); } // @todo Use doQuery() @@ -527,7 +550,7 @@ abstract class QueryPage extends SpecialPage { $this->numRows = $res->numRows(); - $dbr = wfGetDB( DB_SLAVE ); + $dbr = $this->getRecacheDB(); $this->preprocessResults( $dbr, $res ); $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) ); @@ -546,7 +569,7 @@ abstract class QueryPage extends SpecialPage { } 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' ); + $this->showEmptyText(); $out->addHTML( Xml::closeElement( 'div' ) ); return; } diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index 2e6e55a7..9129ee5d 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -33,8 +33,11 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { // Query parameters added by redirects protected $mAddedRedirectParams = array(); - public function execute( $par ) { - $redirect = $this->getRedirect( $par ); + /** + * @param string|null $subpage + */ + public function execute( $subpage ) { + $redirect = $this->getRedirect( $subpage ); $query = $this->getRedirectQuery(); // Redirect to a page title with possible query parameters if ( $redirect instanceof Title ) { @@ -58,22 +61,24 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { * If the special page is a redirect, then get the Title object it redirects to. * False otherwise. * - * @param string $par Subpage string + * @param string|null $subpage * @return Title|bool */ - abstract public function getRedirect( $par ); + abstract public function getRedirect( $subpage ); /** * Return part of the request string for a special redirect page * This allows passing, e.g. action=history to Special:Mypage, etc. * - * @return string + * @return array|bool */ public function getRedirectQuery() { $params = array(); $request = $this->getRequest(); - foreach ( $this->mAllowedRedirectParams as $arg ) { + foreach ( array_merge( $this->mAllowedRedirectParams, + array( 'uselang', 'useskin', 'debug' ) // parameters which can be passed to all pages + ) as $arg ) { if ( $request->getVal( $arg, null ) !== null ) { $params[$arg] = $request->getVal( $arg ); } elseif ( $request->getArray( $arg, null ) !== null ) { @@ -112,12 +117,16 @@ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { $this->mAddedRedirectParams = $addedRedirectParams; } + /** + * @param string|null $subpage + * @return Title|bool + */ public function getRedirect( $subpage ) { if ( $this->redirSubpage === false ) { return SpecialPage::getTitleFor( $this->redirName, $subpage ); - } else { - return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage ); } + + return SpecialPage::getTitleFor( $this->redirName, $this->redirSubpage ); } } @@ -140,11 +149,11 @@ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { * - 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 + * https://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 + * https://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. @@ -159,7 +168,7 @@ abstract class SpecialRedirectToSpecial extends RedirectSpecialPage { * - 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 + * https://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 @@ -198,7 +207,7 @@ abstract class RedirectSpecialArticle extends RedirectSpecialPage { 'section', 'oldid', 'diff', 'dir', 'limit', 'offset', 'feed', # Misc options - 'redlink', 'debug', + 'redlink', # Options for action=raw; missing ctype can break JS or CSS in some browsers 'ctype', 'maxage', 'smaxage', ); diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php index a7a43b0e..65a4eb9a 100644 --- a/includes/specialpage/SpecialPage.php +++ b/includes/specialpage/SpecialPage.php @@ -662,7 +662,6 @@ class SpecialPage { */ 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(); @@ -671,18 +670,6 @@ class SpecialPage { } 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; @@ -697,8 +684,16 @@ class SpecialPage { * @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 '-'; + return 'other'; + } + + /** + * Call wfTransactionalTimeLimit() if this request was POSTed + * @since 1.26 + */ + protected function useTransactionalTimeLimit() { + if ( $this->getRequest()->wasPosted() ) { + wfTransactionalTimeLimit(); + } } } diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index dedfcb6a..e794a5df 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -218,7 +218,7 @@ class SpecialPageFactory { global $wgSpecialPages; global $wgDisableInternalSearch, $wgEmailAuthentication; global $wgEnableEmail, $wgEnableJavaScriptTest; - global $wgPageLanguageUseDB; + global $wgPageLanguageUseDB, $wgContentHandlerUseDB; if ( !is_array( self::$list ) ) { @@ -244,6 +244,9 @@ class SpecialPageFactory { if ( $wgPageLanguageUseDB ) { self::$list['PageLanguage'] = 'SpecialPageLanguage'; } + if ( $wgContentHandlerUseDB ) { + self::$list['ChangeContentModel'] = 'SpecialChangeContentModel'; + } self::$list['Activeusers'] = 'SpecialActiveUsers'; @@ -260,14 +263,13 @@ class SpecialPageFactory { } /** - * 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 + * Initialise and return the list of special page aliases. Returns an array where + * the key is an alias, and the value is the canonical name of the special page. + * All registered special pages are guaranteed to map to themselves. + * @return array */ - private static function getAliasListObject() { - if ( !is_object( self::$aliases ) ) { + private static function getAliasList() { + if ( is_null( self::$aliases ) ) { global $wgContLang; $aliases = $wgContLang->getSpecialPageAliases(); $pageList = self::getPageList(); @@ -310,9 +312,6 @@ class SpecialPageFactory { } } } - - // Cast to object: func()[$key] doesn't work, but func()->$key does - self::$aliases = (object)self::$aliases; } return self::$aliases; @@ -332,8 +331,9 @@ class SpecialPageFactory { $caseFoldedAlias = $wgContLang->caseFold( $bits[0] ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); - if ( isset( self::getAliasListObject()->$caseFoldedAlias ) ) { - $name = self::getAliasListObject()->$caseFoldedAlias; + $aliases = self::getAliasList(); + if ( isset( $aliases[$caseFoldedAlias] ) ) { + $name = $aliases[$caseFoldedAlias]; } else { return array( null, null ); } @@ -347,34 +347,6 @@ class SpecialPageFactory { 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 * @@ -572,7 +544,6 @@ class SpecialPageFactory { $context->setTitle( $page->getPageTitle( $par ) ); } } elseif ( !$page->isIncludable() ) { - return false; } @@ -638,7 +609,7 @@ class SpecialPageFactory { public static function getLocalNameFor( $name, $subpage = false ) { global $wgContLang; $aliases = $wgContLang->getSpecialPageAliases(); - $aliasList = self::getAliasListObject(); + $aliasList = self::getAliasList(); // Find the first alias that maps back to $name if ( isset( $aliases[$name] ) ) { @@ -646,8 +617,8 @@ class SpecialPageFactory { foreach ( $aliases[$name] as $alias ) { $caseFoldedAlias = $wgContLang->caseFold( $alias ); $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); - if ( isset( $aliasList->$caseFoldedAlias ) && - $aliasList->$caseFoldedAlias === $name + if ( isset( $aliasList[$caseFoldedAlias] ) && + $aliasList[$caseFoldedAlias] === $name ) { $name = $alias; $found = true; -- cgit v1.2.3-54-g00ecf From 257401d8b2cf661adf36c84b0e3fd1cf85e33c22 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 18 Dec 2015 06:04:58 +0100 Subject: Update to MediaWiki 1.26.1 --- RELEASE-NOTES-1.26 | 25 ++++++++++ .../SyntaxHighlight_GeSHi.class.php | 29 ++++++++---- extensions/SyntaxHighlight_GeSHi/composer.json | 2 +- .../maintenance/updateCSS.php | 22 +++++++-- .../maintenance/updateLexerList.php | 23 +++++++-- includes/DefaultSettings.php | 8 +++- includes/Hooks.php | 42 +---------------- includes/HttpFunctions.php | 17 ++++++- includes/MediaWiki.php | 44 +++++++++++++++--- includes/Setup.php | 15 ++++++ includes/User.php | 7 ++- includes/debug/logger/LoggerFactory.php | 2 +- includes/libs/MultiHttpClient.php | 13 ++++++ includes/libs/objectcache/APCBagOStuff.php | 54 +++++++++++++++++++++- includes/specialpage/RedirectSpecialPage.php | 12 +++++ includes/specials/SpecialExpandTemplates.php | 2 +- includes/specials/SpecialMyLanguage.php | 11 +++++ includes/specials/SpecialMyRedirectPages.php | 50 ++++++++++++++++++++ includes/specials/SpecialSearch.php | 2 +- includes/utils/IP.php | 22 ++++++--- .../includes/parser/MediaWikiParserTest.php | 1 + tests/phpunit/includes/parser/NewParserTest.php | 1 + tests/phpunit/includes/utils/IPTest.php | 33 +++++++++++-- tests/phpunit/suite.xml | 4 ++ 24 files changed, 356 insertions(+), 85 deletions(-) (limited to 'includes/specialpage') diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index 81405f50..0adfbe20 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -1,6 +1,31 @@ Security reminder: If you have PHP's register_globals option set, you must turn it off. MediaWiki will not work with it enabled. +== MediaWiki 1.26.1 == + +This is a maintenance release of the MediaWiki 1.26 branch. + +=== Changes since 1.26.0 === +* (T117899) SECURITY: $wgArticlePath can no longer be set to relative paths + that do not begin with a slash. This enabled trivial XSS attacks. + Configuration values such as "http://my.wiki.com/wiki/$1" are fine, as are + "/wiki/$1". A value such as "$1" or "wiki/$1" is not and will now throw an + error. +* (T119309) SECURITY: Use hash_equals() for edit token comparison +* (T118032) SECURITY: Don't allow cURL to interpret POST parameters starting + with '@' as file uploads +* (T115522) SECURITY: Passwords generated by User::randomPassword() can no + longer be shorter than $wgMinimalPasswordLength +* (T97897) SECURITY: Improve IP parsing and trimming. Previous behavior could + result in improper blocks being issued +* (T109724) SECURITY: Special:MyPage, Special:MyTalk, Special:MyContributions + and related pages no longer use HTTP redirects and are now redirected by + MediaWiki +* Fixed ConfigException in ExpandTemplates due to AlwaysUseTidy. +* Fixed stray literal \n in Special:Search. +* Fix issue that breaks HHVM Repo Authorative mode. +* (T120267) Work around APCu memory corruption bug + == MediaWiki 1.26 == === Configuration changes in 1.26 === diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php index 535e37af..9eed2763 100644 --- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php +++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php @@ -16,7 +16,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; // @codingStandardsIgnoreStart class SyntaxHighlight_GeSHi { @@ -276,18 +276,29 @@ class SyntaxHighlight_GeSHi { $output = $cache->get( $cacheKey ); if ( $output === false ) { - try { - $pygments = new Pygments( $wgPygmentizePath ); - $output = $pygments->highlight( $code, $lexer, 'html', $options ); - } catch ( RuntimeException $e ) { + $optionPairs = array(); + foreach ( $options as $k => $v ) { + $optionPairs[] = "{$k}={$v}"; + } + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + $process = $builder + ->add( '-l' )->add( $lexer ) + ->add( '-f' )->add( 'html' ) + ->add( '-O' )->add( implode( ',', $optionPairs ) ) + ->getProcess(); + + $process->setInput( $code ); + $process->run(); + + if ( !$process->isSuccessful() ) { $status->warning( 'syntaxhighlight-error-pygments-invocation-failure' ); - wfWarn( - 'Failed to invoke Pygments. Please check that Pygments is installed ' . - 'and that $wgPygmentizePath is accurate.' - ); + wfWarn( 'Failed to invoke Pygments: ' . $process->getErrorOutput() ); $status->value = self::highlight( $code, null, $args )->getValue(); return $status; } + + $output = $process->getOutput(); $cache->set( $cacheKey, $output ); } diff --git a/extensions/SyntaxHighlight_GeSHi/composer.json b/extensions/SyntaxHighlight_GeSHi/composer.json index 709c1fb0..d8b8cc8e 100644 --- a/extensions/SyntaxHighlight_GeSHi/composer.json +++ b/extensions/SyntaxHighlight_GeSHi/composer.json @@ -2,7 +2,7 @@ "name": "mediawiki/syntax-highlight_geshi", "description": "Syntax highlighting extension for MediaWiki", "require": { - "kzykhys/pygments": "1.0" + "symfony/process": "~2.5" }, "require-dev": { "jakub-onderka/php-parallel-lint": "0.9", diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php index a3c0c817..9299cd74 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -39,9 +39,25 @@ class UpdateCSS extends Maintenance { global $wgPygmentizePath; $target = __DIR__ . '/../modules/pygments.generated.css'; - $pygments = new Pygments( $wgPygmentizePath ); $css = "/* Stylesheet generated by updateCSS.php */\n"; - $css .= $pygments->getCss( 'default', '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder + ->add( '-f' )->add( 'html' ) + ->add( '-S' )->add( 'default' ) + ->add( '-a' )->add( '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ) + ->getProcess(); + + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $css .= $process->getOutput(); + if ( file_put_contents( $target, $css ) === false ) { $this->output( "Failed to write to {$target}\n" ); } else { diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php index 75beb9b5..b5a7fc5a 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -43,8 +43,25 @@ class UpdateLanguageList extends Maintenance { $header = '// Generated by ' . basename( __FILE__ ) . "\n\n"; - $pygments = new Pygments( $wgPygmentizePath ); - $lexers = array_keys( $pygments->getLexers() ); + $lexers = array(); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder->add( '-L' )->add( 'lexer' )->getProcess(); + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $output = $process->getOutput(); + foreach ( explode( "\n", $output ) as $line ) { + if ( substr( $line, 0, 1 ) === '*' ) { + $newLexers = explode( ', ', trim( $line, "* :\n" ) ); + $lexers = array_merge( $lexers, $newLexers ); + } + } sort( $lexers ); $code = "getMessage(); - } catch ( Exception $e ) { - restore_error_handler(); - throw $e; - } - - restore_error_handler(); + $retval = call_user_func_array( $callback, $hook_args ); // Process the return value. if ( is_string( $retval ) ) { // String returned means error. throw new FatalError( $retval ); - } elseif ( $badhookmsg !== null ) { - // Exception was thrown from Hooks::hookErrorHandler. - throw new MWException( - 'Detected bug in an extension! ' . - "Hook $func has invalid call signature; " . $badhookmsg - ); } elseif ( $retval === false ) { // False was returned. Stop processing, but no error. return false; @@ -229,27 +212,4 @@ class Hooks { return true; } - - /** - * Handle PHP errors issued inside a hook. Catch errors that have to do - * with a function expecting a reference, and pass all others through to - * MWExceptionHandler::handleError() for default processing. - * - * @since 1.18 - * - * @param int $errno Error number (unused) - * @param string $errstr Error message - * @throws MWHookException If the error has to do with the function signature - * @return bool - */ - public static function hookErrorHandler( $errno, $errstr ) { - if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { - throw new MWHookException( $errstr, $errno ); - } - - // Delegate unhandled errors to the default MW handler - return call_user_func_array( - 'MWExceptionHandler::handleError', func_get_args() - ); - } } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index bc5a9570..3dff9711 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -779,7 +779,22 @@ class CurlHttpRequest extends MWHttpRequest { $this->curlOptions[CURLOPT_HEADER] = true; } elseif ( $this->method == 'POST' ) { $this->curlOptions[CURLOPT_POST] = true; - $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; + $postData = $this->postData; + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true; + } else if ( is_array( $postData ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $postData = wfArrayToCgi( $postData ); + } + $this->curlOptions[CURLOPT_POSTFIELDS] = $postData; + // Suppress 'Expect: 100-continue' header, as some servers // will reject it with a 417 and Curl won't auto retry // with HTTP 1.0 fallback diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index fbacb250..2da2f6ce 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -36,6 +36,11 @@ class MediaWiki { */ private $config; + /** + * @var String Cache what action this request is + */ + private $action; + /** * @param IContextSource|null $context */ @@ -141,13 +146,11 @@ class MediaWiki { * @return string Action */ public function getAction() { - static $action = null; - - if ( $action === null ) { - $action = Action::getActionName( $this->context ); + if ( $this->action === null ) { + $this->action = Action::getActionName( $this->context ); } - return $action; + return $this->action; } /** @@ -242,8 +245,37 @@ class MediaWiki { // Handle any other redirects. // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + // Prevent information leak via Special:MyPage et al (T109724) + if ( $title->isSpecialPage() ) { + $specialPage = SpecialPageFactory::getPage( $title->getDBKey() ); + if ( $specialPage instanceof RedirectSpecialPage + && $this->config->get( 'HideIdentifiableRedirects' ) + && $specialPage->personallyIdentifiableTarget() + ) { + list( , $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBKey() ); + $target = $specialPage->getRedirect( $subpage ); + // target can also be true. We let that case fall through to normal processing. + if ( $target instanceof Title ) { + $query = $specialPage->getRedirectQuery() ?: array(); + $request = new DerivativeRequest( $this->context->getRequest(), $query ); + $request->setRequestURL( $this->context->getRequest()->getRequestURL() ); + $this->context->setRequest( $request ); + // Do not varnish cache these. May vary even for anons + $this->context->getOutput()->lowerCdnMaxage( 0 ); + $this->context->setTitle( $target ); + $wgTitle = $target; + // Reset action type cache. (Special pages have only view) + $this->action = null; + $title = $target; + $output->addJsConfigVars( array( + 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ), + ) ); + $output->addModules( 'mediawiki.action.view.redirect' ); + } + } + } - // Special pages + // Special pages ($title may have changed since if statement above) if ( NS_SPECIAL == $title->getNamespace() ) { // Actions that need to be made when we have a special pages SpecialPageFactory::executePath( $title, $this->context ); diff --git a/includes/Setup.php b/includes/Setup.php index 70e8cde4..905a1d10 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -510,6 +510,21 @@ MWExceptionHandler::installHandler(); require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; + +$ps_validation = Profiler::instance()->scopedProfileIn( $fname . '-validation' ); + +// T48998: Bail out early if $wgArticlePath is non-absolute +if ( !preg_match( '/^(https?:\/\/|\/)/', $wgArticlePath ) ) { + throw new FatalError( + 'If you use a relative URL for $wgArticlePath, it must start ' . + 'with a slash (/).

See ' . + '' . + 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath.' + ); +} + +Profiler::instance()->scopedProfileOut( $ps_validation ); + $ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); if ( $wgScriptExtension !== '.php' || defined( 'MW_ENTRY_PHP5' ) ) { diff --git a/includes/User.php b/includes/User.php index 22c90cdd..199dd1dc 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1029,11 +1029,10 @@ class User implements IDBAccessObject { // stopping at a minimum of 10 chars. $length = max( 10, $wgMinimalPasswordLength ); // Multiply by 1.25 to get the number of hex characters we need - $length = $length * 1.25; // Generate random hex chars - $hex = MWCryptRand::generateHex( $length ); + $hex = MWCryptRand::generateHex( ceil( $length * 1.25 ) ); // Convert from base 16 to base 32 to get a proper password like string - return wfBaseConvert( $hex, 16, 32 ); + return substr( wfBaseConvert( $hex, 16, 32, $length ), -$length ); } /** @@ -4177,7 +4176,7 @@ class User implements IDBAccessObject { $salt, $request ?: $this->getRequest(), $timestamp ); - if ( $val != $sessionToken ) { + if ( !hash_equals( $sessionToken, $val ) ) { wfDebug( "User::matchEditToken: broken session data\n" ); } diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php index 0b6965ff..1e44b708 100644 --- a/includes/debug/logger/LoggerFactory.php +++ b/includes/debug/logger/LoggerFactory.php @@ -94,7 +94,7 @@ class LoggerFactory { * @return \\Psr\\Log\\LoggerInterface */ public static function getInstance( $channel ) { - if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { $message = ( 'MediaWiki requires the PSR-3 logging ' . "library to be present. This library is not embedded directly in MediaWiki's " . diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 6af3ed51..5555cbcb 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -335,6 +335,19 @@ class MultiHttpClient { ); } elseif ( $req['method'] === 'POST' ) { curl_setopt( $ch, CURLOPT_POST, 1 ); + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true ); + } else if ( is_array( $req['body'] ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $req['body'] = wfArrayToCgi( $req['body'] ); + } curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] ); } else { if ( is_resource( $req['body'] ) || $req['body'] !== '' ) { diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0dbbaba9..35e05e80 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -27,22 +27,72 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + + /** + * @var bool If true, trust the APC implementation to serialize and + * deserialize objects correctly. If false, (de-)serialize in PHP. + */ + protected $nativeSerialize; + /** * @var string String to append to each APC key. This may be changed * whenever the handling of values is changed, to prevent existing code * from encountering older values which it cannot handle. - **/ - const KEY_SUFFIX = ':1'; + */ + const KEY_SUFFIX = ':2'; + + /** + * Constructor + * + * Available parameters are: + * - nativeSerialize: If true, pass objects to apc_store(), and trust it + * to serialize them correctly. If false, serialize + * all values in PHP. + * + * @param array $params + */ + public function __construct( array $params = array() ) { + parent::__construct( $params ); + + if ( isset( $params['nativeSerialize'] ) ) { + $this->nativeSerialize = $params['nativeSerialize']; + } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) { + // APCu has a memory corruption bug when the serializer is set to 'default'. + // See T120267, and upstream bug reports: + // - https://github.com/krakjoe/apcu/issues/38 + // - https://github.com/krakjoe/apcu/issues/35 + // - https://github.com/krakjoe/apcu/issues/111 + $this->logger->warning( + 'The APCu extension is loaded and the apc.serializer INI setting ' . + 'is set to "default". This can cause memory corruption! ' . + 'You should change apc.serializer to "php" instead. ' . + 'See .' + ); + $this->nativeSerialize = false; + } else { + $this->nativeSerialize = true; + } + } public function get( $key, &$casToken = null, $flags = 0 ) { $val = apc_fetch( $key . self::KEY_SUFFIX ); $casToken = $val; + if ( is_string( $val ) && !$this->nativeSerialize ) { + $val = $this->isInteger( $val ) + ? intval( $val ) + : unserialize( $val ); + } + return $val; } public function set( $key, $value, $exptime = 0 ) { + if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + apc_store( $key . self::KEY_SUFFIX, $value, $exptime ); return true; diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index 9129ee5d..5047354e 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -94,6 +94,18 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { ? $params : false; } + + /** + * Indicate if the target of this redirect can be used to identify + * a particular user of this wiki (e.g., if the redirect is to the + * user page of a User). See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return false; + } } /** diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index b7582e6c..06eb2769 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage { } $config = $this->getConfig(); - if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + if ( $config->get( 'UseTidy' ) && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); } diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 3d8ff97b..d11fbe63 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -99,4 +99,15 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return $base; } } + + /** + * Target can identify a specific user's language preference. + * + * @see T109724 + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php index 5ef03f13..850b1f63 100644 --- a/includes/specials/SpecialMyRedirectPages.php +++ b/includes/specials/SpecialMyRedirectPages.php @@ -45,6 +45,16 @@ class SpecialMypage extends RedirectSpecialArticle { return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -68,6 +78,16 @@ class SpecialMytalk extends RedirectSpecialArticle { return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -90,6 +110,16 @@ class SpecialMycontributions extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -110,6 +140,16 @@ class SpecialMyuploads extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -132,4 +172,14 @@ class SpecialAllMyUploads extends RedirectSpecialPage { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index f50fb732..8c546edd 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -388,7 +388,7 @@ class SpecialSearch extends SpecialPage { } } - $out->addHTML( '
\n' ); + $out->addHTML( '
' ); if ( $prevnext ) { $out->addHTML( "

{$prevnext}

\n" ); } diff --git a/includes/utils/IP.php b/includes/utils/IP.php index 666660aa..13586f3c 100644 --- a/includes/utils/IP.php +++ b/includes/utils/IP.php @@ -132,8 +132,9 @@ class IP { /** * Convert an IP into a verbose, uppercase, normalized form. - * IPv6 addresses in octet notation are expanded to 8 words. - * IPv4 addresses are just trimmed. + * Both IPv4 and IPv6 addresses are trimmed. Additionally, + * IPv6 addresses in octet notation are expanded to 8 words; + * IPv4 addresses have leading zeros, in each octet, removed. * * @param string $ip IP address in quad or octet form (CIDR or not). * @return string @@ -143,8 +144,16 @@ class IP { if ( $ip === '' ) { return null; } - if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) { - return $ip; // nothing else to do for IPv4 addresses or invalid ones + /* If not an IP, just return trimmed value, since sanitizeIP() is called + * in a number of contexts where usernames are supplied as input. + */ + if ( !self::isIPAddress($ip) ) { + return $ip; + } + if ( self::isIPv4( $ip ) ) { + // Remove leading 0's from octet representation of IPv4 address + $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip ); + return $ip; } // Remove any whitespaces, convert to upper case $ip = strtoupper( $ip ); @@ -399,8 +408,9 @@ class IP { if ( self::isIPv6( $ip ) ) { $n = 'v6-' . self::IPv6ToRawHex( $ip ); } elseif ( self::isIPv4( $ip ) ) { - // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08) - $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip ); + // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08), + // also double/triple 0 needs to be changed to just a single 0 for ip2long. + $ip = self::sanitizeIP( $ip ); $n = ip2long( $ip ); if ( $n < 0 ) { $n += pow( 2, 32 ); diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index 96ae3bec..210c17c5 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -7,6 +7,7 @@ require_once __DIR__ . '/NewParserTest.php'; * an PHPUnit_Framework_Test object * * @group Parser + * @group ParserTests * @group Database */ class MediaWikiParserTest { diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index df7da98c..d95e9225 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -672,6 +672,7 @@ class NewParserTest extends MediaWikiTestCase { /** * @group medium + * @group ParserTests * @dataProvider parserTestProvider * @param string $desc * @param string $input diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php index 04b8f486..34aff796 100644 --- a/tests/phpunit/includes/utils/IPTest.php +++ b/tests/phpunit/includes/utils/IPTest.php @@ -307,12 +307,34 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Improve IP::sanitizeIP() code coverage - * @todo Most probably incomplete + * @covers IP::sanitizeIP + * @dataProvider provideSanitizeIP */ - public function testSanitizeIP() { - $this->assertNull( IP::sanitizeIP( '' ) ); - $this->assertNull( IP::sanitizeIP( ' ' ) ); + public function testSanitizeIP( $expected, $input ) { + $result = IP::sanitizeIP( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testSanitizeIP() + */ + public static function provideSanitizeIP() { + return array( + array( '0.0.0.0', '0.0.0.0' ), + array( '0.0.0.0', '00.00.00.00' ), + array( '0.0.0.0', '000.000.000.000' ), + array( '141.0.11.253', '141.000.011.253' ), + array( '1.2.4.5', '1.2.4.5' ), + array( '1.2.4.5', '01.02.04.05' ), + array( '1.2.4.5', '001.002.004.005' ), + array( '10.0.0.1', '010.0.000.1' ), + array( '80.72.250.4', '080.072.250.04' ), + array( 'Foo.1000.00', 'Foo.1000.00'), + array( 'Bar.01', 'Bar.01'), + array( 'Bar.010', 'Bar.010'), + array( null, ''), + array( null, ' ') + ); } /** @@ -336,6 +358,7 @@ class IPTest extends PHPUnit_Framework_TestCase { array( '80000000', '128.0.0.0' ), array( 'DEADCAFE', '222.173.202.254' ), array( 'FFFFFFFF', '255.255.255.255' ), + array( '8D000BFD', '141.000.11.253' ), array( false, 'IN.VA.LI.D' ), array( 'v6-00000000000000000000000000000001', '::1' ), array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ), diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index 1acbc241..82086b9d 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -24,6 +24,10 @@ phpunit.php enables colors for other OSs at runtime languages + + includes/parser/MediaWikiParserTest.php + suites/ExtensionsParserTestSuite.php + skins -- cgit v1.2.3-54-g00ecf