Merge "Advise extensions not to modify $wgWhitelistRead"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 12 Jul 2017 19:01:57 +0000 (19:01 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 12 Jul 2017 19:01:57 +0000 (19:01 +0000)
12 files changed:
RELEASE-NOTES-1.30
docs/hooks.txt
includes/Sanitizer.php
includes/jobqueue/jobs/RecentChangesUpdateJob.php
includes/libs/mime/MimeAnalyzer.php
includes/page/Article.php
includes/specials/SpecialUndelete.php
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.special/mediawiki.special.apisandbox.js
resources/src/mediawiki/mediawiki.feedback.js
tests/phpunit/includes/SanitizerTest.php

index be21a7d..453368b 100644 (file)
@@ -35,6 +35,8 @@ production.
   LanguageConverter variant.  This allows English-speaking developers
   to develop and test LanguageConverter more easily.  Pig Latin can be
   enabled by setting $wgUsePigLatinVariant to true.
+* Added RecentChangesPurgeRows hook to allow extensions to purge data that
+  depends on the recentchanges table.
 
 === Languages updated in 1.30 ===
 
index 8485b02..fec7d44 100644 (file)
@@ -2716,6 +2716,11 @@ random pages.
 'RecentChange_save': Called at the end of RecentChange::save().
 &$recentChange: RecentChange object
 
+'RecentChangesPurgeRows': Called when old recentchanges rows are purged, after
+deleting those rows but within the same transaction.
+$rows: The deleted rows as an array of recentchanges row objects (with up to
+  $wgUpdateRowsPerQuery items).
+
 'RedirectSpecialArticleRedirectParams': Lets you alter the set of parameter
 names such as "oldid" that are preserved when using redirecting special pages
 such as Special:MyPage and Special:MyTalk.
index dd4a314..b08bc69 100644 (file)
@@ -339,8 +339,8 @@ class Sanitizer {
         */
        static function getAttribsRegex() {
                if ( self::$attribsRegex === null ) {
-                       $attribFirst = '[:A-Z_a-z0-9]';
-                       $attrib = '[:A-Z_a-z-.0-9]';
+                       $attribFirst = "[:_\p{L}\p{N}]";
+                       $attrib = "[:_\.\-\p{L}\p{N}]";
                        $space = '[\x09\x0a\x0c\x0d\x20]';
                        self::$attribsRegex =
                                "/(?:^|$space)({$attribFirst}{$attrib}*)
@@ -351,7 +351,7 @@ class Sanitizer {
                                                | '([^']*)(?:'|\$)
                                                | (((?!$space|>).)*)
                                        )
-                               )?(?=$space|\$)/sx";
+                               )?(?=$space|\$)/sxu";
                }
                return self::$attribsRegex;
        }
index ca57d62..6f349d4 100644 (file)
@@ -86,14 +86,21 @@ class RecentChangesUpdateJob extends Job {
                $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
                $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
                do {
-                       $rcIds = $dbw->selectFieldValues( 'recentchanges',
-                               'rc_id',
+                       $rcIds = [];
+                       $rows = [];
+                       $res = $dbw->select( 'recentchanges',
+                               RecentChange::selectFields(),
                                [ 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ],
                                __METHOD__,
                                [ 'LIMIT' => $wgUpdateRowsPerQuery ]
                        );
+                       foreach ( $res as $row ) {
+                               $rcIds[] = $row->rc_id;
+                               $rows[] = $row;
+                       }
                        if ( $rcIds ) {
                                $dbw->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ );
+                               Hooks::run( 'RecentChangesPurgeRows', [ $rows ] );
                                // There might be more, so try waiting for replica DBs
                                try {
                                        $factory->commitAndWaitForReplication(
index 0083e4b..631bb17 100644 (file)
@@ -709,8 +709,17 @@ EOT;
                                        $this->logger->info( __METHOD__ . ": recognized file as video/x-matroska\n" );
                                        return "video/x-matroska";
                                } elseif ( strncmp( $data, "webm", 4 ) == 0 ) {
-                                       $this->logger->info( __METHOD__ . ": recognized file as video/webm\n" );
-                                       return "video/webm";
+                                       // XXX HACK look for a video track, if we don't find it, this is an audio file
+                                       $videotrack = strpos( $head, "\x86\x85V_VP" );
+
+                                       if ( $videotrack ) {
+                                               // There is a video track, so this is a video file.
+                                               $this->logger->info( __METHOD__ . ": recognized file as video/webm\n" );
+                                               return "video/webm";
+                                       }
+
+                                       $this->logger->info( __METHOD__ . ": recognized file as audio/webm\n" );
+                                       return "audio/webm";
                                }
                        }
                        $this->logger->info( __METHOD__ . ": unknown EBML file\n" );
index dc4096c..16328bc 100644 (file)
@@ -1692,74 +1692,127 @@ class Article implements Page {
                        $suppress = '';
                }
                $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $title );
-               $form = Html::openElement( 'form', [ 'method' => 'post',
-                       'action' => $title->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ] ) .
-                       Html::openElement( 'fieldset', [ 'id' => 'mw-delete-table' ] ) .
-                       Html::element( 'legend', null, wfMessage( 'delete-legend' )->text() ) .
-                       Html::openElement( 'div', [ 'id' => 'mw-deleteconfirm-table' ] ) .
-                       Html::openElement( 'div', [ 'id' => 'wpDeleteReasonListRow' ] ) .
-                       Html::label( wfMessage( 'deletecomment' )->text(), 'wpDeleteReasonList' ) .
-                       '&nbsp;' .
-                       Xml::listDropDown(
-                               'wpDeleteReasonList',
-                               wfMessage( 'deletereason-dropdown' )->inContentLanguage()->text(),
-                               wfMessage( 'deletereasonotherlist' )->inContentLanguage()->text(),
-                               '',
-                               'wpReasonDropDown',
-                               1
-                       ) .
-                       Html::closeElement( 'div' ) .
-                       Html::openElement( 'div', [ 'id' => 'wpDeleteReasonRow' ] ) .
-                       Html::label( wfMessage( 'deleteotherreason' )->text(), 'wpReason' ) .
-                       '&nbsp;' .
-                       Html::input( 'wpReason', $reason, 'text', [
-                               'size' => '60',
-                               'maxlength' => '255',
-                               'tabindex' => '2',
-                               'id' => 'wpReason',
-                               'class' => 'mw-ui-input-inline',
-                               'autofocus'
-                       ] ) .
-                       Html::closeElement( 'div' );
-
-               # Disallow watching if user is not logged in
-               if ( $user->isLoggedIn() ) {
-                       $form .=
-                                       Xml::checkLabel( wfMessage( 'watchthis' )->text(),
-                                               'wpWatch', 'wpWatch', $checkWatch, [ 'tabindex' => '3' ] );
-               }
-
-               $form .=
-                               Html::openElement( 'div' ) .
-                               $suppress .
-                                       Xml::submitButton( wfMessage( 'deletepage' )->text(),
-                                               [
-                                                       'name' => 'wpConfirmB',
-                                                       'id' => 'wpConfirmB',
-                                                       'tabindex' => '5',
-                                                       'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button mw-ui-destructive' : '',
-                                               ]
-                                       ) .
-                               Html::closeElement( 'div' ) .
-                       Html::closeElement( 'div' ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Html::hidden(
-                               'wpEditToken',
-                               $user->getEditToken( [ 'delete', $title->getPrefixedText() ] )
-                       ) .
-                       Xml::closeElement( 'form' );
-
-                       if ( $user->isAllowed( 'editinterface' ) ) {
-                               $link = Linker::linkKnown(
-                                       $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->getTitle(),
-                                       wfMessage( 'delete-edit-reasonlist' )->escaped(),
-                                       [],
-                                       [ 'action' => 'edit' ]
-                               );
-                               $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
+
+               $outputPage->enableOOUI();
+
+               $options = [];
+               $options[] = [
+                       'data' => 'other',
+                       'label' => $ctx->msg( 'deletereasonotherlist' )->inContentLanguage()->text(),
+               ];
+               $list = $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->text();
+               foreach ( explode( "\n", $list ) as $option ) {
+                       $value = trim( $option );
+                       if ( $value == '' ) {
+                               continue;
+                       } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
+                               $options[] = [ 'optgroup' => trim( substr( $value, 1 ) ) ];
+                       } elseif ( substr( $value, 0, 2 ) == '**' ) {
+                               $options[] = [ 'data' => trim( substr( $value, 2 ) ) ];
+                       } else {
+                               $options[] = [ 'data' => trim( $value ) ];
                        }
+               }
+
+               $fields[] = new OOUI\FieldLayout(
+                       new OOUI\DropdownInputWidget( [
+                               'name' => 'wpDeleteReasonList',
+                               'inputId' => 'wpDeleteReasonList',
+                               'tabIndex' => 1,
+                               'infusable' => true,
+                               'value' => '',
+                               'options' => $options
+                       ] ),
+                       [
+                               'label' => $ctx->msg( 'deletecomment' )->text(),
+                               'align' => 'top',
+                       ]
+               );
 
-               $outputPage->addHTML( $form );
+               $fields[] = new OOUI\FieldLayout(
+                       new OOUI\TextInputWidget( [
+                               'name' => 'wpReason',
+                               'inputId' => 'wpReason',
+                               'tabIndex' => 2,
+                               'maxLength' => 255,
+                               'infusable' => true,
+                               'value' => $reason,
+                               'autofocus' => true,
+                       ] ),
+                       [
+                               'label' => $ctx->msg( 'deleteotherreason' )->text(),
+                               'align' => 'top',
+                       ]
+               );
+
+               if ( $user->isLoggedIn() ) {
+                       $fields[] = new OOUI\FieldLayout(
+                               new OOUI\CheckboxInputWidget( [
+                                       'name' => 'wpWatch',
+                                       'inputId' => 'wpWatch',
+                                       'tabIndex' => 3,
+                                       'selected' => $checkWatch,
+                               ] ),
+                               [
+                                       'label' => $ctx->msg( 'watchthis' )->text(),
+                                       'align' => 'inline',
+                                       'infusable' => true,
+                               ]
+                       );
+               }
+
+               $fields[] = new OOUI\FieldLayout(
+                       new OOUI\ButtonInputWidget( [
+                               'name' => 'wpConfirmB',
+                               'inputId' => 'wpConfirmB',
+                               'tabIndex' => 5,
+                               'value' => $ctx->msg( 'deletepage' )->text(),
+                               'label' => $ctx->msg( 'deletepage' )->text(),
+                               'flags' => [ 'primary', 'destructive' ],
+                               'type' => 'submit',
+                       ] ),
+                       [
+                               'align' => 'top',
+                       ]
+               );
+
+               $fieldset = new OOUI\FieldsetLayout( [
+                       'label' => $ctx->msg( 'delete-legend' )->text(),
+                       'id' => 'mw-delete-table',
+                       'items' => $fields,
+               ] );
+
+               $form = new OOUI\FormLayout( [
+                       'method' => 'post',
+                       'action' => $title->getLocalURL( 'action=delete' ),
+                       'id' => 'deleteconfirm',
+               ] );
+               $form->appendContent(
+                       $fieldset,
+                       new OOUI\HtmlSnippet(
+                               Html::hidden( 'wpEditToken', $user->getEditToken( [ 'delete', $title->getPrefixedText() ] ) )
+                       )
+               );
+
+               $outputPage->addHTML(
+                       new OOUI\PanelLayout( [
+                               'classes' => [ 'deletepage-wrapper' ],
+                               'expanded' => false,
+                               'padded' => true,
+                               'framed' => true,
+                               'content' => $form,
+                       ] )
+               );
+
+               if ( $user->isAllowed( 'editinterface' ) ) {
+                       $link = Linker::linkKnown(
+                               $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->getTitle(),
+                               wfMessage( 'delete-edit-reasonlist' )->escaped(),
+                               [],
+                               [ 'action' => 'edit' ]
+                       );
+                       $outputPage->addHTML( '<p class="mw-delete-editreasons">' . $link . '</p>' );
+               }
 
                $deleteLogPage = new LogPage( 'delete' );
                $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
index 864ea0f..8a59773 100644 (file)
@@ -235,31 +235,59 @@ class SpecialUndelete extends SpecialPage {
        function showSearchForm() {
                $out = $this->getOutput();
                $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
-               $fuzzySearch = $this->getRequest()->getVal( "fuzzy", false );
-               $out->addHTML(
-                       Xml::openElement( 'form', [ 'method' => 'get', 'action' => wfScript() ] ) .
-                               Xml::fieldset( $this->msg( 'undelete-search-box' )->text() ) .
+               $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', false );
+
+               $out->enableOOUI();
+
+               $fields[] = new OOUI\ActionFieldLayout(
+                       new OOUI\TextInputWidget( [
+                               'name' => 'prefix',
+                               'inputId' => 'prefix',
+                               'infusable' => true,
+                               'value' => $this->mSearchPrefix,
+                               'autofocus' => true,
+                       ] ),
+                       new OOUI\ButtonInputWidget( [
+                               'label' => $this->msg( 'undelete-search-submit' )->text(),
+                               'flags' => [ 'primary', 'progressive' ],
+                               'inputId' => 'searchUndelete',
+                               'type' => 'submit',
+                       ] ),
+                       [
+                               'label' => new OOUI\HtmlSnippet(
+                                       $this->msg(
+                                               $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix'
+                                       )->parse()
+                               ),
+                               'align' => 'left',
+                       ]
+               );
+
+               $fieldset = new OOUI\FieldsetLayout( [
+                       'label' => $this->msg( 'undelete-search-box' )->text(),
+                       'items' => $fields,
+               ] );
+
+               $form = new OOUI\FormLayout( [
+                       'method' => 'get',
+                       'action' => wfScript(),
+               ] );
+
+               $form->appendContent(
+                       $fieldset,
+                       new OOUI\HtmlSnippet(
                                Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
-                               Html::hidden( 'fuzzy', $this->getRequest()->getVal( 'fuzzy' ) ) .
-                               Html::rawElement(
-                                       'label',
-                                       [ 'for' => 'prefix' ],
-                                       $this->msg( $fuzzySearch ? 'undelete-search-full' : 'undelete-search-prefix' )
-                                               ->parse()
-                               ) .
-                               Xml::input(
-                                       'prefix',
-                                       20,
-                                       $this->mSearchPrefix,
-                                       [ 'id' => 'prefix', 'autofocus' => '' ]
-                               ) .
-                               ' ' .
-                               Xml::submitButton(
-                                       $this->msg( 'undelete-search-submit' )->text(),
-                                       [ 'id' => 'searchUndelete' ]
-                               ) .
-                               Xml::closeElement( 'fieldset' ) .
-                               Xml::closeElement( 'form' )
+                               Html::hidden( 'fuzzy', $this->getRequest()->getVal( 'fuzzy' ) )
+                       )
+               );
+
+               $out->addHTML(
+                       new OOUI\PanelLayout( [
+                               'expanded' => false,
+                               'padded' => true,
+                               'framed' => true,
+                               'content' => $form,
+                       ] )
                );
 
                # List undeletable articles
index 5ce7988..420bb44 100644 (file)
@@ -5,6 +5,7 @@
                // Make sure this uses the interface direction, not the content direction
                direction: ltr;
                border-bottom-right-radius: 0;
+               height: 2.5em;
        }
 
        &.oo-ui-widget-enabled .oo-ui-tagMultiselectWidget-handle {
                        width: 1em;
 
                        &-widget.oo-ui-widget {
+                               border: 1px solid #a2a9b1;
+                               border-left-width: 0;
+                               border-top-left-radius: 0;
+                               border-top-right-radius: 0;
+                               border-bottom-left-radius: 0;
+
                                display: block;
                                text-align: right;
+                               height: 2.5em;
+                               box-sizing: border-box;
 
-                               // Override OOUI rules
-                               &.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:first-child a.oo-ui-buttonElement-button,
-                               .oo-ui-buttonOptionWidget a.oo-ui-buttonElement-button {
-                                       border-radius: 0;
-                                       border-left: 0;
-                               }
-
-                               &.oo-ui-buttonSelectWidget .oo-ui-buttonOptionWidget:last-child a.oo-ui-buttonElement-button {
-                                       border-bottom-right-radius: 2px;
+                               .oo-ui-buttonElement-frameless.oo-ui-iconElement:first-child {
+                                       margin-left: 0;
                                }
 
                        }
index 4bee31e..dcada85 100644 (file)
                                classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
                                items: [
                                        new OO.ui.ButtonOptionWidget( {
+                                               framed: false,
                                                data: 'namespaces',
                                                icon: 'article',
                                                title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
                                        } ),
                                        new OO.ui.ButtonOptionWidget( {
+                                               framed: false,
                                                data: 'tags',
                                                icon: 'tag',
                                                title: mw.msg( 'rcfilters-view-tags-tooltip' )
index 694f86a..7e9ad7f 100644 (file)
                                        break;
 
                                case 'text':
-                                       widget = new OO.ui.TextInputWidget( {
-                                               multiline: true,
+                                       widget = new OO.ui.MultilineTextInputWidget( {
                                                required: Util.apiBool( pi.required )
                                        } );
                                        widget.paramInfo = pi;
                                        new OO.ui.MenuOptionWidget( {
                                                label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
                                                data: new OO.ui.FieldLayout(
-                                                       jsonInput = new OO.ui.TextInputWidget( {
+                                                       jsonInput = new OO.ui.MultilineTextInputWidget( {
                                                                classes: [ 'mw-apisandbox-textInputCode' ],
                                                                readOnly: true,
-                                                               multiline: true,
                                                                autosize: true,
                                                                maxRows: 6,
                                                                value: JSON.stringify( displayParams, null, '\t' )
index 6abdf83..f0e13b4 100644 (file)
                        classes: [ 'mw-feedbackDialog-welcome-message' ]
                } );
                this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
-                       indicator: 'required',
-                       multiline: false
+                       indicator: 'required'
                } );
-               this.feedbackMessageInput = new OO.ui.TextInputWidget( {
-                       autosize: true,
-                       multiline: true
+               this.feedbackMessageInput = new OO.ui.MultilineTextInputWidget( {
+                       autosize: true
                } );
                feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
                        label: mw.msg( 'feedback-subject' )
index c237c50..abcf1d4 100644 (file)
@@ -178,6 +178,10 @@ class SanitizerTest extends MediaWikiTestCase {
        public static function provideTagAttributesToDecode() {
                return [
                        [ [ 'foo' => 'bar' ], 'foo=bar', 'Unquoted attribute' ],
+                       [ [ 'עברית' => 'bar' ], 'עברית=bar', 'Non-Latin attribute' ],
+                       [ [ '६' => 'bar' ], '६=bar', 'Devanagari number' ],
+                       [ [ '搭𨋢' => 'bar' ], '搭𨋢=bar', 'Non-BMP character' ],
+                       [ [], 'ńgh=bar', 'Combining accent is not allowed' ],
                        [ [ 'foo' => 'bar' ], '    foo   =   bar    ', 'Spaced attribute' ],
                        [ [ 'foo' => 'bar' ], 'foo="bar"', 'Double-quoted attribute' ],
                        [ [ 'foo' => 'bar' ], 'foo=\'bar\'', 'Single-quoted attribute' ],