FileRepo.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <?php
  2. /**
  3. * Base class for file repositories
  4. * Do not instantiate, use a derived class.
  5. * @ingroup FileRepo
  6. */
  7. abstract class FileRepo {
  8. const DELETE_SOURCE = 1;
  9. const FIND_PRIVATE = 1;
  10. const FIND_IGNORE_REDIRECT = 2;
  11. const OVERWRITE = 2;
  12. const OVERWRITE_SAME = 4;
  13. var $thumbScriptUrl, $transformVia404;
  14. var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
  15. var $pathDisclosureProtection = 'paranoid';
  16. var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels;
  17. /**
  18. * Factory functions for creating new files
  19. * Override these in the base class
  20. */
  21. var $fileFactory = false, $oldFileFactory = false;
  22. var $fileFactoryKey = false, $oldFileFactoryKey = false;
  23. function __construct( $info ) {
  24. // Required settings
  25. $this->name = $info['name'];
  26. // Optional settings
  27. $this->initialCapital = true; // by default
  28. foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
  29. 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection',
  30. 'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var )
  31. {
  32. if ( isset( $info[$var] ) ) {
  33. $this->$var = $info[$var];
  34. }
  35. }
  36. $this->transformVia404 = !empty( $info['transformVia404'] );
  37. }
  38. /**
  39. * Determine if a string is an mwrepo:// URL
  40. */
  41. static function isVirtualUrl( $url ) {
  42. return substr( $url, 0, 9 ) == 'mwrepo://';
  43. }
  44. /**
  45. * Create a new File object from the local repository
  46. * @param mixed $title Title object or string
  47. * @param mixed $time Time at which the image was uploaded.
  48. * If this is specified, the returned object will be an
  49. * instance of the repository's old file class instead of
  50. * a current file. Repositories not supporting version
  51. * control should return false if this parameter is set.
  52. */
  53. function newFile( $title, $time = false ) {
  54. if ( !($title instanceof Title) ) {
  55. $title = Title::makeTitleSafe( NS_FILE, $title );
  56. if ( !is_object( $title ) ) {
  57. return null;
  58. }
  59. }
  60. if ( $time ) {
  61. if ( $this->oldFileFactory ) {
  62. return call_user_func( $this->oldFileFactory, $title, $this, $time );
  63. } else {
  64. return false;
  65. }
  66. } else {
  67. return call_user_func( $this->fileFactory, $title, $this );
  68. }
  69. }
  70. /**
  71. * Find an instance of the named file created at the specified time
  72. * Returns false if the file does not exist. Repositories not supporting
  73. * version control should return false if the time is specified.
  74. *
  75. * @param mixed $title Title object or string
  76. * @param mixed $time 14-character timestamp, or false for the current version
  77. */
  78. function findFile( $title, $time = false, $flags = 0 ) {
  79. if ( !($title instanceof Title) ) {
  80. $title = Title::makeTitleSafe( NS_FILE, $title );
  81. if ( !is_object( $title ) ) {
  82. return false;
  83. }
  84. }
  85. # First try the current version of the file to see if it precedes the timestamp
  86. $img = $this->newFile( $title );
  87. if ( !$img ) {
  88. return false;
  89. }
  90. if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
  91. return $img;
  92. }
  93. # Now try an old version of the file
  94. if ( $time !== false ) {
  95. $img = $this->newFile( $title, $time );
  96. if ( $img && $img->exists() ) {
  97. if ( !$img->isDeleted(File::DELETED_FILE) ) {
  98. return $img;
  99. } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
  100. return $img;
  101. }
  102. }
  103. }
  104. # Now try redirects
  105. if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) {
  106. return false;
  107. }
  108. $redir = $this->checkRedirect( $title );
  109. if( $redir && $redir->getNamespace() == NS_FILE) {
  110. $img = $this->newFile( $redir );
  111. if( !$img ) {
  112. return false;
  113. }
  114. if( $img->exists() ) {
  115. $img->redirectedFrom( $title->getDBkey() );
  116. return $img;
  117. }
  118. }
  119. return false;
  120. }
  121. /*
  122. * Find many files at once.
  123. * @param array $titles, an array of titles
  124. * @todo Think of a good way to optionally pass timestamps to this function.
  125. */
  126. function findFiles( $titles ) {
  127. $result = array();
  128. foreach ( $titles as $index => $title ) {
  129. $file = $this->findFile( $title );
  130. if ( $file )
  131. $result[$file->getTitle()->getDBkey()] = $file;
  132. }
  133. return $result;
  134. }
  135. /**
  136. * Create a new File object from the local repository
  137. * @param mixed $sha1 SHA-1 key
  138. * @param mixed $time Time at which the image was uploaded.
  139. * If this is specified, the returned object will be an
  140. * instance of the repository's old file class instead of
  141. * a current file. Repositories not supporting version
  142. * control should return false if this parameter is set.
  143. */
  144. function newFileFromKey( $sha1, $time = false ) {
  145. if ( $time ) {
  146. if ( $this->oldFileFactoryKey ) {
  147. return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
  148. } else {
  149. return false;
  150. }
  151. } else {
  152. return call_user_func( $this->fileFactoryKey, $sha1, $this );
  153. }
  154. }
  155. /**
  156. * Find an instance of the file with this key, created at the specified time
  157. * Returns false if the file does not exist. Repositories not supporting
  158. * version control should return false if the time is specified.
  159. *
  160. * @param string $sha1 string
  161. * @param mixed $time 14-character timestamp, or false for the current version
  162. */
  163. function findFileFromKey( $sha1, $time = false, $flags = 0 ) {
  164. # First try the current version of the file to see if it precedes the timestamp
  165. $img = $this->newFileFromKey( $sha1 );
  166. if ( !$img ) {
  167. return false;
  168. }
  169. if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
  170. return $img;
  171. }
  172. # Now try an old version of the file
  173. if ( $time !== false ) {
  174. $img = $this->newFileFromKey( $sha1, $time );
  175. if ( $img->exists() ) {
  176. if ( !$img->isDeleted(File::DELETED_FILE) ) {
  177. return $img;
  178. } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
  179. return $img;
  180. }
  181. }
  182. }
  183. return false;
  184. }
  185. /**
  186. * Get the URL of thumb.php
  187. */
  188. function getThumbScriptUrl() {
  189. return $this->thumbScriptUrl;
  190. }
  191. /**
  192. * Returns true if the repository can transform files via a 404 handler
  193. */
  194. function canTransformVia404() {
  195. return $this->transformVia404;
  196. }
  197. /**
  198. * Get the name of an image from its title object
  199. */
  200. function getNameFromTitle( $title ) {
  201. global $wgCapitalLinks;
  202. if ( $this->initialCapital != $wgCapitalLinks ) {
  203. global $wgContLang;
  204. $name = $title->getUserCaseDBKey();
  205. if ( $this->initialCapital ) {
  206. $name = $wgContLang->ucfirst( $name );
  207. }
  208. } else {
  209. $name = $title->getDBkey();
  210. }
  211. return $name;
  212. }
  213. static function getHashPathForLevel( $name, $levels ) {
  214. if ( $levels == 0 ) {
  215. return '';
  216. } else {
  217. $hash = md5( $name );
  218. $path = '';
  219. for ( $i = 1; $i <= $levels; $i++ ) {
  220. $path .= substr( $hash, 0, $i ) . '/';
  221. }
  222. return $path;
  223. }
  224. }
  225. /**
  226. * Get a relative path including trailing slash, e.g. f/fa/
  227. * If the repo is not hashed, returns an empty string
  228. */
  229. function getHashPath( $name ) {
  230. return self::getHashPathForLevel( $name, $this->hashLevels );
  231. }
  232. /**
  233. * Get the name of this repository, as specified by $info['name]' to the constructor
  234. */
  235. function getName() {
  236. return $this->name;
  237. }
  238. /**
  239. * Get the URL of an image description page. May return false if it is
  240. * unknown or not applicable. In general this should only be called by the
  241. * File class, since it may return invalid results for certain kinds of
  242. * repositories. Use File::getDescriptionUrl() in user code.
  243. *
  244. * In particular, it uses the article paths as specified to the repository
  245. * constructor, whereas local repositories use the local Title functions.
  246. */
  247. function getDescriptionUrl( $name ) {
  248. $encName = wfUrlencode( $name );
  249. if ( !is_null( $this->descBaseUrl ) ) {
  250. # "http://example.com/wiki/Image:"
  251. return $this->descBaseUrl . $encName;
  252. }
  253. if ( !is_null( $this->articleUrl ) ) {
  254. # "http://example.com/wiki/$1"
  255. #
  256. # We use "Image:" as the canonical namespace for
  257. # compatibility across all MediaWiki versions.
  258. return str_replace( '$1',
  259. "Image:$encName", $this->articleUrl );
  260. }
  261. if ( !is_null( $this->scriptDirUrl ) ) {
  262. # "http://example.com/w"
  263. #
  264. # We use "Image:" as the canonical namespace for
  265. # compatibility across all MediaWiki versions,
  266. # and just sort of hope index.php is right. ;)
  267. return $this->scriptDirUrl .
  268. "/index.php?title=Image:$encName";
  269. }
  270. return false;
  271. }
  272. /**
  273. * Get the URL of the content-only fragment of the description page. For
  274. * MediaWiki this means action=render. This should only be called by the
  275. * repository's file class, since it may return invalid results. User code
  276. * should use File::getDescriptionText().
  277. * @param string $name Name of image to fetch
  278. * @param string $lang Language to fetch it in, if any.
  279. */
  280. function getDescriptionRenderUrl( $name, $lang = null ) {
  281. $query = 'action=render';
  282. if ( !is_null( $lang ) ) {
  283. $query .= '&uselang=' . $lang;
  284. }
  285. if ( isset( $this->scriptDirUrl ) ) {
  286. return $this->scriptDirUrl . '/index.php?title=' .
  287. wfUrlencode( 'Image:' . $name ) .
  288. "&$query";
  289. } else {
  290. $descUrl = $this->getDescriptionUrl( $name );
  291. if ( $descUrl ) {
  292. return wfAppendQuery( $descUrl, $query );
  293. } else {
  294. return false;
  295. }
  296. }
  297. }
  298. /**
  299. * Store a file to a given destination.
  300. *
  301. * @param string $srcPath Source path or virtual URL
  302. * @param string $dstZone Destination zone
  303. * @param string $dstRel Destination relative path
  304. * @param integer $flags Bitwise combination of the following flags:
  305. * self::DELETE_SOURCE Delete the source file after upload
  306. * self::OVERWRITE Overwrite an existing destination file instead of failing
  307. * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
  308. * same contents as the source
  309. * @return FileRepoStatus
  310. */
  311. function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
  312. $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
  313. if ( $status->successCount == 0 ) {
  314. $status->ok = false;
  315. }
  316. return $status;
  317. }
  318. /**
  319. * Store a batch of files
  320. *
  321. * @param array $triplets (src,zone,dest) triplets as per store()
  322. * @param integer $flags Flags as per store
  323. */
  324. abstract function storeBatch( $triplets, $flags = 0 );
  325. /**
  326. * Pick a random name in the temp zone and store a file to it.
  327. * Returns a FileRepoStatus object with the URL in the value.
  328. *
  329. * @param string $originalName The base name of the file as specified
  330. * by the user. The file extension will be maintained.
  331. * @param string $srcPath The current location of the file.
  332. */
  333. abstract function storeTemp( $originalName, $srcPath );
  334. /**
  335. * Remove a temporary file or mark it for garbage collection
  336. * @param string $virtualUrl The virtual URL returned by storeTemp
  337. * @return boolean True on success, false on failure
  338. * STUB
  339. */
  340. function freeTemp( $virtualUrl ) {
  341. return true;
  342. }
  343. /**
  344. * Copy or move a file either from the local filesystem or from an mwrepo://
  345. * virtual URL, into this repository at the specified destination location.
  346. *
  347. * Returns a FileRepoStatus object. On success, the value contains "new" or
  348. * "archived", to indicate whether the file was new with that name.
  349. *
  350. * @param string $srcPath The source path or URL
  351. * @param string $dstRel The destination relative path
  352. * @param string $archiveRel The relative path where the existing file is to
  353. * be archived, if there is one. Relative to the public zone root.
  354. * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
  355. * that the source file should be deleted if possible
  356. */
  357. function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
  358. $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
  359. if ( $status->successCount == 0 ) {
  360. $status->ok = false;
  361. }
  362. if ( isset( $status->value[0] ) ) {
  363. $status->value = $status->value[0];
  364. } else {
  365. $status->value = false;
  366. }
  367. return $status;
  368. }
  369. /**
  370. * Publish a batch of files
  371. * @param array $triplets (source,dest,archive) triplets as per publish()
  372. * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
  373. * that the source files should be deleted if possible
  374. */
  375. abstract function publishBatch( $triplets, $flags = 0 );
  376. /**
  377. * Move a group of files to the deletion archive.
  378. *
  379. * If no valid deletion archive is configured, this may either delete the
  380. * file or throw an exception, depending on the preference of the repository.
  381. *
  382. * The overwrite policy is determined by the repository -- currently FSRepo
  383. * assumes a naming scheme in the deleted zone based on content hash, as
  384. * opposed to the public zone which is assumed to be unique.
  385. *
  386. * @param array $sourceDestPairs Array of source/destination pairs. Each element
  387. * is a two-element array containing the source file path relative to the
  388. * public root in the first element, and the archive file path relative
  389. * to the deleted zone root in the second element.
  390. * @return FileRepoStatus
  391. */
  392. abstract function deleteBatch( $sourceDestPairs );
  393. /**
  394. * Move a file to the deletion archive.
  395. * If no valid deletion archive exists, this may either delete the file
  396. * or throw an exception, depending on the preference of the repository
  397. * @param mixed $srcRel Relative path for the file to be deleted
  398. * @param mixed $archiveRel Relative path for the archive location.
  399. * Relative to a private archive directory.
  400. * @return WikiError object (wikitext-formatted), or true for success
  401. */
  402. function delete( $srcRel, $archiveRel ) {
  403. return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
  404. }
  405. /**
  406. * Get properties of a file with a given virtual URL
  407. * The virtual URL must refer to this repo
  408. * Properties should ultimately be obtained via File::getPropsFromPath()
  409. */
  410. abstract function getFileProps( $virtualUrl );
  411. /**
  412. * Call a callback function for every file in the repository
  413. * May use either the database or the filesystem
  414. * STUB
  415. */
  416. function enumFiles( $callback ) {
  417. throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
  418. }
  419. /**
  420. * Determine if a relative path is valid, i.e. not blank or involving directory traveral
  421. */
  422. function validateFilename( $filename ) {
  423. if ( strval( $filename ) == '' ) {
  424. return false;
  425. }
  426. if ( wfIsWindows() ) {
  427. $filename = strtr( $filename, '\\', '/' );
  428. }
  429. /**
  430. * Use the same traversal protection as Title::secureAndSplit()
  431. */
  432. if ( strpos( $filename, '.' ) !== false &&
  433. ( $filename === '.' || $filename === '..' ||
  434. strpos( $filename, './' ) === 0 ||
  435. strpos( $filename, '../' ) === 0 ||
  436. strpos( $filename, '/./' ) !== false ||
  437. strpos( $filename, '/../' ) !== false ) )
  438. {
  439. return false;
  440. } else {
  441. return true;
  442. }
  443. }
  444. /**#@+
  445. * Path disclosure protection functions
  446. */
  447. function paranoidClean( $param ) { return '[hidden]'; }
  448. function passThrough( $param ) { return $param; }
  449. /**
  450. * Get a callback function to use for cleaning error message parameters
  451. */
  452. function getErrorCleanupFunction() {
  453. switch ( $this->pathDisclosureProtection ) {
  454. case 'none':
  455. $callback = array( $this, 'passThrough' );
  456. break;
  457. default: // 'paranoid'
  458. $callback = array( $this, 'paranoidClean' );
  459. }
  460. return $callback;
  461. }
  462. /**#@-*/
  463. /**
  464. * Create a new fatal error
  465. */
  466. function newFatal( $message /*, parameters...*/ ) {
  467. $params = func_get_args();
  468. array_unshift( $params, $this );
  469. return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
  470. }
  471. /**
  472. * Create a new good result
  473. */
  474. function newGood( $value = null ) {
  475. return FileRepoStatus::newGood( $this, $value );
  476. }
  477. /**
  478. * Delete files in the deleted directory if they are not referenced in the filearchive table
  479. * STUB
  480. */
  481. function cleanupDeletedBatch( $storageKeys ) {}
  482. /**
  483. * Checks if there is a redirect named as $title. If there is, return the
  484. * title object. If not, return false.
  485. * STUB
  486. *
  487. * @param Title $title Title of image
  488. */
  489. function checkRedirect( $title ) {
  490. return false;
  491. }
  492. /**
  493. * Invalidates image redirect cache related to that image
  494. *
  495. * @param Title $title Title of image
  496. */
  497. function invalidateImageRedirect( $title ) {
  498. global $wgMemc;
  499. $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
  500. $wgMemc->delete( $memcKey );
  501. }
  502. function findBySha1( $hash ) {
  503. return array();
  504. }
  505. /**
  506. * Get the human-readable name of the repo.
  507. * @return string
  508. */
  509. public function getDisplayName() {
  510. // We don't name our own repo, return nothing
  511. if ( $this->name == 'local' ) {
  512. return null;
  513. }
  514. $repoName = wfMsg( 'shared-repo-name-' . $this->name );
  515. if ( !wfEmptyMsg( 'shared-repo-name-' . $this->name, $repoName ) ) {
  516. return $repoName;
  517. }
  518. return wfMsg( 'shared-repo' );
  519. }
  520. function getSlaveDB() {
  521. return wfGetDB( DB_SLAVE );
  522. }
  523. function getMasterDB() {
  524. return wfGetDB( DB_MASTER );
  525. }
  526. function getMemcKey( $key ) {
  527. return wfWikiID( $this->getSlaveDB() ) . ":{$key}";
  528. }
  529. }