1<?php 2/** 3 * Foreign file accessible through api.php requests. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup FileAbstraction 22 */ 23 24use MediaWiki\MediaWikiServices; 25 26/** 27 * Foreign file accessible through api.php requests. 28 * Very hacky and inefficient, do not use :D 29 * 30 * @ingroup FileAbstraction 31 */ 32class ForeignAPIFile extends File { 33 /** @var bool */ 34 private $mExists; 35 /** @var array */ 36 private $mInfo = []; 37 38 protected $repoClass = ForeignAPIRepo::class; 39 40 /** 41 * @param Title|string|bool $title 42 * @param ForeignApiRepo $repo 43 * @param array $info 44 * @param bool $exists 45 */ 46 public function __construct( $title, $repo, $info, $exists = false ) { 47 parent::__construct( $title, $repo ); 48 49 $this->mInfo = $info; 50 $this->mExists = $exists; 51 52 $this->assertRepoDefined(); 53 } 54 55 /** 56 * @param Title $title 57 * @param ForeignApiRepo $repo 58 * @return ForeignAPIFile|null 59 */ 60 public static function newFromTitle( Title $title, $repo ) { 61 $data = $repo->fetchImageQuery( [ 62 'titles' => 'File:' . $title->getDBkey(), 63 'iiprop' => self::getProps(), 64 'prop' => 'imageinfo', 65 'iimetadataversion' => MediaHandler::getMetadataVersion(), 66 // extmetadata is language-dependant, accessing the current language here 67 // would be problematic, so we just get them all 68 'iiextmetadatamultilang' => 1, 69 ] ); 70 71 $info = $repo->getImageInfo( $data ); 72 73 if ( $info ) { 74 $lastRedirect = isset( $data['query']['redirects'] ) 75 ? count( $data['query']['redirects'] ) - 1 76 : -1; 77 if ( $lastRedirect >= 0 ) { 78 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable 79 $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] ); 80 $img = new self( $newtitle, $repo, $info, true ); 81 $img->redirectedFrom( $title->getDBkey() ); 82 } else { 83 $img = new self( $title, $repo, $info, true ); 84 } 85 86 return $img; 87 } else { 88 return null; 89 } 90 } 91 92 /** 93 * Get the property string for iiprop and aiprop 94 * @return string 95 */ 96 public static function getProps() { 97 return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype|extmetadata'; 98 } 99 100 /** 101 * @return ForeignAPIRepo|bool 102 */ 103 public function getRepo() { 104 return $this->repo; 105 } 106 107 // Dummy functions... 108 109 /** 110 * @return bool 111 */ 112 public function exists() { 113 return $this->mExists; 114 } 115 116 /** 117 * @return bool 118 */ 119 public function getPath() { 120 return false; 121 } 122 123 /** 124 * @param array $params 125 * @param int $flags 126 * @return bool|MediaTransformOutput 127 */ 128 public function transform( $params, $flags = 0 ) { 129 if ( !$this->canRender() ) { 130 // show icon 131 return parent::transform( $params, $flags ); 132 } 133 134 // Note, the this->canRender() check above implies 135 // that we have a handler, and it can do makeParamString. 136 $otherParams = $this->handler->makeParamString( $params ); 137 $width = $params['width'] ?? -1; 138 $height = $params['height'] ?? -1; 139 140 $thumbUrl = $this->repo->getThumbUrlFromCache( 141 $this->getName(), 142 $width, 143 $height, 144 $otherParams 145 ); 146 if ( $thumbUrl === false ) { 147 global $wgLang; 148 149 return $this->repo->getThumbError( 150 $this->getName(), 151 $width, 152 $height, 153 $otherParams, 154 $wgLang->getCode() 155 ); 156 } 157 158 return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params ); 159 } 160 161 // Info we can get from API... 162 163 /** 164 * @param int $page 165 * @return int 166 */ 167 public function getWidth( $page = 1 ) { 168 return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0; 169 } 170 171 /** 172 * @param int $page 173 * @return int 174 */ 175 public function getHeight( $page = 1 ) { 176 return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0; 177 } 178 179 /** 180 * @return bool|null|string 181 */ 182 public function getMetadata() { 183 if ( isset( $this->mInfo['metadata'] ) ) { 184 return serialize( self::parseMetadata( $this->mInfo['metadata'] ) ); 185 } 186 187 return null; 188 } 189 190 /** 191 * @return array|null Extended metadata (see imageinfo API for format) or 192 * null on error 193 */ 194 public function getExtendedMetadata() { 195 return $this->mInfo['extmetadata'] ?? null; 196 } 197 198 /** 199 * @param mixed $metadata 200 * @return mixed 201 */ 202 public static function parseMetadata( $metadata ) { 203 if ( !is_array( $metadata ) ) { 204 return $metadata; 205 } 206 '@phan-var array[] $metadata'; 207 $ret = []; 208 foreach ( $metadata as $meta ) { 209 $ret[$meta['name']] = self::parseMetadata( $meta['value'] ); 210 } 211 212 return $ret; 213 } 214 215 /** 216 * @return bool|int|null 217 */ 218 public function getSize() { 219 return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null; 220 } 221 222 /** 223 * @return null|string 224 */ 225 public function getUrl() { 226 return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null; 227 } 228 229 /** 230 * Get short description URL for a file based on the foreign API response, 231 * or if unavailable, the short URL is constructed from the foreign page ID. 232 * 233 * @return null|string 234 * @since 1.27 235 */ 236 public function getDescriptionShortUrl() { 237 if ( isset( $this->mInfo['descriptionshorturl'] ) ) { 238 return $this->mInfo['descriptionshorturl']; 239 } elseif ( isset( $this->mInfo['pageid'] ) ) { 240 $url = $this->repo->makeUrl( [ 'curid' => $this->mInfo['pageid'] ] ); 241 if ( $url !== false ) { 242 return $url; 243 } 244 } 245 return null; 246 } 247 248 /** 249 * @param string $type 250 * @return int|null|string 251 */ 252 public function getUser( $type = 'text' ) { 253 if ( $type == 'text' ) { 254 return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; 255 } else { 256 return 0; // What makes sense here, for a remote user? 257 } 258 } 259 260 /** 261 * @param int $audience 262 * @param User|null $user 263 * @return null|string 264 */ 265 public function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { 266 return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null; 267 } 268 269 /** 270 * @return null|string 271 */ 272 public function getSha1() { 273 return isset( $this->mInfo['sha1'] ) 274 ? Wikimedia\base_convert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) 275 : null; 276 } 277 278 /** 279 * @return bool|string 280 */ 281 public function getTimestamp() { 282 return wfTimestamp( TS_MW, 283 isset( $this->mInfo['timestamp'] ) 284 ? strval( $this->mInfo['timestamp'] ) 285 : null 286 ); 287 } 288 289 /** 290 * @return string 291 */ 292 public function getMimeType() { 293 if ( !isset( $this->mInfo['mime'] ) ) { 294 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); 295 $this->mInfo['mime'] = $magic->getMimeTypeFromExtensionOrNull( $this->getExtension() ); 296 } 297 298 return $this->mInfo['mime']; 299 } 300 301 /** 302 * @return int|string 303 */ 304 public function getMediaType() { 305 if ( isset( $this->mInfo['mediatype'] ) ) { 306 return $this->mInfo['mediatype']; 307 } 308 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); 309 310 return $magic->getMediaType( null, $this->getMimeType() ); 311 } 312 313 /** 314 * @return bool|string 315 */ 316 public function getDescriptionUrl() { 317 return $this->mInfo['descriptionurl'] ?? false; 318 } 319 320 /** 321 * Only useful if we're locally caching thumbs anyway... 322 * @param string $suffix 323 * @return null|string 324 */ 325 public function getThumbPath( $suffix = '' ) { 326 if ( !$this->repo->canCacheThumbs() ) { 327 return null; 328 } 329 330 $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getHashPath(); 331 if ( $suffix ) { 332 $path .= $suffix . '/'; 333 } 334 return $path; 335 } 336 337 /** 338 * @return string[] 339 */ 340 protected function getThumbnails() { 341 $dir = $this->getThumbPath( $this->getName() ); 342 $iter = $this->repo->getBackend()->getFileList( [ 'dir' => $dir ] ); 343 344 $files = []; 345 if ( $iter ) { 346 foreach ( $iter as $file ) { 347 $files[] = $file; 348 } 349 } 350 351 return $files; 352 } 353 354 public function purgeCache( $options = [] ) { 355 $this->purgeThumbnails( $options ); 356 $this->purgeDescriptionPage(); 357 } 358 359 private function purgeDescriptionPage() { 360 $services = MediaWikiServices::getInstance(); 361 $url = $this->repo->getDescriptionRenderUrl( 362 $this->getName(), $services->getContentLanguage()->getCode() ); 363 $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5( $url ) ); 364 365 $services->getMainWANObjectCache()->delete( $key ); 366 } 367 368 /** 369 * @param array $options 370 */ 371 public function purgeThumbnails( $options = [] ) { 372 $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() ); 373 MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key ); 374 375 $files = $this->getThumbnails(); 376 // Give media handler a chance to filter the purge list 377 $handler = $this->getHandler(); 378 if ( $handler ) { 379 $handler->filterThumbnailPurgeList( $files, $options ); 380 } 381 382 $dir = $this->getThumbPath( $this->getName() ); 383 $purgeList = []; 384 foreach ( $files as $file ) { 385 $purgeList[] = "{$dir}{$file}"; 386 } 387 388 # Delete the thumbnails 389 $this->repo->quickPurgeBatch( $purgeList ); 390 # Clear out the thumbnail directory if empty 391 $this->repo->quickCleanDir( $dir ); 392 } 393 394 /** 395 * The thumbnail is created on the foreign server and fetched over internet 396 * @since 1.25 397 * @return bool 398 */ 399 public function isTransformedLocally() { 400 return false; 401 } 402} 403