Merge "Create API to allow content handlers to handle structured data definitions"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 7 Jul 2016 16:56:09 +0000 (16:56 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 7 Jul 2016 16:56:09 +0000 (16:56 +0000)
23 files changed:
RELEASE-NOTES-1.28
autoload.php
docs/hooks.txt
includes/DefaultSettings.php
includes/Setup.php
includes/actions/Action.php
includes/actions/HistoryAction.php
includes/api/ApiHelp.php
includes/api/ApiUpload.php
includes/diff/DifferenceEngine.php
includes/gallery/ImageGalleryBase.php
includes/gallery/SliderImageGallery.php [new file with mode: 0644]
includes/page/ImagePage.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialContributions.php
includes/specials/SpecialUpload.php
includes/upload/UploadBase.php
maintenance/interwiki.list
maintenance/jsduck/categories.json
resources/Resources.php
resources/src/mediawiki/api.js
resources/src/mediawiki/page/gallery-slider.js [new file with mode: 0644]
resources/src/mediawiki/page/gallery.css

index d0bb57f..6976655 100644 (file)
@@ -19,9 +19,13 @@ production.
 
 === New features in 1.28 ===
 * User::isBot() method for checking if an account is a bot role account.
+* Added a new 'slider' mode for galleries.
 * Added a new hook, 'UserIsBot', to aid in determining if a user is a bot.
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
   interact with API parsing.
+* Added a new hook, 'UploadVerifyUpload', which can be used to reject a file
+  upload. Unlike 'UploadVerifyFile' it provides information about upload comment
+  and the file description page, but does not run for uploads to stash.
 
 === External library changes in 1.28 ===
 
index 16d69d0..8e214ce 100644 (file)
@@ -1251,6 +1251,7 @@ $wgAutoloadLocalClasses = [
        'SkinFallback' => __DIR__ . '/includes/skins/SkinFallback.php',
        'SkinFallbackTemplate' => __DIR__ . '/includes/skins/SkinFallbackTemplate.php',
        'SkinTemplate' => __DIR__ . '/includes/skins/SkinTemplate.php',
+       'SliderImageGallery' => __DIR__ . '/includes/gallery/SliderImageGallery.php',
        'SpecialActiveUsers' => __DIR__ . '/includes/specials/SpecialActiveusers.php',
        'SpecialAllMessages' => __DIR__ . '/includes/specials/SpecialAllMessages.php',
        'SpecialAllMyUploads' => __DIR__ . '/includes/specials/SpecialMyRedirectPages.php',
index af4dddb..d65fb78 100644 (file)
@@ -1140,85 +1140,6 @@ $page: SpecialPage object for DeletedContributions
 $row: the DB row for this line
 &$classes: the classes to add to the surrounding <li>
 
-'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as patrolled" link
-which is shown both on the diff header as well as on the bottom of a page, usually
-wrapped in a span element which has class="patrollink".
-$differenceEngine: DifferenceEngine object
-&$markAsPatrolledLink: The "mark as patrolled" link HTML (string)
-$rcid: Recent change ID (rc_id) for this change (int)
-$token: Patrol token; $rcid is used in generating this variable
-
-'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the rcid parameter.
-For example the rcid might be set to zero due to the user being the same as the
-performer of the change but an extension might still want to show it under certain
-conditions.
-&$rcid: rc_id (int) of the change or 0
-$differenceEngine: DifferenceEngine object
-$change: RecentChange object
-$user: User object representing the current user
-
-'DifferenceEngineNewHeader': Allows extensions to change the $newHeader variable, which
-contains information about the new revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
-$differenceEngine: DifferenceEngine object
-&$newHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$formattedRevisionTools: Array containing revision tools, some of which may have
-been injected with the DiffRevisionTools hook
-$nextlink: String containing the link to the next revision (if any); also included in $newHeader
-$rollback: Rollback link (string) to roll this revision back to the previous one, if any
-$newminor: String indicating if the new revision was marked as a minor edit
-$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $nextlink)
-$rdel: RevisionDelete link for the new revision, if the current user is allowed
-to use the RevisionDelete feature
-$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-
-'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader variable, which
-contains information about the old revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
-$differenceEngine: DifferenceEngine object
-&$oldHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$prevlink: String containing the link to the previous revision (if any); also included in $oldHeader
-$oldminor: String indicating if the old revision was marked as a minor edit
-$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $prevlink)
-$ldel: RevisionDelete link for the old revision, if the current user is allowed
-to use the RevisionDelete feature
-$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-
-'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases when
-there is no old revision
-&$oldHeader: empty string by default
-
-'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the parser output.
-Return false to not add parser output via OutputPage's addParserOutput method.
-$differenceEngine: DifferenceEngine object
-$out: OutputPage object
-$parserOutput: ParserOutput object
-$wikiPage: WikiPage object
-
-'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into this hook
-point and return false to not show the final "mark as patrolled" link on the bottom
-of a page.
-This hook has no arguments.
-
-'DifferenceEngineShowDiff': Allows extensions to affect the diff text which
-eventually gets sent to the OutputPage object.
-$differenceEngine: DifferenceEngine object
-
-'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff table
-body (without header) in cases when there is no old revision or the old and new
-revisions are identical.
-$differenceEngine: DifferenceEngine object
-
-'DifferenceEngineShowDiffPage': Add additional output via the available OutputPage
-object into the diff view
-$out: OutputPage object
-
 'DiffRevisionTools': Override or extend the revision tools available from the
 diff view, i.e. undo, etc.
 $newRev: Revision object of the "new" revision
@@ -3385,6 +3306,19 @@ $mime: (string) The uploaded file's MIME type, as detected by MediaWiki.
   representing the problem with the file, where the first element is the message
   key and the remaining elements are used as parameters to the message.
 
+'UploadVerifyUpload': Upload verification, based on both file properties like
+MIME type (same as UploadVerifyFile) and the information entered by the user
+(upload comment, file page contents etc.).
+$upload: (object) An instance of UploadBase, with all info about the upload
+$user: (object) An instance of User, the user uploading this file
+$props: (array) File properties, as returned by FSFile::getPropsFromPath()
+$comment: (string) Upload log comment (also used as edit summary)
+$pageText: (string) File description page text (only used for new uploads)
+&$error: output: If the file upload should be prevented, set this to the reason
+  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
+  instance (you might want to use ApiMessage to provide machine-readable details
+  for the API).
+
 'UserIsBot': when determining whether a user is a bot account
 $user: the user
 &$isBot: whether this is user a bot or not (boolean)
index 6d08eec..f2e2420 100644 (file)
@@ -8059,10 +8059,9 @@ $wgUpdateRowsPerQuery = 100;
 
 /**
  * Name of the external diff engine to use. Supported values:
- * * false: default PHP implementation
- * * 'wikidiff2': Wikimedia's fast difference engine implemented as a PHP/HHVM module
- * * 'wikidiff' and 'wikidiff3' are treated as false for backwards compatibility
- * * any other string is treated as a path to external diff executable
+ * * string: path to an external diff executable
+ * * false: wikidiff2 PHP/HHVM module if installed, otherwise the default PHP implementation
+ * * 'wikidiff', 'wikidiff2', and 'wikidiff3' are treated as false for backwards compatibility
  */
 $wgExternalDiffEngine = false;
 
index 5877932..cb1bd71 100644 (file)
@@ -690,7 +690,9 @@ wfDebugLog( 'caches',
        ', WAN: ' . $wgMainWANCache .
        ', stash: ' . $wgMainStash .
        ', message: ' . get_class( $messageMemc ) .
-       ', parser: ' . get_class( $parserMemc ) );
+       ', parser: ' . get_class( $parserMemc ) .
+       ', session: ' . get_class( ObjectCache::getInstance( $wgSessionCacheType ) )
+);
 
 Profiler::instance()->scopedProfileOut( $ps_memcached );
 
index 84bf16e..f06f828 100644 (file)
@@ -88,7 +88,7 @@ abstract class Action {
         * @since 1.17
         * @param string $action
         * @param Page $page
-        * @param IContextSource $context
+        * @param IContextSource|null $context
         * @return Action|bool|null False if the action is disabled, null
         *     if it is not recognised
         */
@@ -264,7 +264,7 @@ abstract class Action {
         * Only public since 1.21
         *
         * @param Page $page
-        * @param IContextSource $context
+        * @param IContextSource|null $context
         */
        public function __construct( Page $page, IContextSource $context = null ) {
                if ( $context === null ) {
index 700e201..0df372e 100644 (file)
@@ -116,8 +116,10 @@ class HistoryAction extends FormlessAction {
                // Setup page variables.
                $out->setFeedAppendQuery( 'action=history' );
                $out->addModules( 'mediawiki.action.history' );
-               $out->addModuleStyles( 'mediawiki.action.history.styles' );
-               $out->addModuleStyles( 'mediawiki.special.changeslist' );
+               $out->addModuleStyles( [
+                       'mediawiki.action.history.styles',
+                       'mediawiki.special.changeslist',
+               ] );
                if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) {
                        $out = $this->getOutput();
                        $out->addModuleStyles( [
index 0f0fbdc..a3bb6a2 100644 (file)
@@ -100,8 +100,10 @@ class ApiHelp extends ApiBase {
                }
 
                $out = $context->getOutput();
-               $out->addModuleStyles( 'mediawiki.hlist' );
-               $out->addModuleStyles( 'mediawiki.apihelp' );
+               $out->addModuleStyles( [
+                       'mediawiki.hlist',
+                       'mediawiki.apihelp',
+               ] );
                if ( !empty( $options['toc'] ) ) {
                        $out->addModules( 'mediawiki.toc' );
                }
index 15c1e39..cb8d938 100644 (file)
@@ -350,9 +350,10 @@ class ApiUpload extends ApiBase {
         * @param array|string|MessageSpecifier $error Error suitable for passing to dieUsageMsg()
         * @param string $parameter Parameter that needs revising
         * @param array $data Optional extra data to pass to the user
+        * @param string $code Error code to use if the error is unknown
         * @throws UsageException
         */
-       private function dieRecoverableError( $error, $parameter, $data = [] ) {
+       private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
                try {
                        $data['filekey'] = $this->performStash();
                        $data['sessionkey'] = $data['filekey'];
@@ -365,6 +366,9 @@ class ApiUpload extends ApiBase {
                if ( isset( $parsed['data'] ) ) {
                        $data = array_merge( $data, $parsed['data'] );
                }
+               if ( $parsed['code'] === 'unknownerror' ) {
+                       $parsed['code'] = $code;
+               }
 
                $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
        }
@@ -754,9 +758,14 @@ class ApiUpload extends ApiBase {
                                $this->mParams['text'], $watch, $this->getUser(), $this->mParams['tags'] );
 
                        if ( !$status->isGood() ) {
-                               $error = $status->getErrorsArray();
-                               ApiResult::setIndexedTagName( $error, 'error' );
-                               $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
+                               // Is there really no better way to do this?
+                               $errors = $status->getErrorsByType( 'error' );
+                               $msg = array_merge( [ $errors[0]['message'] ], $errors[0]['params'] );
+                               $data = $status->getErrorsArray();
+                               ApiResult::setIndexedTagName( $data, 'error' );
+                               // For backwards-compatibility, we use the 'internal-error' fallback key and merge $data
+                               // into the root of the response (rather than something sane like [ 'details' => $data ]).
+                               $this->dieRecoverableError( $msg, null, $data, 'internal-error' );
                        }
                        $result['result'] = 'Success';
                }
index deeb405..6bb8874 100644 (file)
@@ -236,14 +236,12 @@ class DifferenceEngine extends ContextSource {
        }
 
        public function showDiffPage( $diffOnly = false ) {
+
                # Allow frames except in certain special cases
                $out = $this->getOutput();
                $out->allowClickjacking();
                $out->setRobotPolicy( 'noindex,nofollow' );
 
-               // Allow extensions to add any extra output here
-               Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
-
                if ( !$this->loadRevisionData() ) {
                        $this->showMissingRevision();
 
@@ -285,8 +283,6 @@ class DifferenceEngine extends ContextSource {
                        $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
                        $samePage = true;
                        $oldHeader = '';
-                       // Allow extensions to change the $oldHeader variable
-                       Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
                } else {
                        Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
 
@@ -356,10 +352,6 @@ class DifferenceEngine extends ContextSource {
                                '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
                                '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
 
-                       // Allow extensions to change the $oldHeader variable
-                       Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
-                               $diffOnly, $ldel, $this->unhide ] );
-
                        if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
                                $deleted = true; // old revisions text is hidden
                                if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
@@ -421,10 +413,6 @@ class DifferenceEngine extends ContextSource {
                        '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
                        '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
 
-               // Allow extensions to change the $newHeader variable
-               Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
-                       $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
-
                if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
                        $deleted = true; // new revisions text is hidden
                        if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
@@ -497,9 +485,6 @@ class DifferenceEngine extends ContextSource {
                                                        'token' => $linkInfo['token'],
                                                ]
                                        ) . ']</span>';
-                               // Allow extensions to change the markpatrolled link
-                               Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
-                                       &$this->mMarkPatrolledLink, $linkInfo['rcid'], $linkInfo['token'] ] );
                        }
                }
                return $this->mMarkPatrolledLink;
@@ -543,13 +528,6 @@ class DifferenceEngine extends ContextSource {
                                // If the user could patrol this it already would be patrolled
                                $rcid = 0;
                        }
-
-                       // Allow extensions to possibly change the rcid here
-                       // For example the rcid might be set to zero due to the user
-                       // being the same as the performer of the change but an extension
-                       // might still want to show it under certain conditions
-                       Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
-
                        // Build the link
                        if ( $rcid ) {
                                $this->getOutput()->preventClickjacking();
@@ -637,20 +615,15 @@ class DifferenceEngine extends ContextSource {
 
                                # WikiPage::getParserOutput() should not return false, but just in case
                                if ( $parserOutput ) {
-                                       // Allow extensions to change parser output here
-                                       if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput', [ $this, $out, $parserOutput, $wikiPage ] ) ) {
-                                               $out->addParserOutput( $parserOutput );
-                                       }
+                                       $out->addParserOutput( $parserOutput );
                                }
                        }
                }
                # @codingStandardsIgnoreEnd
 
-               // Allow extensions to optionally not show the final patrolled link
-               if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
-                       # Add redundant patrol link on bottom...
-                       $out->addHTML( $this->markPatrolledLink() );
-               }
+               # Add redundant patrol link on bottom...
+               $out->addHTML( $this->markPatrolledLink() );
+
        }
 
        protected function getParserOutput( WikiPage $page, Revision $rev ) {
@@ -676,9 +649,6 @@ class DifferenceEngine extends ContextSource {
         * @return bool
         */
        public function showDiff( $otitle, $ntitle, $notice = '' ) {
-               // Allow extensions to affect the output here
-               Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
-
                $diff = $this->getDiff( $otitle, $ntitle, $notice );
                if ( $diff === false ) {
                        $this->showMissingRevision();
@@ -748,9 +718,7 @@ class DifferenceEngine extends ContextSource {
                if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
                        && $this->mOldRev->getId() == $this->mNewRev->getId() )
                ) {
-                       if ( !Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
-                               return '';
-                       }
+                       return '';
                }
                // Cacheable?
                $key = false;
@@ -923,18 +891,23 @@ class DifferenceEngine extends ContextSource {
                if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
                        wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
                        $wgExternalDiffEngine = false;
+               } elseif ( $wgExternalDiffEngine == 'wikidiff2' ) {
+                       // Same as above, but with no deprecation warnings
+                       $wgExternalDiffEngine = false;
+               } elseif ( !is_string( $wgExternalDiffEngine ) ) {
+                       // And prevent people from shooting themselves in the foot...
+                       wfWarn( '$wgExternalDiffEngine is set to a non-string value, forcing it to false' );
+                       $wgExternalDiffEngine = false;
                }
 
-               if ( $wgExternalDiffEngine == 'wikidiff2' ) {
-                       if ( function_exists( 'wikidiff2_do_diff' ) ) {
-                               # Better external diff engine, the 2 may some day be dropped
-                               # This one does the escaping and segmenting itself
-                               $text = wikidiff2_do_diff( $otext, $ntext, 2 );
-                               $text .= $this->debug( 'wikidiff2' );
+               if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+                       # Better external diff engine, the 2 may some day be dropped
+                       # This one does the escaping and segmenting itself
+                       $text = wikidiff2_do_diff( $otext, $ntext, 2 );
+                       $text .= $this->debug( 'wikidiff2' );
 
-                               return $text;
-                       }
-               } elseif ( $wgExternalDiffEngine !== false ) {
+                       return $text;
+               } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
                        # Diff via the shell
                        $tmpDir = wfTempDir();
                        $tempName1 = tempnam( $tmpDir, 'diff_' );
index c8a25f0..73f4b19 100644 (file)
@@ -113,6 +113,7 @@ abstract class ImageGalleryBase extends ContextSource {
                                'packed' => 'PackedImageGallery',
                                'packed-hover' => 'PackedHoverImageGallery',
                                'packed-overlay' => 'PackedOverlayImageGallery',
+                               'slider' => 'SliderImageGallery',
                        ];
                        // Allow extensions to make a new gallery format.
                        Hooks::run( 'GalleryGetModes', [ &self::$modeMapping ] );
diff --git a/includes/gallery/SliderImageGallery.php b/includes/gallery/SliderImageGallery.php
new file mode 100644 (file)
index 0000000..67be9ce
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Slider gallery shows one image at a time with controls to move around.
+ *
+ * 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
+ */
+
+class SliderImageGallery extends TraditionalImageGallery {
+       function __construct( $mode = 'traditional', IContextSource $context = null ) {
+               parent::__construct( $mode, $context );
+               // Does not support per row option.
+               $this->mPerRow = 0;
+       }
+
+       /**
+        * Add javascript adds interface elements
+        * @return array
+        */
+       protected function getModules() {
+               return [ 'mediawiki.page.gallery.slider' ];
+       }
+}
index 106911c..1396685 100644 (file)
@@ -222,11 +222,12 @@ class ImagePage extends Article {
                                $out->addStyle( $css );
                        }
                }
-               // always show the local local Filepage.css, bug 29277
-               $out->addModuleStyles( 'filepage' );
 
-               // Add MediaWiki styles for a file page
-               $out->addModuleStyles( 'mediawiki.action.view.filepage' );
+               $out->addModuleStyles( [
+                       'filepage', // always show the local local Filepage.css, bug 29277
+                       'mediawiki.action.view.filepage', // Add MediaWiki styles for a file page
+               ] );
+
        }
 
        /**
index 6c061b3..60f1dd8 100644 (file)
@@ -475,9 +475,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
        protected function addModules() {
                $out = $this->getOutput();
                // Styles and behavior for the legend box (see makeLegend())
-               $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
+               $out->addModuleStyles( [
+                       'mediawiki.special.changeslist.legend',
+                       'mediawiki.special.changeslist',
+               ] );
                $out->addModules( 'mediawiki.special.changeslist.legend.js' );
-               $out->addModuleStyles( 'mediawiki.special.changeslist' );
        }
 
        protected function getGroupName() {
index daf602b..cce88b9 100644 (file)
@@ -37,8 +37,10 @@ class SpecialContributions extends IncludableSpecialPage {
                $this->setHeaders();
                $this->outputHeader();
                $out = $this->getOutput();
-               $out->addModuleStyles( 'mediawiki.special' );
-               $out->addModuleStyles( 'mediawiki.special.changeslist' );
+               $out->addModuleStyles( [
+                       'mediawiki.special',
+                       'mediawiki.special.changeslist',
+               ] );
                $this->addHelpLink( 'Help:User contributions' );
 
                $this->opts = [];
index 4b731cb..0ef6af1 100644 (file)
@@ -535,7 +535,7 @@ class SpecialUpload extends SpecialPage {
                );
 
                if ( !$status->isGood() ) {
-                       $this->showUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
+                       $this->showRecoverableUploadError( $this->getOutput()->parse( $status->getWikiText() ) );
 
                        return;
                }
index 08fbeea..6dcc948 100644 (file)
@@ -717,13 +717,23 @@ abstract class UploadBase {
         */
        public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) {
                $this->getLocalFile()->load( File::READ_LATEST );
+               $props = $this->mFileProps;
+
+               $error = null;
+               Hooks::run( 'UploadVerifyUpload', [ $this, $user, $props, $comment, $pageText, &$error ] );
+               if ( $error ) {
+                       if ( !is_array( $error ) ) {
+                               $error = [ $error ];
+                       }
+                       return call_user_func_array( 'Status::newFatal', $error );
+               }
 
                $status = $this->getLocalFile()->upload(
                        $this->mTempPath,
                        $comment,
                        $pageText,
                        File::DELETE_SOURCE,
-                       $this->mFileProps,
+                       $props,
                        false,
                        $user,
                        $tags
index 4899143..9104fd7 100644 (file)
@@ -26,7 +26,7 @@ imdb|http://www.imdb.com/find?q=$1&tt=on|0|
 jargonfile|http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=$1|0|
 kmwiki|http://kmwiki.wikispaces.com/$1|0|
 linuxwiki|http://linuxwiki.de/$1|0|
-lojban|http://www.lojban.org/tiki/tiki-index.php?page=$1|0|
+lojban|http://mw.lojban.org/papri/$1|0|
 lqwiki|http://wiki.linuxquestions.org/wiki/$1|0|
 lugkr|http://www.lug-kr.de/wiki/$1|0|
 meatball|http://www.usemod.com/cgi-bin/mb.pl?$1|0|
index d9e2c50..631d2a7 100644 (file)
@@ -64,7 +64,8 @@
                                        "mw.Feedback*",
                                        "mw.Upload*",
                                        "mw.ForeignUpload",
-                                       "mw.ForeignStructuredUpload*"
+                                       "mw.ForeignStructuredUpload*",
+                                       "mw.GallerySlider"
                                ]
                        },
                        {
index 8526ec6..1805c84 100644 (file)
@@ -1656,6 +1656,18 @@ return [
                'position' => 'top',
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.page.gallery.slider' => [
+               'scripts' => 'resources/src/mediawiki/page/gallery-slider.js',
+               'position' => 'top',
+               'dependencies' => [
+                       'mediawiki.api',
+                       'mediawiki.Title',
+                       'oojs',
+                       'oojs-ui-core',
+                       'oojs-ui-widgets',
+                       'oojs-ui.styles.icons-media'
+               ]
+       ],
        'mediawiki.page.ready' => [
                'scripts' => 'resources/src/mediawiki/page/ready.js',
                'dependencies' => [
index 3e010d0..a8ee4c7 100644 (file)
                 */
                postWithToken: function ( tokenType, params, ajaxOptions ) {
                        var api = this,
-                               abortable;
+                               abortedPromise = $.Deferred().reject( 'http',
+                                       { textStatus: 'abort', exception: 'abort' } ).promise(),
+                               abortable,
+                               aborted;
 
-                       return ( abortable = api.getToken( tokenType, params.assert ) ).then( function ( token ) {
+                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
                                params.token = token;
+                               // Request was aborted while token request was running, but we
+                               // don't want to unnecessarily abort token requests, so abort
+                               // a fake request instead
+                               if ( aborted ) {
+                                       return abortedPromise;
+                               }
+
                                return ( abortable = api.post( params, ajaxOptions ) ).then(
                                        // If no error, return to caller as-is
                                        null,
                                                        api.badToken( tokenType );
                                                        // Try again, once
                                                        params.token = undefined;
-                                                       return ( abortable = api.getToken( tokenType, params.assert ) ).then( function ( token ) {
+                                                       abortable = null;
+                                                       return api.getToken( tokenType, params.assert ).then( function ( token ) {
                                                                params.token = token;
-                                                               return ( abortable = api.post( params, ajaxOptions ) ).promise();
+                                                               if ( aborted ) {
+                                                                       return abortedPromise;
+                                                               }
+
+                                                               return ( abortable = api.post( params, ajaxOptions ) );
                                                        } );
                                                }
 
                                        }
                                );
                        } ).promise( { abort: function () {
-                               abortable.abort();
+                               if ( abortable ) {
+                                       abortable.abort();
+                               } else {
+                                       aborted = true;
+                               }
                        } } );
                },
 
diff --git a/resources/src/mediawiki/page/gallery-slider.js b/resources/src/mediawiki/page/gallery-slider.js
new file mode 100644 (file)
index 0000000..82b22e8
--- /dev/null
@@ -0,0 +1,453 @@
+/*!
+ * mw.GallerySlider: Interface controls for the slider gallery
+ */
+( function ( mw, $, OO ) {
+       /**
+        * mw.GallerySlider encapsulates the user interface of the slider
+        * galleries. An object is instantiated for each `.mw-gallery-slider`
+        * element.
+        *
+        * @class mw.GallerySlider
+        * @uses mw.Title
+        * @uses mw.Api
+        * @param {jQuery} gallery The `<ul>` element of the gallery.
+        */
+       mw.GallerySlider = function ( gallery ) {
+               // Properties
+               this.$gallery = $( gallery );
+               this.$galleryCaption = this.$gallery.find( '.gallerycaption' );
+               this.$galleryBox = this.$gallery.find( '.gallerybox' );
+               this.$currentImage = null;
+               this.imageInfoCache = {};
+               if ( this.$gallery.parent().attr( 'id' ) !== 'mw-content-text' ) {
+                       this.$container = this.$gallery.parent();
+               }
+
+               // Initialize
+               this.drawCarousel();
+               this.setSizeRequirement();
+               this.toggleThumbnails( false );
+               this.showCurrentImage();
+
+               // Events
+               $( window ).on(
+                       'resize',
+                       OO.ui.debounce(
+                               this.setSizeRequirement.bind( this ),
+                               100
+                       )
+               );
+
+               // Disable thumbnails' link, instead show the image in the carousel
+               this.$galleryBox.on( 'click', function ( e ) {
+                       this.$currentImage = $( e.currentTarget );
+                       this.showCurrentImage();
+                       return false;
+               }.bind( this ) );
+       };
+
+       /* Properties */
+       /**
+        * @property {jQuery} $gallery The `<ul>` element of the gallery.
+        */
+
+       /**
+        * @property {jQuery} $galleryCaption The `<li>` that has the gallery caption.
+        */
+
+       /**
+        * @property {jQuery} $galleryBox Selection of `<li>` elements that have thumbnails.
+        */
+
+       /**
+        * @property {jQuery} $carousel The `<li>` elements that contains the carousel.
+        */
+
+       /**
+        * @property {jQuery} $interface The `<div>` elements that contains the interface buttons.
+        */
+
+       /**
+        * @property {jQuery} $img The `<img>` element that'll display the current image.
+        */
+
+       /**
+        * @property {jQuery} $imgLink The `<a>` element that links to the image's File page.
+        */
+
+       /**
+        * @property {jQuery} $imgCaption The `<p>` element that holds the image caption.
+        */
+
+       /**
+        * @property {jQuery} $imgContainer The `<div>` element that contains the image.
+        */
+
+       /**
+        * @property {jQuery} $currentImage The `<li>` element of the current image.
+        */
+
+       /**
+        * @property {jQuery} $container If the gallery contained in an element that is
+        *      not the main content element, then it stores that element.
+        */
+
+       /**
+        * @property {Object} imageInfoCache A key value pair of thumbnail URLs and image info.
+        */
+
+       /**
+        * @property {number} imageWidth Width of the image based on viewport size
+        */
+
+       /**
+        * @property {number} imageHeight Height of the image based on viewport size
+        *      the URLs in the required size.
+        */
+
+       /* Setup */
+       OO.initClass( mw.GallerySlider );
+
+       /* Methods */
+       /**
+        * Draws the carousel and the interface around it.
+        */
+       mw.GallerySlider.prototype.drawCarousel = function () {
+               var next, prev, toggle, interfaceElements, carouselStack;
+
+               this.$carousel = $( '<li>' ).addClass( 'gallerycarousel' );
+
+               // Buttons for the interface
+               prev = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'previous'
+               } ).on( 'click', this.prevImage.bind( this ) );
+
+               next = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'next'
+               } ).on( 'click', this.nextImage.bind( this ) );
+
+               toggle = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       icon: 'imageGallery'
+               } ).on( 'click', this.toggleThumbnails.bind( this ) );
+
+               interfaceElements = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       classes: [ 'mw-gallery-slider-buttons' ],
+                       $content: $( '<div>' ).append(
+                               prev.$element,
+                               toggle.$element,
+                               next.$element
+                       )
+               } );
+               this.$interface = interfaceElements.$element;
+
+               // Containers for the current image, caption etc.
+               this.$img = $( '<img>' );
+               this.$imgLink = $( '<a>' ).append( this.$img );
+               this.$imgCaption = $( '<p>' ).attr( 'class', 'mw-gallery-slider-caption' );
+               this.$imgContainer = $( '<div>' )
+                       .attr( 'class', 'mw-gallery-slider-img-container' )
+                       .append( this.$imgLink );
+
+               carouselStack = new OO.ui.StackLayout( {
+                       continuous: true,
+                       expanded: false,
+                       items: [
+                               interfaceElements,
+                               new OO.ui.PanelLayout( {
+                                       expanded: false,
+                                       $content: this.$imgContainer
+                               } ),
+                               new OO.ui.PanelLayout( {
+                                       expanded: false,
+                                       $content: this.$imgCaption
+                               } )
+                       ]
+               } );
+               this.$carousel.append( carouselStack.$element );
+
+               // Append below the caption or as the first element in the gallery
+               if ( this.$galleryCaption.length !== 0 ) {
+                       this.$galleryCaption.after( this.$carousel );
+               } else {
+                       this.$gallery.prepend( this.$carousel );
+               }
+       };
+
+       /**
+        * Sets the {@link #imageWidth} and {@link #imageHeight} properties
+        * based on the size of the window. Also flushes the
+        * {@link #imageInfoCache} as we'll now need URLs for a different
+        * size.
+        */
+       mw.GallerySlider.prototype.setSizeRequirement = function () {
+               var w, h;
+
+               if ( this.$container !== undefined ) {
+                       w = this.$container.width() * 0.9;
+                       h = ( this.$container.height() - this.getChromeHeight() ) * 0.9;
+               } else {
+                       w = this.$imgContainer.width();
+                       h = Math.min( $( window ).height() * ( 3 / 4 ), this.$imgContainer.width() ) - this.getChromeHeight();
+               }
+
+               // Only update and flush the cache if the size changed
+               if ( w !== this.imageWidth || h !== this.imageHeight ) {
+                       this.imageWidth = w;
+                       this.imageHeight = h;
+                       this.imageInfoCache = {};
+                       this.setImageSize();
+               }
+       };
+
+       /**
+        * Gets the height of the interface elements and the
+        * gallery's caption.
+        */
+       mw.GallerySlider.prototype.getChromeHeight = function () {
+               return this.$interface.outerHeight() + this.$galleryCaption.outerHeight();
+       };
+
+       /**
+        * Sets the height and width of {@link #$img} based on the
+        * proportion of the image and the values generated by
+        * {@link #setSizeRequirement}.
+        *
+        * @return {boolean} Whether or not the image was sized.
+        */
+       mw.GallerySlider.prototype.setImageSize = function () {
+               if ( this.$img === undefined || this.$thumbnail === undefined ) {
+                       return false;
+               }
+
+               // Reset height and width
+               this.$img
+                       .removeAttr( 'width' )
+                       .removeAttr( 'height' );
+
+               // Stretch image to take up the required size
+               if ( this.$thumbnail.width() > this.$thumbnail.height() ) {
+                       this.$img.attr( 'width', this.imageWidth + 'px' );
+               } else {
+                       this.$img.attr( 'height', this.imageHeight + 'px' );
+               }
+
+               // Make the image smaller in case the current image
+               // size is larger than the original file size.
+               this.getImageInfo( this.$thumbnail ).done( function ( info ) {
+                       // NOTE: There will be a jump when resizing the window
+                       // because the cache is cleared and this a new network request.
+                       if (
+                               info.thumbwidth < this.$img.width() ||
+                               info.thumbheight < this.$img.height()
+                       ) {
+                               this.$img.attr( 'width', info.thumbwidth + 'px' );
+                               this.$img.attr( 'height', info.thumbheight + 'px' );
+                       }
+               }.bind( this ) );
+
+               return true;
+       };
+
+       /**
+        * Displays the image set as {@link #$currentImage} in the carousel.
+        */
+       mw.GallerySlider.prototype.showCurrentImage = function () {
+               var imageLi = this.getCurrentImage(),
+                       caption = imageLi.find( '.gallerytext' );
+
+               // Highlight current thumbnail
+               this.$gallery
+                       .find( '.gallerybox.slider-current' )
+                       .removeClass( 'slider-current' );
+               imageLi.addClass( 'slider-current' );
+
+               // Show thumbnail stretched to the right size while the image loads
+               this.$thumbnail = imageLi.find( 'img' );
+               this.$img.attr( 'src', this.$thumbnail.attr( 'src' ) );
+               this.$imgLink.attr( 'href', imageLi.find( 'a' ).eq( 0 ).attr( 'href' ) );
+               this.setImageSize();
+
+               // Copy caption
+               this.$imgCaption
+                       .empty()
+                       .append( caption.clone() );
+
+               // Load image at the required size
+               this.loadImage( this.$thumbnail ).done( function ( info, $img ) {
+                       // Show this image to the user only if its still the current one
+                       if ( this.$thumbnail.attr( 'src' ) === $img.attr( 'src' ) ) {
+                               this.$img.attr( 'src', info.thumburl );
+                               this.setImageSize();
+
+                               // Keep the next image ready
+                               this.loadImage( this.getNextImage().find( 'img' ) );
+                       }
+               }.bind( this ) );
+       };
+
+       /**
+        * Loads the full image given the `<img>` element of the thumbnail.
+        *
+        * @param {Object} $img
+        * @return {jQuery.Promise} Resolves with the images URL and original
+        *      element once the image has loaded.
+        */
+       mw.GallerySlider.prototype.loadImage = function ( $img ) {
+               var img, d = $.Deferred();
+
+               this.getImageInfo( $img ).done( function ( info ) {
+                       img = new Image();
+                       img.src = info.thumburl;
+                       img.onload = function () {
+                               d.resolve( info, $img );
+                       };
+                       img.onerror = function () {
+                               d.reject();
+                       };
+               } ).fail( function () {
+                       d.reject();
+               } );
+
+               return d.promise();
+       };
+
+       /**
+        * Gets the image's info given an `<img>` element.
+        *
+        * @param {Object} $img
+        * @return {jQuery.Promise} Resolves with the image's info.
+        */
+       mw.GallerySlider.prototype.getImageInfo = function ( $img ) {
+               var api, title, params,
+                       imageSrc = $img.attr( 'src' );
+
+               if ( this.imageInfoCache[ imageSrc ] === undefined ) {
+                       api = new mw.Api();
+                       // TODO: This supports only gallery of images
+                       title = new mw.Title.newFromImg( $img );
+                       params = {
+                               action: 'query',
+                               formatversion: 2,
+                               titles: title.toString(),
+                               prop: 'imageinfo',
+                               iiprop: 'url'
+                       };
+
+                       // Check which dimension we need to request, based on
+                       // image and container proportions.
+                       if ( this.getDimensionToRequest( $img ) === 'height' ) {
+                               params.iiurlheight = this.imageHeight;
+                       } else {
+                               params.iiurlwidth = this.imageWidth;
+                       }
+
+                       this.imageInfoCache[ imageSrc ] = api.get( params ).then( function ( data ) {
+                               if ( OO.getProp( data, 'query', 'pages', 0, 'imageinfo', 0, 'thumburl' ) !== undefined ) {
+                                       return data.query.pages[ 0 ].imageinfo[ 0 ];
+                               } else {
+                                       return $.Deferred().reject();
+                               }
+                       } );
+               }
+
+               return this.imageInfoCache[ imageSrc ];
+       };
+
+       /**
+        * Given an image, the method checks whether to use the height
+        * or the width to request the larger image.
+        *
+        * @param {jQuery} $img
+        * @return {string}
+        */
+       mw.GallerySlider.prototype.getDimensionToRequest = function ( $img ) {
+               var ratio = $img.width() / $img.height();
+
+               if ( this.imageHeight * ratio <= this.imageWidth ) {
+                       return 'height';
+               } else {
+                       return 'width';
+               }
+       };
+
+       /**
+        * Toggles visibility of the thumbnails.
+        *
+        * @param {boolean} show Optional argument to control the state
+        */
+       mw.GallerySlider.prototype.toggleThumbnails = function ( show ) {
+               this.$galleryBox.toggle( show );
+               this.$carousel.toggleClass( 'mw-gallery-slider-thumbnails-toggled', show );
+       };
+
+       /**
+        * Getter method for {@link #$currentImage}
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlider.prototype.getCurrentImage = function () {
+               this.$currentImage = this.$currentImage || this.$galleryBox.eq( 0 );
+               return this.$currentImage;
+       };
+
+       /**
+        * Gets the image after the current one. Returns the first image if
+        * the current one is the last.
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlider.prototype.getNextImage = function () {
+               // Not the last image in the gallery
+               if ( this.$currentImage.next( '.gallerybox' )[ 0 ] !== undefined ) {
+                       return this.$currentImage.next( '.gallerybox' );
+               } else {
+                       return this.$galleryBox.eq( 0 );
+               }
+       };
+
+       /**
+        * Gets the image before the current one. Returns the last image if
+        * the current one is the first.
+        *
+        * @return {jQuery}
+        */
+       mw.GallerySlider.prototype.getPrevImage = function () {
+               // Not the first image in the gallery
+               if ( this.$currentImage.prev( '.gallerybox' )[ 0 ] !== undefined ) {
+                       return this.$currentImage.prev( '.gallerybox' );
+               } else {
+                       return this.$galleryBox.last();
+               }
+       };
+
+       /**
+        * Sets the {@link #$currentImage} to the next one and shows
+        * it in the carousel
+        */
+       mw.GallerySlider.prototype.nextImage = function () {
+               this.$currentImage = this.getNextImage();
+               this.showCurrentImage();
+       };
+
+       /**
+        * Sets the {@link #$currentImage} to the previous one and shows
+        * it in the carousel
+        */
+       mw.GallerySlider.prototype.prevImage = function () {
+               this.$currentImage = this.getPrevImage();
+               this.showCurrentImage();
+       };
+
+       // Bootstrap all slider galleries
+       $( function () {
+               $( '.mw-gallery-slider' ).each( function () {
+                       /*jshint -W031 */
+                       new mw.GallerySlider( this );
+                       /*jshint +W031 */
+               } );
+       } );
+}( mediaWiki, jQuery, OO ) );
index 3ed4870..7bf0f81 100644 (file)
@@ -124,3 +124,49 @@ ul.mw-gallery-packed-overlay,
 ul.mw-gallery-packed {
        text-align: center;
 }
+
+/* Slider */
+ul.gallery.mw-gallery-slider {
+       display: block;
+       margin: 4em 0;
+}
+
+ul.gallery.mw-gallery-slider .gallerycaption {
+       font-size: 1.3em;
+       margin: 0;
+}
+
+ul.gallery.mw-gallery-slider .gallerycarousel.mw-gallery-slider-thumbnails-toggled {
+       margin-bottom: 1.3em;
+}
+
+ul.gallery.mw-gallery-slider .mw-gallery-slider-buttons {
+       opacity: 0.5;
+       padding: 1.3em 0;
+}
+
+ul.gallery.mw-gallery-slider .mw-gallery-slider-buttons .oo-ui-buttonElement {
+       margin: 0 2em;
+}
+
+.mw-gallery-slider li.gallerybox.slider-current {
+       background: #efefef;
+}
+
+.mw-gallery-slider .gallerybox > div {
+       max-width: 120px;
+}
+
+ul.mw-gallery-slider li.gallerybox div.thumb {
+       border: none;
+       background: transparent;
+}
+
+ul.mw-gallery-slider li.gallerycarousel {
+       display: block;
+       text-align: center;
+}
+
+.mw-gallery-slider-img-container a {
+       display: block;
+}
\ No newline at end of file