api: Update QueryFilearchive to provide information to everyone
authorVincent Privat <vincent.privat@gmail.com>
Sun, 18 Aug 2019 14:21:40 +0000 (16:21 +0200)
committerVincent Privat <vincent.privat@gmail.com>
Fri, 6 Sep 2019 20:21:58 +0000 (22:21 +0200)
Wikimedia Commons needs the ability to quickly detect, given a SHA-1, if
a file has been previously uploaded but was deleted later. This is
currently not possible in an efficient manner because the fa_sha1
field of the public database replica is not indexed, and this API
requires the 'deletedhistory' user right.

Effectively removes the 'deletedhistory' requirement, as this API does
not expose more information than the public toolforge database replica.

Bug : T60993
Change-Id: I2e9e1d50b6db9fa17acaf14d0975b6e9145a411e

RELEASE-NOTES-1.34
includes/api/ApiQueryFilearchive.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json

index 229fda4..3ba9537 100644 (file)
@@ -153,6 +153,8 @@ $wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false;
 === Action API changes in 1.34 ===
 * The 'recenteditcount' response property from action=query list=allusers,
   deprecated in 1.25, has been removed.
+* (T60993) action=query list=filearchive no longer requires the 'deletedhistory'
+  user right.
 
 === Action API internal changes in 1.34 ===
 * …
index f9087eb..c84f457 100644 (file)
@@ -38,9 +38,6 @@ class ApiQueryFilearchive extends ApiQueryBase {
        }
 
        public function execute() {
-               // Before doing anything at all, let's check permissions
-               $this->checkUserRightsAny( 'deletedhistory' );
-
                $user = $this->getUser();
                $db = $this->getDB();
                $commentStore = CommentStore::getStore();
@@ -60,6 +57,17 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $fld_bitdepth = isset( $prop['bitdepth'] );
                $fld_archivename = isset( $prop['archivename'] );
 
+               if ( $fld_description &&
+                       !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' )
+               ) {
+                       $this->dieWithError( 'apierror-cantview-deleted-description', 'permissiondenied' );
+               }
+               if ( $fld_metadata &&
+                       !$this->getPermissionManager()->userHasAnyRight( $user, 'deletedtext', 'undelete' )
+               ) {
+                       $this->dieWithError( 'apierror-cantview-deleted-metadata', 'permissiondenied' );
+               }
+
                $fileQuery = ArchivedFile::getQueryInfo();
                $this->addTables( $fileQuery['tables'] );
                $this->addFields( $fileQuery['fields'] );
@@ -110,23 +118,22 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        }
                        if ( $sha1 ) {
                                $this->addWhereFld( 'fa_sha1', $sha1 );
+                               // Paranoia: avoid brute force searches (T19342)
+                               if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedtext' ) ) {
+                                       $bitmask = File::DELETED_FILE;
+                               } elseif ( !$this->getPermissionManager()
+                                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
+                               ) {
+                                       $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED;
+                               } else {
+                                       $bitmask = 0;
+                               }
+                               if ( $bitmask ) {
+                                       $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" );
+                               }
                        }
                }
 
-               // Exclude files this user can't view.
-               if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedtext' ) ) {
-                       $bitmask = File::DELETED_FILE;
-               } elseif ( !$this->getPermissionManager()
-                       ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
-               ) {
-                       $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED;
-               } else {
-                       $bitmask = 0;
-               }
-               if ( $bitmask ) {
-                       $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" );
-               }
-
                $limit = $params['limit'];
                $this->addOption( 'LIMIT', $limit + 1 );
                $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
@@ -150,6 +157,8 @@ class ApiQueryFilearchive extends ApiQueryBase {
                                break;
                        }
 
+                       $canViewFile = RevisionRecord::userCanBitfield( $row->fa_deleted, File::DELETED_FILE, $user );
+
                        $file = [];
                        $file['id'] = (int)$row->fa_id;
                        $file['name'] = $row->fa_name;
@@ -171,13 +180,13 @@ class ApiQueryFilearchive extends ApiQueryBase {
                                $file['userid'] = (int)$row->fa_user;
                                $file['user'] = $row->fa_user_text;
                        }
-                       if ( $fld_sha1 ) {
+                       if ( $fld_sha1 && $canViewFile ) {
                                $file['sha1'] = Wikimedia\base_convert( $row->fa_sha1, 36, 16, 40 );
                        }
                        if ( $fld_timestamp ) {
                                $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp );
                        }
-                       if ( $fld_size || $fld_dimensions ) {
+                       if ( ( $fld_size || $fld_dimensions ) && $canViewFile ) {
                                $file['size'] = $row->fa_size;
 
                                $pageCount = ArchivedFile::newFromRow( $row )->pageCount();
@@ -188,18 +197,18 @@ class ApiQueryFilearchive extends ApiQueryBase {
                                $file['height'] = $row->fa_height;
                                $file['width'] = $row->fa_width;
                        }
-                       if ( $fld_mediatype ) {
+                       if ( $fld_mediatype && $canViewFile ) {
                                $file['mediatype'] = $row->fa_media_type;
                        }
-                       if ( $fld_metadata ) {
+                       if ( $fld_metadata && $canViewFile ) {
                                $file['metadata'] = $row->fa_metadata
                                        ? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result )
                                        : null;
                        }
-                       if ( $fld_bitdepth ) {
+                       if ( $fld_bitdepth && $canViewFile ) {
                                $file['bitdepth'] = $row->fa_bits;
                        }
-                       if ( $fld_mime ) {
+                       if ( $fld_mime && $canViewFile ) {
                                $file['mime'] = "$row->fa_major_mime/$row->fa_minor_mime";
                        }
                        if ( $fld_archivename && !is_null( $row->fa_archive_name ) ) {
index 8b42a07..35e45f1 100644 (file)
        "apierror-cantoverwrite-sharedfile": "The target file exists on a shared repository and you do not have permission to override it.",
        "apierror-cantsend": "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email.",
        "apierror-cantundelete": "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already.",
+       "apierror-cantview-deleted-description": "You don't have permission to view descriptions of deleted files.",
+       "apierror-cantview-deleted-metadata": "You don't have permission to view metadata of deleted files.",
        "apierror-changeauth-norequest": "Failed to create change request.",
        "apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
        "apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
index 87f056b..e796d25 100644 (file)
        "apierror-cantoverwrite-sharedfile": "{{doc-apierror}}",
        "apierror-cantsend": "{{doc-apierror}}",
        "apierror-cantundelete": "{{doc-apierror}}",
+       "apierror-cantview-deleted-description": "{{doc-apierror}}",
+       "apierror-cantview-deleted-metadata": "{{doc-apierror}}",
        "apierror-changeauth-norequest": "{{doc-apierror}}",
        "apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
        "apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",