wikimedia/mediawiki-core

View on GitHub
includes/filerepo/file/ForeignAPIFile.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
/**
 * 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
 */

use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\Authority;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityValue;

/**
 * Foreign file accessible through api.php requests.
 *
 * @ingroup FileAbstraction
 */
class ForeignAPIFile extends File {
    /** @var bool */
    private $mExists;
    /** @var array */
    private $mInfo;

    /** @inheritDoc */
    protected $repoClass = ForeignAPIRepo::class;

    /**
     * @param Title|string|false $title
     * @param ForeignApiRepo $repo
     * @param array $info
     * @param bool $exists
     */
    public function __construct( $title, $repo, $info, $exists = false ) {
        parent::__construct( $title, $repo );

        $this->mInfo = $info;
        $this->mExists = $exists;

        $this->assertRepoDefined();
    }

    /**
     * @param Title $title
     * @param ForeignApiRepo $repo
     * @return ForeignAPIFile|null
     */
    public static function newFromTitle( Title $title, $repo ) {
        $data = $repo->fetchImageQuery( [
            'titles' => 'File:' . $title->getDBkey(),
            'iiprop' => self::getProps(),
            'prop' => 'imageinfo',
            'iimetadataversion' => MediaHandler::getMetadataVersion(),
            // extmetadata is language-dependent, accessing the current language here
            // would be problematic, so we just get them all
            'iiextmetadatamultilang' => 1,
        ] );

        $info = $repo->getImageInfo( $data );

        if ( $info ) {
            $lastRedirect = count( $data['query']['redirects'] ?? [] ) - 1;
            if ( $lastRedirect >= 0 ) {
                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
                $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] );
                $img = new self( $newtitle, $repo, $info, true );
                $img->redirectedFrom( $title->getDBkey() );
            } else {
                $img = new self( $title, $repo, $info, true );
            }

            return $img;
        } else {
            return null;
        }
    }

    /**
     * Get the property string for iiprop and aiprop
     * @return string
     */
    public static function getProps() {
        return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype|extmetadata';
    }

    /**
     * @return ForeignAPIRepo|false
     */
    public function getRepo() {
        return $this->repo;
    }

    // Dummy functions...

    /**
     * @return bool
     */
    public function exists() {
        return $this->mExists;
    }

    /**
     * @return bool
     */
    public function getPath() {
        return false;
    }

    /**
     * @param array $params
     * @param int $flags
     * @return MediaTransformOutput|false
     */
    public function transform( $params, $flags = 0 ) {
        if ( !$this->canRender() ) {
            // show icon
            return parent::transform( $params, $flags );
        }

        // Note, the this->canRender() check above implies
        // that we have a handler, and it can do makeParamString.
        $otherParams = $this->handler->makeParamString( $params );
        $width = $params['width'] ?? -1;
        $height = $params['height'] ?? -1;
        $thumbUrl = false;

        if ( $width > 0 || $height > 0 ) {
            // Only query the remote if there are dimensions
            $thumbUrl = $this->repo->getThumbUrlFromCache(
                $this->getName(),
                $width,
                $height,
                $otherParams
            );
        } elseif ( $this->getMediaType() === MEDIATYPE_AUDIO ) {
            // This has no dimensions, but we still need to pass a value to getTransform()
            $thumbUrl = '/';
        }
        if ( $thumbUrl === false ) {
            global $wgLang;

            return $this->repo->getThumbError(
                $this->getName(),
                $width,
                $height,
                $otherParams,
                $wgLang->getCode()
            );
        }

        return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
    }

    // Info we can get from API...

    /**
     * @param int $page
     * @return int
     */
    public function getWidth( $page = 1 ) {
        return (int)( $this->mInfo['width'] ?? 0 );
    }

    /**
     * @param int $page
     * @return int
     */
    public function getHeight( $page = 1 ) {
        return (int)( $this->mInfo['height'] ?? 0 );
    }

    /**
     * @return string|false
     */
    public function getMetadata() {
        if ( isset( $this->mInfo['metadata'] ) ) {
            return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
        }

        return false;
    }

    /**
     * @return array
     */
    public function getMetadataArray(): array {
        if ( isset( $this->mInfo['metadata'] ) ) {
            return self::parseMetadata( $this->mInfo['metadata'] );
        }

        return [];
    }

    /**
     * @return array|null Extended metadata (see imageinfo API for format) or
     *   null on error
     */
    public function getExtendedMetadata() {
        return $this->mInfo['extmetadata'] ?? null;
    }

    /**
     * @param mixed $metadata
     * @return array
     */
    public static function parseMetadata( $metadata ) {
        if ( !is_array( $metadata ) ) {
            return [ '_error' => $metadata ];
        }
        '@phan-var array[] $metadata';
        $ret = [];
        foreach ( $metadata as $meta ) {
            $ret[$meta['name']] = self::parseMetadataValue( $meta['value'] );
        }

        return $ret;
    }

    /**
     * @param mixed $metadata
     * @return mixed
     */
    private static function parseMetadataValue( $metadata ) {
        if ( !is_array( $metadata ) ) {
            return $metadata;
        }
        '@phan-var array[] $metadata';
        $ret = [];
        foreach ( $metadata as $meta ) {
            $ret[$meta['name']] = self::parseMetadataValue( $meta['value'] );
        }

        return $ret;
    }

    /**
     * @return int|null|false
     */
    public function getSize() {
        return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
    }

    /**
     * @return null|string
     */
    public function getUrl() {
        return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
    }

    /**
     * Get short description URL for a file based on the foreign API response,
     * or if unavailable, the short URL is constructed from the foreign page ID.
     *
     * @return null|string
     * @since 1.27
     */
    public function getDescriptionShortUrl() {
        if ( isset( $this->mInfo['descriptionshorturl'] ) ) {
            return $this->mInfo['descriptionshorturl'];
        } elseif ( isset( $this->mInfo['pageid'] ) ) {
            $url = $this->repo->makeUrl( [ 'curid' => $this->mInfo['pageid'] ] );
            if ( $url !== false ) {
                return $url;
            }
        }
        return null;
    }

    public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
        if ( isset( $this->mInfo['user'] ) ) {
            return UserIdentityValue::newExternal( $this->getRepoName(), $this->mInfo['user'] );
        }
        return null;
    }

    /**
     * @param int $audience
     * @param Authority|null $performer
     * @return null|string
     */
    public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
        return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
    }

    /**
     * @return null|string
     */
    public function getSha1() {
        return isset( $this->mInfo['sha1'] )
            ? Wikimedia\base_convert( strval( $this->mInfo['sha1'] ), 16, 36, 31 )
            : null;
    }

    /**
     * @return string|false
     */
    public function getTimestamp() {
        return wfTimestamp( TS_MW,
            isset( $this->mInfo['timestamp'] )
                ? strval( $this->mInfo['timestamp'] )
                : null
        );
    }

    /**
     * @return string
     */
    public function getMimeType() {
        if ( !isset( $this->mInfo['mime'] ) ) {
            $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
            $this->mInfo['mime'] = $magic->getMimeTypeFromExtensionOrNull( $this->getExtension() );
        }

        return $this->mInfo['mime'];
    }

    /**
     * @return int|string
     */
    public function getMediaType() {
        if ( isset( $this->mInfo['mediatype'] ) ) {
            return $this->mInfo['mediatype'];
        }
        $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();

        return $magic->getMediaType( null, $this->getMimeType() );
    }

    /**
     * @return string|false
     */
    public function getDescriptionUrl() {
        return $this->mInfo['descriptionurl'] ?? false;
    }

    /**
     * Only useful if we're locally caching thumbs anyway...
     * @param string $suffix
     * @return null|string
     */
    public function getThumbPath( $suffix = '' ) {
        if ( !$this->repo->canCacheThumbs() ) {
            return null;
        }

        $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getHashPath();
        if ( $suffix ) {
            $path .= $suffix . '/';
        }
        return $path;
    }

    /**
     * @return string[]
     */
    protected function getThumbnails() {
        $dir = $this->getThumbPath( $this->getName() );
        $iter = $this->repo->getBackend()->getFileList( [ 'dir' => $dir ] );

        $files = [];
        if ( $iter ) {
            foreach ( $iter as $file ) {
                $files[] = $file;
            }
        }

        return $files;
    }

    public function purgeCache( $options = [] ) {
        $this->purgeThumbnails( $options );
        $this->purgeDescriptionPage();
    }

    private function purgeDescriptionPage() {
        $services = MediaWikiServices::getInstance();
        $langCode = $services->getContentLanguage()->getCode();

        // Key must match File::getDescriptionText
        $key = $this->repo->getLocalCacheKey( 'file-remote-description', $langCode, md5( $this->getName() ) );
        $services->getMainWANObjectCache()->delete( $key );
    }

    /**
     * @param array $options
     */
    public function purgeThumbnails( $options = [] ) {
        $key = $this->repo->getLocalCacheKey( 'file-thumb-url', sha1( $this->getName() ) );
        MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );

        $files = $this->getThumbnails();
        // Give media handler a chance to filter the purge list
        $handler = $this->getHandler();
        if ( $handler ) {
            $handler->filterThumbnailPurgeList( $files, $options );
        }

        $dir = $this->getThumbPath( $this->getName() );
        $purgeList = [];
        foreach ( $files as $file ) {
            $purgeList[] = "{$dir}{$file}";
        }

        # Delete the thumbnails
        $this->repo->quickPurgeBatch( $purgeList );
        # Clear out the thumbnail directory if empty
        $this->repo->quickCleanDir( $dir );
    }

    /**
     * The thumbnail is created on the foreign server and fetched over internet
     * @since 1.25
     * @return bool
     */
    public function isTransformedLocally() {
        return false;
    }
}