LocalFile.php 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860
  1. <?php
  2. /**
  3. */
  4. /**
  5. * Bump this number when serialized cache records may be incompatible.
  6. */
  7. define( 'MW_FILE_VERSION', 8 );
  8. /**
  9. * Class to represent a local file in the wiki's own database
  10. *
  11. * Provides methods to retrieve paths (physical, logical, URL),
  12. * to generate image thumbnails or for uploading.
  13. *
  14. * Note that only the repo object knows what its file class is called. You should
  15. * never name a file class explictly outside of the repo class. Instead use the
  16. * repo's factory functions to generate file objects, for example:
  17. *
  18. * RepoGroup::singleton()->getLocalRepo()->newFile($title);
  19. *
  20. * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
  21. * in most cases.
  22. *
  23. * @ingroup FileRepo
  24. */
  25. class LocalFile extends File
  26. {
  27. /**#@+
  28. * @private
  29. */
  30. var $fileExists, # does the file file exist on disk? (loadFromXxx)
  31. $historyLine, # Number of line to return by nextHistoryLine() (constructor)
  32. $historyRes, # result of the query for the file's history (nextHistoryLine)
  33. $width, # \
  34. $height, # |
  35. $bits, # --- returned by getimagesize (loadFromXxx)
  36. $attr, # /
  37. $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
  38. $mime, # MIME type, determined by MimeMagic::guessMimeType
  39. $major_mime, # Major mime type
  40. $minor_mime, # Minor mime type
  41. $size, # Size in bytes (loadFromXxx)
  42. $metadata, # Handler-specific metadata
  43. $timestamp, # Upload timestamp
  44. $sha1, # SHA-1 base 36 content hash
  45. $user, $user_text, # User, who uploaded the file
  46. $description, # Description of current revision of the file
  47. $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
  48. $upgraded, # Whether the row was upgraded on load
  49. $locked, # True if the image row is locked
  50. $deleted; # Bitfield akin to rev_deleted
  51. /**#@-*/
  52. /**
  53. * Create a LocalFile from a title
  54. * Do not call this except from inside a repo class.
  55. *
  56. * Note: $unused param is only here to avoid an E_STRICT
  57. */
  58. static function newFromTitle( $title, $repo, $unused = null ) {
  59. return new self( $title, $repo );
  60. }
  61. /**
  62. * Create a LocalFile from a title
  63. * Do not call this except from inside a repo class.
  64. */
  65. static function newFromRow( $row, $repo ) {
  66. $title = Title::makeTitle( NS_FILE, $row->img_name );
  67. $file = new self( $title, $repo );
  68. $file->loadFromRow( $row );
  69. return $file;
  70. }
  71. /**
  72. * Create a LocalFile from a SHA-1 key
  73. * Do not call this except from inside a repo class.
  74. */
  75. static function newFromKey( $sha1, $repo, $timestamp = false ) {
  76. # Polymorphic function name to distinguish foreign and local fetches
  77. $fname = get_class( $this ) . '::' . __FUNCTION__;
  78. $conds = array( 'img_sha1' => $sha1 );
  79. if( $timestamp ) {
  80. $conds['img_timestamp'] = $timestamp;
  81. }
  82. $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), $conds, $fname );
  83. if( $row ) {
  84. return self::newFromRow( $row, $repo );
  85. } else {
  86. return false;
  87. }
  88. }
  89. /**
  90. * Fields in the image table
  91. */
  92. static function selectFields() {
  93. return array(
  94. 'img_name',
  95. 'img_size',
  96. 'img_width',
  97. 'img_height',
  98. 'img_metadata',
  99. 'img_bits',
  100. 'img_media_type',
  101. 'img_major_mime',
  102. 'img_minor_mime',
  103. 'img_description',
  104. 'img_user',
  105. 'img_user_text',
  106. 'img_timestamp',
  107. 'img_sha1',
  108. );
  109. }
  110. /**
  111. * Constructor.
  112. * Do not call this except from inside a repo class.
  113. */
  114. function __construct( $title, $repo ) {
  115. if( !is_object( $title ) ) {
  116. throw new MWException( __CLASS__.' constructor given bogus title.' );
  117. }
  118. parent::__construct( $title, $repo );
  119. $this->metadata = '';
  120. $this->historyLine = 0;
  121. $this->historyRes = null;
  122. $this->dataLoaded = false;
  123. }
  124. /**
  125. * Get the memcached key
  126. */
  127. function getCacheKey() {
  128. $hashedName = md5($this->getName());
  129. return wfMemcKey( 'file', $hashedName );
  130. }
  131. /**
  132. * Try to load file metadata from memcached. Returns true on success.
  133. */
  134. function loadFromCache() {
  135. global $wgMemc;
  136. wfProfileIn( __METHOD__ );
  137. $this->dataLoaded = false;
  138. $key = $this->getCacheKey();
  139. if ( !$key ) {
  140. return false;
  141. }
  142. $cachedValues = $wgMemc->get( $key );
  143. // Check if the key existed and belongs to this version of MediaWiki
  144. if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
  145. wfDebug( "Pulling file metadata from cache key $key\n" );
  146. $this->fileExists = $cachedValues['fileExists'];
  147. if ( $this->fileExists ) {
  148. $this->setProps( $cachedValues );
  149. }
  150. $this->dataLoaded = true;
  151. }
  152. if ( $this->dataLoaded ) {
  153. wfIncrStats( 'image_cache_hit' );
  154. } else {
  155. wfIncrStats( 'image_cache_miss' );
  156. }
  157. wfProfileOut( __METHOD__ );
  158. return $this->dataLoaded;
  159. }
  160. /**
  161. * Save the file metadata to memcached
  162. */
  163. function saveToCache() {
  164. global $wgMemc;
  165. $this->load();
  166. $key = $this->getCacheKey();
  167. if ( !$key ) {
  168. return;
  169. }
  170. $fields = $this->getCacheFields( '' );
  171. $cache = array( 'version' => MW_FILE_VERSION );
  172. $cache['fileExists'] = $this->fileExists;
  173. if ( $this->fileExists ) {
  174. foreach ( $fields as $field ) {
  175. $cache[$field] = $this->$field;
  176. }
  177. }
  178. $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
  179. }
  180. /**
  181. * Load metadata from the file itself
  182. */
  183. function loadFromFile() {
  184. $this->setProps( self::getPropsFromPath( $this->getPath() ) );
  185. }
  186. function getCacheFields( $prefix = 'img_' ) {
  187. static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
  188. 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
  189. static $results = array();
  190. if ( $prefix == '' ) {
  191. return $fields;
  192. }
  193. if ( !isset( $results[$prefix] ) ) {
  194. $prefixedFields = array();
  195. foreach ( $fields as $field ) {
  196. $prefixedFields[] = $prefix . $field;
  197. }
  198. $results[$prefix] = $prefixedFields;
  199. }
  200. return $results[$prefix];
  201. }
  202. /**
  203. * Load file metadata from the DB
  204. */
  205. function loadFromDB() {
  206. # Polymorphic function name to distinguish foreign and local fetches
  207. $fname = get_class( $this ) . '::' . __FUNCTION__;
  208. wfProfileIn( $fname );
  209. # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
  210. $this->dataLoaded = true;
  211. $dbr = $this->repo->getMasterDB();
  212. $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
  213. array( 'img_name' => $this->getName() ), $fname );
  214. if ( $row ) {
  215. $this->loadFromRow( $row );
  216. } else {
  217. $this->fileExists = false;
  218. }
  219. wfProfileOut( $fname );
  220. }
  221. /**
  222. * Decode a row from the database (either object or array) to an array
  223. * with timestamps and MIME types decoded, and the field prefix removed.
  224. */
  225. function decodeRow( $row, $prefix = 'img_' ) {
  226. $array = (array)$row;
  227. $prefixLength = strlen( $prefix );
  228. // Sanity check prefix once
  229. if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
  230. throw new MWException( __METHOD__. ': incorrect $prefix parameter' );
  231. }
  232. $decoded = array();
  233. foreach ( $array as $name => $value ) {
  234. $decoded[substr( $name, $prefixLength )] = $value;
  235. }
  236. $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
  237. if ( empty( $decoded['major_mime'] ) ) {
  238. $decoded['mime'] = "unknown/unknown";
  239. } else {
  240. if (!$decoded['minor_mime']) {
  241. $decoded['minor_mime'] = "unknown";
  242. }
  243. $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
  244. }
  245. # Trim zero padding from char/binary field
  246. $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
  247. return $decoded;
  248. }
  249. /*
  250. * Load file metadata from a DB result row
  251. */
  252. function loadFromRow( $row, $prefix = 'img_' ) {
  253. $this->dataLoaded = true;
  254. $array = $this->decodeRow( $row, $prefix );
  255. foreach ( $array as $name => $value ) {
  256. $this->$name = $value;
  257. }
  258. $this->fileExists = true;
  259. $this->maybeUpgradeRow();
  260. }
  261. /**
  262. * Load file metadata from cache or DB, unless already loaded
  263. */
  264. function load() {
  265. if ( !$this->dataLoaded ) {
  266. if ( !$this->loadFromCache() ) {
  267. $this->loadFromDB();
  268. $this->saveToCache();
  269. }
  270. $this->dataLoaded = true;
  271. }
  272. }
  273. /**
  274. * Upgrade a row if it needs it
  275. */
  276. function maybeUpgradeRow() {
  277. if ( wfReadOnly() ) {
  278. return;
  279. }
  280. if ( is_null($this->media_type) ||
  281. $this->mime == 'image/svg'
  282. ) {
  283. $this->upgradeRow();
  284. $this->upgraded = true;
  285. } else {
  286. $handler = $this->getHandler();
  287. if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
  288. $this->upgradeRow();
  289. $this->upgraded = true;
  290. }
  291. }
  292. }
  293. function getUpgraded() {
  294. return $this->upgraded;
  295. }
  296. /**
  297. * Fix assorted version-related problems with the image row by reloading it from the file
  298. */
  299. function upgradeRow() {
  300. wfProfileIn( __METHOD__ );
  301. $this->loadFromFile();
  302. # Don't destroy file info of missing files
  303. if ( !$this->fileExists ) {
  304. wfDebug( __METHOD__.": file does not exist, aborting\n" );
  305. return;
  306. }
  307. $dbw = $this->repo->getMasterDB();
  308. list( $major, $minor ) = self::splitMime( $this->mime );
  309. if ( wfReadOnly() ) {
  310. return;
  311. }
  312. wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
  313. $dbw->update( 'image',
  314. array(
  315. 'img_width' => $this->width,
  316. 'img_height' => $this->height,
  317. 'img_bits' => $this->bits,
  318. 'img_media_type' => $this->media_type,
  319. 'img_major_mime' => $major,
  320. 'img_minor_mime' => $minor,
  321. 'img_metadata' => $this->metadata,
  322. 'img_sha1' => $this->sha1,
  323. ), array( 'img_name' => $this->getName() ),
  324. __METHOD__
  325. );
  326. $this->saveToCache();
  327. wfProfileOut( __METHOD__ );
  328. }
  329. /**
  330. * Set properties in this object to be equal to those given in the
  331. * associative array $info. Only cacheable fields can be set.
  332. *
  333. * If 'mime' is given, it will be split into major_mime/minor_mime.
  334. * If major_mime/minor_mime are given, $this->mime will also be set.
  335. */
  336. function setProps( $info ) {
  337. $this->dataLoaded = true;
  338. $fields = $this->getCacheFields( '' );
  339. $fields[] = 'fileExists';
  340. foreach ( $fields as $field ) {
  341. if ( isset( $info[$field] ) ) {
  342. $this->$field = $info[$field];
  343. }
  344. }
  345. // Fix up mime fields
  346. if ( isset( $info['major_mime'] ) ) {
  347. $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
  348. } elseif ( isset( $info['mime'] ) ) {
  349. list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
  350. }
  351. }
  352. /** splitMime inherited */
  353. /** getName inherited */
  354. /** getTitle inherited */
  355. /** getURL inherited */
  356. /** getViewURL inherited */
  357. /** getPath inherited */
  358. /** isVisible inhereted */
  359. /**
  360. * Return the width of the image
  361. *
  362. * Returns false on error
  363. * @public
  364. */
  365. function getWidth( $page = 1 ) {
  366. $this->load();
  367. if ( $this->isMultipage() ) {
  368. $dim = $this->getHandler()->getPageDimensions( $this, $page );
  369. if ( $dim ) {
  370. return $dim['width'];
  371. } else {
  372. return false;
  373. }
  374. } else {
  375. return $this->width;
  376. }
  377. }
  378. /**
  379. * Return the height of the image
  380. *
  381. * Returns false on error
  382. * @public
  383. */
  384. function getHeight( $page = 1 ) {
  385. $this->load();
  386. if ( $this->isMultipage() ) {
  387. $dim = $this->getHandler()->getPageDimensions( $this, $page );
  388. if ( $dim ) {
  389. return $dim['height'];
  390. } else {
  391. return false;
  392. }
  393. } else {
  394. return $this->height;
  395. }
  396. }
  397. /**
  398. * Returns ID or name of user who uploaded the file
  399. *
  400. * @param $type string 'text' or 'id'
  401. */
  402. function getUser($type='text') {
  403. $this->load();
  404. if( $type == 'text' ) {
  405. return $this->user_text;
  406. } elseif( $type == 'id' ) {
  407. return $this->user;
  408. }
  409. }
  410. /**
  411. * Get handler-specific metadata
  412. */
  413. function getMetadata() {
  414. $this->load();
  415. return $this->metadata;
  416. }
  417. function getBitDepth() {
  418. $this->load();
  419. return $this->bits;
  420. }
  421. /**
  422. * Return the size of the image file, in bytes
  423. * @public
  424. */
  425. function getSize() {
  426. $this->load();
  427. return $this->size;
  428. }
  429. /**
  430. * Returns the mime type of the file.
  431. */
  432. function getMimeType() {
  433. $this->load();
  434. return $this->mime;
  435. }
  436. /**
  437. * Return the type of the media in the file.
  438. * Use the value returned by this function with the MEDIATYPE_xxx constants.
  439. */
  440. function getMediaType() {
  441. $this->load();
  442. return $this->media_type;
  443. }
  444. /** canRender inherited */
  445. /** mustRender inherited */
  446. /** allowInlineDisplay inherited */
  447. /** isSafeFile inherited */
  448. /** isTrustedFile inherited */
  449. /**
  450. * Returns true if the file file exists on disk.
  451. * @return boolean Whether file file exist on disk.
  452. * @public
  453. */
  454. function exists() {
  455. $this->load();
  456. return $this->fileExists;
  457. }
  458. /** getTransformScript inherited */
  459. /** getUnscaledThumb inherited */
  460. /** thumbName inherited */
  461. /** createThumb inherited */
  462. /** getThumbnail inherited */
  463. /** transform inherited */
  464. /**
  465. * Fix thumbnail files from 1.4 or before, with extreme prejudice
  466. */
  467. function migrateThumbFile( $thumbName ) {
  468. $thumbDir = $this->getThumbPath();
  469. $thumbPath = "$thumbDir/$thumbName";
  470. if ( is_dir( $thumbPath ) ) {
  471. // Directory where file should be
  472. // This happened occasionally due to broken migration code in 1.5
  473. // Rename to broken-*
  474. for ( $i = 0; $i < 100 ; $i++ ) {
  475. $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName";
  476. if ( !file_exists( $broken ) ) {
  477. rename( $thumbPath, $broken );
  478. break;
  479. }
  480. }
  481. // Doesn't exist anymore
  482. clearstatcache();
  483. }
  484. if ( is_file( $thumbDir ) ) {
  485. // File where directory should be
  486. unlink( $thumbDir );
  487. // Doesn't exist anymore
  488. clearstatcache();
  489. }
  490. }
  491. /** getHandler inherited */
  492. /** iconThumb inherited */
  493. /** getLastError inherited */
  494. /**
  495. * Get all thumbnail names previously generated for this file
  496. */
  497. function getThumbnails() {
  498. $this->load();
  499. $files = array();
  500. $dir = $this->getThumbPath();
  501. if ( is_dir( $dir ) ) {
  502. $handle = opendir( $dir );
  503. if ( $handle ) {
  504. while ( false !== ( $file = readdir($handle) ) ) {
  505. if ( $file{0} != '.' ) {
  506. $files[] = $file;
  507. }
  508. }
  509. closedir( $handle );
  510. }
  511. }
  512. return $files;
  513. }
  514. /**
  515. * Refresh metadata in memcached, but don't touch thumbnails or squid
  516. */
  517. function purgeMetadataCache() {
  518. $this->loadFromDB();
  519. $this->saveToCache();
  520. $this->purgeHistory();
  521. }
  522. /**
  523. * Purge the shared history (OldLocalFile) cache
  524. */
  525. function purgeHistory() {
  526. global $wgMemc;
  527. $hashedName = md5($this->getName());
  528. $oldKey = wfMemcKey( 'oldfile', $hashedName );
  529. $wgMemc->delete( $oldKey );
  530. }
  531. /**
  532. * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
  533. */
  534. function purgeCache() {
  535. // Refresh metadata cache
  536. $this->purgeMetadataCache();
  537. // Delete thumbnails
  538. $this->purgeThumbnails();
  539. // Purge squid cache for this file
  540. SquidUpdate::purge( array( $this->getURL() ) );
  541. }
  542. /**
  543. * Delete cached transformed files
  544. */
  545. function purgeThumbnails() {
  546. global $wgUseSquid;
  547. // Delete thumbnails
  548. $files = $this->getThumbnails();
  549. $dir = $this->getThumbPath();
  550. $urls = array();
  551. foreach ( $files as $file ) {
  552. # Check that the base file name is part of the thumb name
  553. # This is a basic sanity check to avoid erasing unrelated directories
  554. if ( strpos( $file, $this->getName() ) !== false ) {
  555. $url = $this->getThumbUrl( $file );
  556. $urls[] = $url;
  557. @unlink( "$dir/$file" );
  558. }
  559. }
  560. // Purge the squid
  561. if ( $wgUseSquid ) {
  562. SquidUpdate::purge( $urls );
  563. }
  564. }
  565. /** purgeDescription inherited */
  566. /** purgeEverything inherited */
  567. function getHistory($limit = null, $start = null, $end = null, $inc = true) {
  568. $dbr = $this->repo->getSlaveDB();
  569. $tables = array('oldimage');
  570. $fields = OldLocalFile::selectFields();
  571. $conds = $opts = $join_conds = array();
  572. $eq = $inc ? "=" : "";
  573. $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() );
  574. if( $start ) {
  575. $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
  576. }
  577. if( $end ) {
  578. $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
  579. }
  580. if( $limit ) {
  581. $opts['LIMIT'] = $limit;
  582. }
  583. // Search backwards for time > x queries
  584. $order = (!$start && $end !== null) ? "ASC" : "DESC";
  585. $opts['ORDER BY'] = "oi_timestamp $order";
  586. $opts['USE INDEX'] = array('oldimage' => 'oi_name_timestamp');
  587. wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
  588. &$conds, &$opts, &$join_conds ) );
  589. $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
  590. $r = array();
  591. while( $row = $dbr->fetchObject($res) ) {
  592. $r[] = OldLocalFile::newFromRow($row, $this->repo);
  593. }
  594. if( $order == "ASC" ) {
  595. $r = array_reverse( $r ); // make sure it ends up descending
  596. }
  597. return $r;
  598. }
  599. /**
  600. * Return the history of this file, line by line.
  601. * starts with current version, then old versions.
  602. * uses $this->historyLine to check which line to return:
  603. * 0 return line for current version
  604. * 1 query for old versions, return first one
  605. * 2, ... return next old version from above query
  606. *
  607. * @public
  608. */
  609. function nextHistoryLine() {
  610. # Polymorphic function name to distinguish foreign and local fetches
  611. $fname = get_class( $this ) . '::' . __FUNCTION__;
  612. $dbr = $this->repo->getSlaveDB();
  613. if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
  614. $this->historyRes = $dbr->select( 'image',
  615. array(
  616. '*',
  617. "'' AS oi_archive_name",
  618. '0 as oi_deleted',
  619. 'img_sha1'
  620. ),
  621. array( 'img_name' => $this->title->getDBkey() ),
  622. $fname
  623. );
  624. if ( 0 == $dbr->numRows( $this->historyRes ) ) {
  625. $dbr->freeResult($this->historyRes);
  626. $this->historyRes = null;
  627. return FALSE;
  628. }
  629. } else if ( $this->historyLine == 1 ) {
  630. $dbr->freeResult($this->historyRes);
  631. $this->historyRes = $dbr->select( 'oldimage', '*',
  632. array( 'oi_name' => $this->title->getDBkey() ),
  633. $fname,
  634. array( 'ORDER BY' => 'oi_timestamp DESC' )
  635. );
  636. }
  637. $this->historyLine ++;
  638. return $dbr->fetchObject( $this->historyRes );
  639. }
  640. /**
  641. * Reset the history pointer to the first element of the history
  642. * @public
  643. */
  644. function resetHistory() {
  645. $this->historyLine = 0;
  646. if (!is_null($this->historyRes)) {
  647. $this->repo->getSlaveDB()->freeResult($this->historyRes);
  648. $this->historyRes = null;
  649. }
  650. }
  651. /** getFullPath inherited */
  652. /** getHashPath inherited */
  653. /** getRel inherited */
  654. /** getUrlRel inherited */
  655. /** getArchiveRel inherited */
  656. /** getThumbRel inherited */
  657. /** getArchivePath inherited */
  658. /** getThumbPath inherited */
  659. /** getArchiveUrl inherited */
  660. /** getThumbUrl inherited */
  661. /** getArchiveVirtualUrl inherited */
  662. /** getThumbVirtualUrl inherited */
  663. /** isHashed inherited */
  664. /**
  665. * Upload a file and record it in the DB
  666. * @param string $srcPath Source path or virtual URL
  667. * @param string $comment Upload description
  668. * @param string $pageText Text to use for the new description page, if a new description page is created
  669. * @param integer $flags Flags for publish()
  670. * @param array $props File properties, if known. This can be used to reduce the
  671. * upload time when uploading virtual URLs for which the file info
  672. * is already known
  673. * @param string $timestamp Timestamp for img_timestamp, or false to use the current time
  674. *
  675. * @return FileRepoStatus object. On success, the value member contains the
  676. * archive name, or an empty string if it was a new file.
  677. */
  678. function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
  679. $this->lock();
  680. $status = $this->publish( $srcPath, $flags );
  681. if ( $status->ok ) {
  682. if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
  683. $status->fatal( 'filenotfound', $srcPath );
  684. }
  685. }
  686. $this->unlock();
  687. return $status;
  688. }
  689. /**
  690. * Record a file upload in the upload log and the image table
  691. * @deprecated use upload()
  692. */
  693. function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
  694. $watch = false, $timestamp = false )
  695. {
  696. $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source );
  697. if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
  698. return false;
  699. }
  700. if ( $watch ) {
  701. global $wgUser;
  702. $wgUser->addWatch( $this->getTitle() );
  703. }
  704. return true;
  705. }
  706. /**
  707. * Record a file upload in the upload log and the image table
  708. */
  709. function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null )
  710. {
  711. if( is_null( $user ) ) {
  712. global $wgUser;
  713. $user = $wgUser;
  714. }
  715. $dbw = $this->repo->getMasterDB();
  716. $dbw->begin();
  717. if ( !$props ) {
  718. $props = $this->repo->getFileProps( $this->getVirtualUrl() );
  719. }
  720. $props['description'] = $comment;
  721. $props['user'] = $user->getId();
  722. $props['user_text'] = $user->getName();
  723. $props['timestamp'] = wfTimestamp( TS_MW );
  724. $this->setProps( $props );
  725. // Delete thumbnails and refresh the metadata cache
  726. $this->purgeThumbnails();
  727. $this->saveToCache();
  728. SquidUpdate::purge( array( $this->getURL() ) );
  729. // Fail now if the file isn't there
  730. if ( !$this->fileExists ) {
  731. wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" );
  732. return false;
  733. }
  734. $reupload = false;
  735. if ( $timestamp === false ) {
  736. $timestamp = $dbw->timestamp();
  737. }
  738. # Test to see if the row exists using INSERT IGNORE
  739. # This avoids race conditions by locking the row until the commit, and also
  740. # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
  741. $dbw->insert( 'image',
  742. array(
  743. 'img_name' => $this->getName(),
  744. 'img_size'=> $this->size,
  745. 'img_width' => intval( $this->width ),
  746. 'img_height' => intval( $this->height ),
  747. 'img_bits' => $this->bits,
  748. 'img_media_type' => $this->media_type,
  749. 'img_major_mime' => $this->major_mime,
  750. 'img_minor_mime' => $this->minor_mime,
  751. 'img_timestamp' => $timestamp,
  752. 'img_description' => $comment,
  753. 'img_user' => $user->getId(),
  754. 'img_user_text' => $user->getName(),
  755. 'img_metadata' => $this->metadata,
  756. 'img_sha1' => $this->sha1
  757. ),
  758. __METHOD__,
  759. 'IGNORE'
  760. );
  761. if( $dbw->affectedRows() == 0 ) {
  762. $reupload = true;
  763. # Collision, this is an update of a file
  764. # Insert previous contents into oldimage
  765. $dbw->insertSelect( 'oldimage', 'image',
  766. array(
  767. 'oi_name' => 'img_name',
  768. 'oi_archive_name' => $dbw->addQuotes( $oldver ),
  769. 'oi_size' => 'img_size',
  770. 'oi_width' => 'img_width',
  771. 'oi_height' => 'img_height',
  772. 'oi_bits' => 'img_bits',
  773. 'oi_timestamp' => 'img_timestamp',
  774. 'oi_description' => 'img_description',
  775. 'oi_user' => 'img_user',
  776. 'oi_user_text' => 'img_user_text',
  777. 'oi_metadata' => 'img_metadata',
  778. 'oi_media_type' => 'img_media_type',
  779. 'oi_major_mime' => 'img_major_mime',
  780. 'oi_minor_mime' => 'img_minor_mime',
  781. 'oi_sha1' => 'img_sha1'
  782. ), array( 'img_name' => $this->getName() ), __METHOD__
  783. );
  784. # Update the current image row
  785. $dbw->update( 'image',
  786. array( /* SET */
  787. 'img_size' => $this->size,
  788. 'img_width' => intval( $this->width ),
  789. 'img_height' => intval( $this->height ),
  790. 'img_bits' => $this->bits,
  791. 'img_media_type' => $this->media_type,
  792. 'img_major_mime' => $this->major_mime,
  793. 'img_minor_mime' => $this->minor_mime,
  794. 'img_timestamp' => $timestamp,
  795. 'img_description' => $comment,
  796. 'img_user' => $user->getId(),
  797. 'img_user_text' => $user->getName(),
  798. 'img_metadata' => $this->metadata,
  799. 'img_sha1' => $this->sha1
  800. ), array( /* WHERE */
  801. 'img_name' => $this->getName()
  802. ), __METHOD__
  803. );
  804. } else {
  805. # This is a new file
  806. # Update the image count
  807. $site_stats = $dbw->tableName( 'site_stats' );
  808. $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
  809. }
  810. $descTitle = $this->getTitle();
  811. $article = new ImagePage( $descTitle );
  812. $article->setFile( $this );
  813. # Add the log entry
  814. $log = new LogPage( 'upload' );
  815. $action = $reupload ? 'overwrite' : 'upload';
  816. $log->addEntry( $action, $descTitle, $comment, array(), $user );
  817. if( $descTitle->exists() ) {
  818. # Create a null revision
  819. $latest = $descTitle->getLatestRevID();
  820. $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(),
  821. $log->getRcComment(), false );
  822. $nullRevision->insertOn( $dbw );
  823. wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $user) );
  824. $article->updateRevisionOn( $dbw, $nullRevision );
  825. # Invalidate the cache for the description page
  826. $descTitle->invalidateCache();
  827. $descTitle->purgeSquid();
  828. } else {
  829. // New file; create the description page.
  830. // There's already a log entry, so don't make a second RC entry
  831. $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
  832. }
  833. # Hooks, hooks, the magic of hooks...
  834. wfRunHooks( 'FileUpload', array( $this ) );
  835. # Commit the transaction now, in case something goes wrong later
  836. # The most important thing is that files don't get lost, especially archives
  837. $dbw->immediateCommit();
  838. # Invalidate cache for all pages using this file
  839. $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
  840. $update->doUpdate();
  841. # Invalidate cache for all pages that redirects on this page
  842. $redirs = $this->getTitle()->getRedirectsHere();
  843. foreach( $redirs as $redir ) {
  844. $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
  845. $update->doUpdate();
  846. }
  847. return true;
  848. }
  849. /**
  850. * Move or copy a file to its public location. If a file exists at the
  851. * destination, move it to an archive. Returns the archive name on success
  852. * or an empty string if it was a new file, and a wikitext-formatted
  853. * WikiError object on failure.
  854. *
  855. * The archive name should be passed through to recordUpload for database
  856. * registration.
  857. *
  858. * @param string $sourcePath Local filesystem path to the source image
  859. * @param integer $flags A bitwise combination of:
  860. * File::DELETE_SOURCE Delete the source file, i.e. move
  861. * rather than copy
  862. * @return FileRepoStatus object. On success, the value member contains the
  863. * archive name, or an empty string if it was a new file.
  864. */
  865. function publish( $srcPath, $flags = 0 ) {
  866. $this->lock();
  867. $dstRel = $this->getRel();
  868. $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
  869. $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
  870. $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
  871. $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
  872. if ( $status->value == 'new' ) {
  873. $status->value = '';
  874. } else {
  875. $status->value = $archiveName;
  876. }
  877. $this->unlock();
  878. return $status;
  879. }
  880. /** getLinksTo inherited */
  881. /** getExifData inherited */
  882. /** isLocal inherited */
  883. /** wasDeleted inherited */
  884. /**
  885. * Move file to the new title
  886. *
  887. * Move current, old version and all thumbnails
  888. * to the new filename. Old file is deleted.
  889. *
  890. * Cache purging is done; checks for validity
  891. * and logging are caller's responsibility
  892. *
  893. * @param $target Title New file name
  894. * @return FileRepoStatus object.
  895. */
  896. function move( $target ) {
  897. wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
  898. $this->lock();
  899. $batch = new LocalFileMoveBatch( $this, $target );
  900. $batch->addCurrent();
  901. $batch->addOlds();
  902. $status = $batch->execute();
  903. wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
  904. $this->purgeEverything();
  905. $this->unlock();
  906. if ( $status->isOk() ) {
  907. // Now switch the object
  908. $this->title = $target;
  909. // Force regeneration of the name and hashpath
  910. unset( $this->name );
  911. unset( $this->hashPath );
  912. // Purge the new image
  913. $this->purgeEverything();
  914. }
  915. return $status;
  916. }
  917. /**
  918. * Delete all versions of the file.
  919. *
  920. * Moves the files into an archive directory (or deletes them)
  921. * and removes the database rows.
  922. *
  923. * Cache purging is done; logging is caller's responsibility.
  924. *
  925. * @param $reason
  926. * @param $suppress
  927. * @return FileRepoStatus object.
  928. */
  929. function delete( $reason, $suppress = false ) {
  930. $this->lock();
  931. $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
  932. $batch->addCurrent();
  933. # Get old version relative paths
  934. $dbw = $this->repo->getMasterDB();
  935. $result = $dbw->select( 'oldimage',
  936. array( 'oi_archive_name' ),
  937. array( 'oi_name' => $this->getName() ) );
  938. while ( $row = $dbw->fetchObject( $result ) ) {
  939. $batch->addOld( $row->oi_archive_name );
  940. }
  941. $status = $batch->execute();
  942. if ( $status->ok ) {
  943. // Update site_stats
  944. $site_stats = $dbw->tableName( 'site_stats' );
  945. $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
  946. $this->purgeEverything();
  947. }
  948. $this->unlock();
  949. return $status;
  950. }
  951. /**
  952. * Delete an old version of the file.
  953. *
  954. * Moves the file into an archive directory (or deletes it)
  955. * and removes the database row.
  956. *
  957. * Cache purging is done; logging is caller's responsibility.
  958. *
  959. * @param $reason
  960. * @param $suppress
  961. * @throws MWException or FSException on database or filestore failure
  962. * @return FileRepoStatus object.
  963. */
  964. function deleteOld( $archiveName, $reason, $suppress=false ) {
  965. $this->lock();
  966. $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
  967. $batch->addOld( $archiveName );
  968. $status = $batch->execute();
  969. $this->unlock();
  970. if ( $status->ok ) {
  971. $this->purgeDescription();
  972. $this->purgeHistory();
  973. }
  974. return $status;
  975. }
  976. /**
  977. * Restore all or specified deleted revisions to the given file.
  978. * Permissions and logging are left to the caller.
  979. *
  980. * May throw database exceptions on error.
  981. *
  982. * @param $versions set of record ids of deleted items to restore,
  983. * or empty to restore all revisions.
  984. * @param $unuppress
  985. * @return FileRepoStatus
  986. */
  987. function restore( $versions = array(), $unsuppress = false ) {
  988. $batch = new LocalFileRestoreBatch( $this, $unsuppress );
  989. if ( !$versions ) {
  990. $batch->addAll();
  991. } else {
  992. $batch->addIds( $versions );
  993. }
  994. $status = $batch->execute();
  995. if ( !$status->ok ) {
  996. return $status;
  997. }
  998. $cleanupStatus = $batch->cleanup();
  999. $cleanupStatus->successCount = 0;
  1000. $cleanupStatus->failCount = 0;
  1001. $status->merge( $cleanupStatus );
  1002. return $status;
  1003. }
  1004. /** isMultipage inherited */
  1005. /** pageCount inherited */
  1006. /** scaleHeight inherited */
  1007. /** getImageSize inherited */
  1008. /**
  1009. * Get the URL of the file description page.
  1010. */
  1011. function getDescriptionUrl() {
  1012. return $this->title->getLocalUrl();
  1013. }
  1014. /**
  1015. * Get the HTML text of the description page
  1016. * This is not used by ImagePage for local files, since (among other things)
  1017. * it skips the parser cache.
  1018. */
  1019. function getDescriptionText() {
  1020. global $wgParser;
  1021. $revision = Revision::newFromTitle( $this->title );
  1022. if ( !$revision ) return false;
  1023. $text = $revision->getText();
  1024. if ( !$text ) return false;
  1025. $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
  1026. return $pout->getText();
  1027. }
  1028. function getDescription() {
  1029. $this->load();
  1030. return $this->description;
  1031. }
  1032. function getTimestamp() {
  1033. $this->load();
  1034. return $this->timestamp;
  1035. }
  1036. function getSha1() {
  1037. $this->load();
  1038. // Initialise now if necessary
  1039. if ( $this->sha1 == '' && $this->fileExists ) {
  1040. $this->sha1 = File::sha1Base36( $this->getPath() );
  1041. if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
  1042. $dbw = $this->repo->getMasterDB();
  1043. $dbw->update( 'image',
  1044. array( 'img_sha1' => $this->sha1 ),
  1045. array( 'img_name' => $this->getName() ),
  1046. __METHOD__ );
  1047. $this->saveToCache();
  1048. }
  1049. }
  1050. return $this->sha1;
  1051. }
  1052. /**
  1053. * Start a transaction and lock the image for update
  1054. * Increments a reference counter if the lock is already held
  1055. * @return boolean True if the image exists, false otherwise
  1056. */
  1057. function lock() {
  1058. $dbw = $this->repo->getMasterDB();
  1059. if ( !$this->locked ) {
  1060. $dbw->begin();
  1061. $this->locked++;
  1062. }
  1063. return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
  1064. }
  1065. /**
  1066. * Decrement the lock reference count. If the reference count is reduced to zero, commits
  1067. * the transaction and thereby releases the image lock.
  1068. */
  1069. function unlock() {
  1070. if ( $this->locked ) {
  1071. --$this->locked;
  1072. if ( !$this->locked ) {
  1073. $dbw = $this->repo->getMasterDB();
  1074. $dbw->commit();
  1075. }
  1076. }
  1077. }
  1078. /**
  1079. * Roll back the DB transaction and mark the image unlocked
  1080. */
  1081. function unlockAndRollback() {
  1082. $this->locked = false;
  1083. $dbw = $this->repo->getMasterDB();
  1084. $dbw->rollback();
  1085. }
  1086. } // LocalFile class
  1087. #------------------------------------------------------------------------------
  1088. /**
  1089. * Helper class for file deletion
  1090. * @ingroup FileRepo
  1091. */
  1092. class LocalFileDeleteBatch {
  1093. var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
  1094. var $status;
  1095. function __construct( File $file, $reason = '', $suppress = false ) {
  1096. $this->file = $file;
  1097. $this->reason = $reason;
  1098. $this->suppress = $suppress;
  1099. $this->status = $file->repo->newGood();
  1100. }
  1101. function addCurrent() {
  1102. $this->srcRels['.'] = $this->file->getRel();
  1103. }
  1104. function addOld( $oldName ) {
  1105. $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
  1106. $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
  1107. }
  1108. function getOldRels() {
  1109. if ( !isset( $this->srcRels['.'] ) ) {
  1110. $oldRels =& $this->srcRels;
  1111. $deleteCurrent = false;
  1112. } else {
  1113. $oldRels = $this->srcRels;
  1114. unset( $oldRels['.'] );
  1115. $deleteCurrent = true;
  1116. }
  1117. return array( $oldRels, $deleteCurrent );
  1118. }
  1119. /*protected*/ function getHashes() {
  1120. $hashes = array();
  1121. list( $oldRels, $deleteCurrent ) = $this->getOldRels();
  1122. if ( $deleteCurrent ) {
  1123. $hashes['.'] = $this->file->getSha1();
  1124. }
  1125. if ( count( $oldRels ) ) {
  1126. $dbw = $this->file->repo->getMasterDB();
  1127. $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ),
  1128. 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
  1129. __METHOD__ );
  1130. while ( $row = $dbw->fetchObject( $res ) ) {
  1131. if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
  1132. // Get the hash from the file
  1133. $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
  1134. $props = $this->file->repo->getFileProps( $oldUrl );
  1135. if ( $props['fileExists'] ) {
  1136. // Upgrade the oldimage row
  1137. $dbw->update( 'oldimage',
  1138. array( 'oi_sha1' => $props['sha1'] ),
  1139. array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
  1140. __METHOD__ );
  1141. $hashes[$row->oi_archive_name] = $props['sha1'];
  1142. } else {
  1143. $hashes[$row->oi_archive_name] = false;
  1144. }
  1145. } else {
  1146. $hashes[$row->oi_archive_name] = $row->oi_sha1;
  1147. }
  1148. }
  1149. }
  1150. $missing = array_diff_key( $this->srcRels, $hashes );
  1151. foreach ( $missing as $name => $rel ) {
  1152. $this->status->error( 'filedelete-old-unregistered', $name );
  1153. }
  1154. foreach ( $hashes as $name => $hash ) {
  1155. if ( !$hash ) {
  1156. $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
  1157. unset( $hashes[$name] );
  1158. }
  1159. }
  1160. return $hashes;
  1161. }
  1162. function doDBInserts() {
  1163. global $wgUser;
  1164. $dbw = $this->file->repo->getMasterDB();
  1165. $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
  1166. $encUserId = $dbw->addQuotes( $wgUser->getId() );
  1167. $encReason = $dbw->addQuotes( $this->reason );
  1168. $encGroup = $dbw->addQuotes( 'deleted' );
  1169. $ext = $this->file->getExtension();
  1170. $dotExt = $ext === '' ? '' : ".$ext";
  1171. $encExt = $dbw->addQuotes( $dotExt );
  1172. list( $oldRels, $deleteCurrent ) = $this->getOldRels();
  1173. // Bitfields to further suppress the content
  1174. if ( $this->suppress ) {
  1175. $bitfield = 0;
  1176. // This should be 15...
  1177. $bitfield |= Revision::DELETED_TEXT;
  1178. $bitfield |= Revision::DELETED_COMMENT;
  1179. $bitfield |= Revision::DELETED_USER;
  1180. $bitfield |= Revision::DELETED_RESTRICTED;
  1181. } else {
  1182. $bitfield = 'oi_deleted';
  1183. }
  1184. if ( $deleteCurrent ) {
  1185. $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
  1186. $where = array( 'img_name' => $this->file->getName() );
  1187. $dbw->insertSelect( 'filearchive', 'image',
  1188. array(
  1189. 'fa_storage_group' => $encGroup,
  1190. 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
  1191. 'fa_deleted_user' => $encUserId,
  1192. 'fa_deleted_timestamp' => $encTimestamp,
  1193. 'fa_deleted_reason' => $encReason,
  1194. 'fa_deleted' => $this->suppress ? $bitfield : 0,
  1195. 'fa_name' => 'img_name',
  1196. 'fa_archive_name' => 'NULL',
  1197. 'fa_size' => 'img_size',
  1198. 'fa_width' => 'img_width',
  1199. 'fa_height' => 'img_height',
  1200. 'fa_metadata' => 'img_metadata',
  1201. 'fa_bits' => 'img_bits',
  1202. 'fa_media_type' => 'img_media_type',
  1203. 'fa_major_mime' => 'img_major_mime',
  1204. 'fa_minor_mime' => 'img_minor_mime',
  1205. 'fa_description' => 'img_description',
  1206. 'fa_user' => 'img_user',
  1207. 'fa_user_text' => 'img_user_text',
  1208. 'fa_timestamp' => 'img_timestamp'
  1209. ), $where, __METHOD__ );
  1210. }
  1211. if ( count( $oldRels ) ) {
  1212. $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
  1213. $where = array(
  1214. 'oi_name' => $this->file->getName(),
  1215. 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
  1216. $dbw->insertSelect( 'filearchive', 'oldimage',
  1217. array(
  1218. 'fa_storage_group' => $encGroup,
  1219. 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
  1220. 'fa_deleted_user' => $encUserId,
  1221. 'fa_deleted_timestamp' => $encTimestamp,
  1222. 'fa_deleted_reason' => $encReason,
  1223. 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
  1224. 'fa_name' => 'oi_name',
  1225. 'fa_archive_name' => 'oi_archive_name',
  1226. 'fa_size' => 'oi_size',
  1227. 'fa_width' => 'oi_width',
  1228. 'fa_height' => 'oi_height',
  1229. 'fa_metadata' => 'oi_metadata',
  1230. 'fa_bits' => 'oi_bits',
  1231. 'fa_media_type' => 'oi_media_type',
  1232. 'fa_major_mime' => 'oi_major_mime',
  1233. 'fa_minor_mime' => 'oi_minor_mime',
  1234. 'fa_description' => 'oi_description',
  1235. 'fa_user' => 'oi_user',
  1236. 'fa_user_text' => 'oi_user_text',
  1237. 'fa_timestamp' => 'oi_timestamp',
  1238. 'fa_deleted' => $bitfield
  1239. ), $where, __METHOD__ );
  1240. }
  1241. }
  1242. function doDBDeletes() {
  1243. $dbw = $this->file->repo->getMasterDB();
  1244. list( $oldRels, $deleteCurrent ) = $this->getOldRels();
  1245. if ( count( $oldRels ) ) {
  1246. $dbw->delete( 'oldimage',
  1247. array(
  1248. 'oi_name' => $this->file->getName(),
  1249. 'oi_archive_name' => array_keys( $oldRels )
  1250. ), __METHOD__ );
  1251. }
  1252. if ( $deleteCurrent ) {
  1253. $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
  1254. }
  1255. }
  1256. /**
  1257. * Run the transaction
  1258. */
  1259. function execute() {
  1260. global $wgUser, $wgUseSquid;
  1261. wfProfileIn( __METHOD__ );
  1262. $this->file->lock();
  1263. // Leave private files alone
  1264. $privateFiles = array();
  1265. list( $oldRels, $deleteCurrent ) = $this->getOldRels();
  1266. $dbw = $this->file->repo->getMasterDB();
  1267. if( !empty( $oldRels ) ) {
  1268. $res = $dbw->select( 'oldimage',
  1269. array( 'oi_archive_name' ),
  1270. array( 'oi_name' => $this->file->getName(),
  1271. 'oi_archive_name IN (' . $dbw->makeList( array_keys($oldRels) ) . ')',
  1272. 'oi_deleted & ' . File::DELETED_FILE => File::DELETED_FILE ),
  1273. __METHOD__ );
  1274. while( $row = $dbw->fetchObject( $res ) ) {
  1275. $privateFiles[$row->oi_archive_name] = 1;
  1276. }
  1277. }
  1278. // Prepare deletion batch
  1279. $hashes = $this->getHashes();
  1280. $this->deletionBatch = array();
  1281. $ext = $this->file->getExtension();
  1282. $dotExt = $ext === '' ? '' : ".$ext";
  1283. foreach ( $this->srcRels as $name => $srcRel ) {
  1284. // Skip files that have no hash (missing source).
  1285. // Keep private files where they are.
  1286. if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
  1287. $hash = $hashes[$name];
  1288. $key = $hash . $dotExt;
  1289. $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
  1290. $this->deletionBatch[$name] = array( $srcRel, $dstRel );
  1291. }
  1292. }
  1293. // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
  1294. // We acquire this lock by running the inserts now, before the file operations.
  1295. //
  1296. // This potentially has poor lock contention characteristics -- an alternative
  1297. // scheme would be to insert stub filearchive entries with no fa_name and commit
  1298. // them in a separate transaction, then run the file ops, then update the fa_name fields.
  1299. $this->doDBInserts();
  1300. // Execute the file deletion batch
  1301. $status = $this->file->repo->deleteBatch( $this->deletionBatch );
  1302. if ( !$status->isGood() ) {
  1303. $this->status->merge( $status );
  1304. }
  1305. if ( !$this->status->ok ) {
  1306. // Critical file deletion error
  1307. // Roll back inserts, release lock and abort
  1308. // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
  1309. $this->file->unlockAndRollback();
  1310. return $this->status;
  1311. }
  1312. // Purge squid
  1313. if ( $wgUseSquid ) {
  1314. $urls = array();
  1315. foreach ( $this->srcRels as $srcRel ) {
  1316. $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
  1317. $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
  1318. }
  1319. SquidUpdate::purge( $urls );
  1320. }
  1321. // Delete image/oldimage rows
  1322. $this->doDBDeletes();
  1323. // Commit and return
  1324. $this->file->unlock();
  1325. wfProfileOut( __METHOD__ );
  1326. return $this->status;
  1327. }
  1328. }
  1329. #------------------------------------------------------------------------------
  1330. /**
  1331. * Helper class for file undeletion
  1332. * @ingroup FileRepo
  1333. */
  1334. class LocalFileRestoreBatch {
  1335. var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
  1336. function __construct( File $file, $unsuppress = false ) {
  1337. $this->file = $file;
  1338. $this->cleanupBatch = $this->ids = array();
  1339. $this->ids = array();
  1340. $this->unsuppress = $unsuppress;
  1341. }
  1342. /**
  1343. * Add a file by ID
  1344. */
  1345. function addId( $fa_id ) {
  1346. $this->ids[] = $fa_id;
  1347. }
  1348. /**
  1349. * Add a whole lot of files by ID
  1350. */
  1351. function addIds( $ids ) {
  1352. $this->ids = array_merge( $this->ids, $ids );
  1353. }
  1354. /**
  1355. * Add all revisions of the file
  1356. */
  1357. function addAll() {
  1358. $this->all = true;
  1359. }
  1360. /**
  1361. * Run the transaction, except the cleanup batch.
  1362. * The cleanup batch should be run in a separate transaction, because it locks different
  1363. * rows and there's no need to keep the image row locked while it's acquiring those locks
  1364. * The caller may have its own transaction open.
  1365. * So we save the batch and let the caller call cleanup()
  1366. */
  1367. function execute() {
  1368. global $wgUser, $wgLang;
  1369. if ( !$this->all && !$this->ids ) {
  1370. // Do nothing
  1371. return $this->file->repo->newGood();
  1372. }
  1373. $exists = $this->file->lock();
  1374. $dbw = $this->file->repo->getMasterDB();
  1375. $status = $this->file->repo->newGood();
  1376. // Fetch all or selected archived revisions for the file,
  1377. // sorted from the most recent to the oldest.
  1378. $conditions = array( 'fa_name' => $this->file->getName() );
  1379. if( !$this->all ) {
  1380. $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
  1381. }
  1382. $result = $dbw->select( 'filearchive', '*',
  1383. $conditions,
  1384. __METHOD__,
  1385. array( 'ORDER BY' => 'fa_timestamp DESC' )
  1386. );
  1387. $idsPresent = array();
  1388. $storeBatch = array();
  1389. $insertBatch = array();
  1390. $insertCurrent = false;
  1391. $deleteIds = array();
  1392. $first = true;
  1393. $archiveNames = array();
  1394. while( $row = $dbw->fetchObject( $result ) ) {
  1395. $idsPresent[] = $row->fa_id;
  1396. if ( $row->fa_name != $this->file->getName() ) {
  1397. $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
  1398. $status->failCount++;
  1399. continue;
  1400. }
  1401. if ( $row->fa_storage_key == '' ) {
  1402. // Revision was missing pre-deletion
  1403. $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
  1404. $status->failCount++;
  1405. continue;
  1406. }
  1407. $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
  1408. $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
  1409. $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
  1410. # Fix leading zero
  1411. if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
  1412. $sha1 = substr( $sha1, 1 );
  1413. }
  1414. if( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
  1415. || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
  1416. || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
  1417. || is_null( $row->fa_metadata ) ) {
  1418. // Refresh our metadata
  1419. // Required for a new current revision; nice for older ones too. :)
  1420. $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
  1421. } else {
  1422. $props = array(
  1423. 'minor_mime' => $row->fa_minor_mime,
  1424. 'major_mime' => $row->fa_major_mime,
  1425. 'media_type' => $row->fa_media_type,
  1426. 'metadata' => $row->fa_metadata
  1427. );
  1428. }
  1429. if ( $first && !$exists ) {
  1430. // This revision will be published as the new current version
  1431. $destRel = $this->file->getRel();
  1432. $insertCurrent = array(
  1433. 'img_name' => $row->fa_name,
  1434. 'img_size' => $row->fa_size,
  1435. 'img_width' => $row->fa_width,
  1436. 'img_height' => $row->fa_height,
  1437. 'img_metadata' => $props['metadata'],
  1438. 'img_bits' => $row->fa_bits,
  1439. 'img_media_type' => $props['media_type'],
  1440. 'img_major_mime' => $props['major_mime'],
  1441. 'img_minor_mime' => $props['minor_mime'],
  1442. 'img_description' => $row->fa_description,
  1443. 'img_user' => $row->fa_user,
  1444. 'img_user_text' => $row->fa_user_text,
  1445. 'img_timestamp' => $row->fa_timestamp,
  1446. 'img_sha1' => $sha1
  1447. );
  1448. // The live (current) version cannot be hidden!
  1449. if( !$this->unsuppress && $row->fa_deleted ) {
  1450. $storeBatch[] = array( $deletedUrl, 'public', $destRel );
  1451. $this->cleanupBatch[] = $row->fa_storage_key;
  1452. }
  1453. } else {
  1454. $archiveName = $row->fa_archive_name;
  1455. if( $archiveName == '' ) {
  1456. // This was originally a current version; we
  1457. // have to devise a new archive name for it.
  1458. // Format is <timestamp of archiving>!<name>
  1459. $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
  1460. do {
  1461. $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
  1462. $timestamp++;
  1463. } while ( isset( $archiveNames[$archiveName] ) );
  1464. }
  1465. $archiveNames[$archiveName] = true;
  1466. $destRel = $this->file->getArchiveRel( $archiveName );
  1467. $insertBatch[] = array(
  1468. 'oi_name' => $row->fa_name,
  1469. 'oi_archive_name' => $archiveName,
  1470. 'oi_size' => $row->fa_size,
  1471. 'oi_width' => $row->fa_width,
  1472. 'oi_height' => $row->fa_height,
  1473. 'oi_bits' => $row->fa_bits,
  1474. 'oi_description' => $row->fa_description,
  1475. 'oi_user' => $row->fa_user,
  1476. 'oi_user_text' => $row->fa_user_text,
  1477. 'oi_timestamp' => $row->fa_timestamp,
  1478. 'oi_metadata' => $props['metadata'],
  1479. 'oi_media_type' => $props['media_type'],
  1480. 'oi_major_mime' => $props['major_mime'],
  1481. 'oi_minor_mime' => $props['minor_mime'],
  1482. 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
  1483. 'oi_sha1' => $sha1 );
  1484. }
  1485. $deleteIds[] = $row->fa_id;
  1486. if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
  1487. // private files can stay where they are
  1488. $status->successCount++;
  1489. } else {
  1490. $storeBatch[] = array( $deletedUrl, 'public', $destRel );
  1491. $this->cleanupBatch[] = $row->fa_storage_key;
  1492. }
  1493. $first = false;
  1494. }
  1495. unset( $result );
  1496. // Add a warning to the status object for missing IDs
  1497. $missingIds = array_diff( $this->ids, $idsPresent );
  1498. foreach ( $missingIds as $id ) {
  1499. $status->error( 'undelete-missing-filearchive', $id );
  1500. }
  1501. // Run the store batch
  1502. // Use the OVERWRITE_SAME flag to smooth over a common error
  1503. $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
  1504. $status->merge( $storeStatus );
  1505. if ( !$status->ok ) {
  1506. // Store batch returned a critical error -- this usually means nothing was stored
  1507. // Stop now and return an error
  1508. $this->file->unlock();
  1509. return $status;
  1510. }
  1511. // Run the DB updates
  1512. // Because we have locked the image row, key conflicts should be rare.
  1513. // If they do occur, we can roll back the transaction at this time with
  1514. // no data loss, but leaving unregistered files scattered throughout the
  1515. // public zone.
  1516. // This is not ideal, which is why it's important to lock the image row.
  1517. if ( $insertCurrent ) {
  1518. $dbw->insert( 'image', $insertCurrent, __METHOD__ );
  1519. }
  1520. if ( $insertBatch ) {
  1521. $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
  1522. }
  1523. if ( $deleteIds ) {
  1524. $dbw->delete( 'filearchive',
  1525. array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
  1526. __METHOD__ );
  1527. }
  1528. if( $status->successCount > 0 ) {
  1529. if( !$exists ) {
  1530. wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" );
  1531. // Update site_stats
  1532. $site_stats = $dbw->tableName( 'site_stats' );
  1533. $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
  1534. $this->file->purgeEverything();
  1535. } else {
  1536. wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" );
  1537. $this->file->purgeDescription();
  1538. $this->file->purgeHistory();
  1539. }
  1540. }
  1541. $this->file->unlock();
  1542. return $status;
  1543. }
  1544. /**
  1545. * Delete unused files in the deleted zone.
  1546. * This should be called from outside the transaction in which execute() was called.
  1547. */
  1548. function cleanup() {
  1549. if ( !$this->cleanupBatch ) {
  1550. return $this->file->repo->newGood();
  1551. }
  1552. $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
  1553. return $status;
  1554. }
  1555. }
  1556. #------------------------------------------------------------------------------
  1557. /**
  1558. * Helper class for file movement
  1559. * @ingroup FileRepo
  1560. */
  1561. class LocalFileMoveBatch {
  1562. var $file, $cur, $olds, $oldCount, $archive, $target, $db;
  1563. function __construct( File $file, Title $target ) {
  1564. $this->file = $file;
  1565. $this->target = $target;
  1566. $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
  1567. $this->newHash = $this->file->repo->getHashPath( $this->target->getDBKey() );
  1568. $this->oldName = $this->file->getName();
  1569. $this->newName = $this->file->repo->getNameFromTitle( $this->target );
  1570. $this->oldRel = $this->oldHash . $this->oldName;
  1571. $this->newRel = $this->newHash . $this->newName;
  1572. $this->db = $file->repo->getMasterDb();
  1573. }
  1574. /*
  1575. * Add the current image to the batch
  1576. */
  1577. function addCurrent() {
  1578. $this->cur = array( $this->oldRel, $this->newRel );
  1579. }
  1580. /*
  1581. * Add the old versions of the image to the batch
  1582. */
  1583. function addOlds() {
  1584. $archiveBase = 'archive';
  1585. $this->olds = array();
  1586. $this->oldCount = 0;
  1587. $result = $this->db->select( 'oldimage',
  1588. array( 'oi_archive_name', 'oi_deleted' ),
  1589. array( 'oi_name' => $this->oldName ),
  1590. __METHOD__
  1591. );
  1592. while( $row = $this->db->fetchObject( $result ) ) {
  1593. $oldName = $row->oi_archive_name;
  1594. $bits = explode( '!', $oldName, 2 );
  1595. if( count( $bits ) != 2 ) {
  1596. wfDebug( "Invalid old file name: $oldName \n" );
  1597. continue;
  1598. }
  1599. list( $timestamp, $filename ) = $bits;
  1600. if( $this->oldName != $filename ) {
  1601. wfDebug( "Invalid old file name: $oldName \n" );
  1602. continue;
  1603. }
  1604. $this->oldCount++;
  1605. // Do we want to add those to oldCount?
  1606. if( $row->oi_deleted & File::DELETED_FILE ) {
  1607. continue;
  1608. }
  1609. $this->olds[] = array(
  1610. "{$archiveBase}/{$this->oldHash}{$oldName}",
  1611. "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
  1612. );
  1613. }
  1614. $this->db->freeResult( $result );
  1615. }
  1616. /*
  1617. * Perform the move.
  1618. */
  1619. function execute() {
  1620. $repo = $this->file->repo;
  1621. $status = $repo->newGood();
  1622. $triplets = $this->getMoveTriplets();
  1623. $statusDb = $this->doDBUpdates();
  1624. wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
  1625. $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
  1626. wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
  1627. if( !$statusMove->isOk() ) {
  1628. wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
  1629. $this->db->rollback();
  1630. }
  1631. $status->merge( $statusDb );
  1632. $status->merge( $statusMove );
  1633. return $status;
  1634. }
  1635. /*
  1636. * Do the database updates and return a new WikiError indicating how many
  1637. * rows where updated.
  1638. */
  1639. function doDBUpdates() {
  1640. $repo = $this->file->repo;
  1641. $status = $repo->newGood();
  1642. $dbw = $this->db;
  1643. // Update current image
  1644. $dbw->update(
  1645. 'image',
  1646. array( 'img_name' => $this->newName ),
  1647. array( 'img_name' => $this->oldName ),
  1648. __METHOD__
  1649. );
  1650. if( $dbw->affectedRows() ) {
  1651. $status->successCount++;
  1652. } else {
  1653. $status->failCount++;
  1654. }
  1655. // Update old images
  1656. $dbw->update(
  1657. 'oldimage',
  1658. array(
  1659. 'oi_name' => $this->newName,
  1660. 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ),
  1661. ),
  1662. array( 'oi_name' => $this->oldName ),
  1663. __METHOD__
  1664. );
  1665. $affected = $dbw->affectedRows();
  1666. $total = $this->oldCount;
  1667. $status->successCount += $affected;
  1668. $status->failCount += $total - $affected;
  1669. return $status;
  1670. }
  1671. /*
  1672. * Generate triplets for FSRepo::storeBatch().
  1673. */
  1674. function getMoveTriplets() {
  1675. $moves = array_merge( array( $this->cur ), $this->olds );
  1676. $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
  1677. foreach( $moves as $move ) {
  1678. // $move: (oldRelativePath, newRelativePath)
  1679. $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
  1680. $triplets[] = array( $srcUrl, 'public', $move[1] );
  1681. wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->name}: {$srcUrl} :: public :: {$move[1]}" );
  1682. }
  1683. return $triplets;
  1684. }
  1685. }