SpecialUndelete.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303
  1. <?php
  2. /**
  3. * Special page allowing users with the appropriate permissions to view
  4. * and restore deleted content
  5. *
  6. * @file
  7. * @ingroup SpecialPage
  8. */
  9. /**
  10. * Constructor
  11. */
  12. function wfSpecialUndelete( $par ) {
  13. global $wgRequest;
  14. $form = new UndeleteForm( $wgRequest, $par );
  15. $form->execute();
  16. }
  17. /**
  18. * Used to show archived pages and eventually restore them.
  19. * @ingroup SpecialPage
  20. */
  21. class PageArchive {
  22. protected $title;
  23. var $fileStatus;
  24. function __construct( $title ) {
  25. if( is_null( $title ) ) {
  26. throw new MWException( 'Archiver() given a null title.');
  27. }
  28. $this->title = $title;
  29. }
  30. /**
  31. * List all deleted pages recorded in the archive table. Returns result
  32. * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
  33. * namespace/title.
  34. *
  35. * @return ResultWrapper
  36. */
  37. public static function listAllPages() {
  38. $dbr = wfGetDB( DB_SLAVE );
  39. return self::listPages( $dbr, '' );
  40. }
  41. /**
  42. * List deleted pages recorded in the archive table matching the
  43. * given title prefix.
  44. * Returns result wrapper with (ar_namespace, ar_title, count) fields.
  45. *
  46. * @return ResultWrapper
  47. */
  48. public static function listPagesByPrefix( $prefix ) {
  49. $dbr = wfGetDB( DB_SLAVE );
  50. $title = Title::newFromText( $prefix );
  51. if( $title ) {
  52. $ns = $title->getNamespace();
  53. $encPrefix = $dbr->escapeLike( $title->getDBkey() );
  54. } else {
  55. // Prolly won't work too good
  56. // @todo handle bare namespace names cleanly?
  57. $ns = 0;
  58. $encPrefix = $dbr->escapeLike( $prefix );
  59. }
  60. $conds = array(
  61. 'ar_namespace' => $ns,
  62. "ar_title LIKE '$encPrefix%'",
  63. );
  64. return self::listPages( $dbr, $conds );
  65. }
  66. protected static function listPages( $dbr, $condition ) {
  67. return $dbr->resultObject(
  68. $dbr->select(
  69. array( 'archive' ),
  70. array(
  71. 'ar_namespace',
  72. 'ar_title',
  73. 'COUNT(*) AS count'
  74. ),
  75. $condition,
  76. __METHOD__,
  77. array(
  78. 'GROUP BY' => 'ar_namespace,ar_title',
  79. 'ORDER BY' => 'ar_namespace,ar_title',
  80. 'LIMIT' => 100,
  81. )
  82. )
  83. );
  84. }
  85. /**
  86. * List the revisions of the given page. Returns result wrapper with
  87. * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
  88. *
  89. * @return ResultWrapper
  90. */
  91. function listRevisions() {
  92. $dbr = wfGetDB( DB_SLAVE );
  93. $res = $dbr->select( 'archive',
  94. array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
  95. array( 'ar_namespace' => $this->title->getNamespace(),
  96. 'ar_title' => $this->title->getDBkey() ),
  97. 'PageArchive::listRevisions',
  98. array( 'ORDER BY' => 'ar_timestamp DESC' ) );
  99. $ret = $dbr->resultObject( $res );
  100. return $ret;
  101. }
  102. /**
  103. * List the deleted file revisions for this page, if it's a file page.
  104. * Returns a result wrapper with various filearchive fields, or null
  105. * if not a file page.
  106. *
  107. * @return ResultWrapper
  108. * @todo Does this belong in Image for fuller encapsulation?
  109. */
  110. function listFiles() {
  111. if( $this->title->getNamespace() == NS_FILE ) {
  112. $dbr = wfGetDB( DB_SLAVE );
  113. $res = $dbr->select( 'filearchive',
  114. array(
  115. 'fa_id',
  116. 'fa_name',
  117. 'fa_archive_name',
  118. 'fa_storage_key',
  119. 'fa_storage_group',
  120. 'fa_size',
  121. 'fa_width',
  122. 'fa_height',
  123. 'fa_bits',
  124. 'fa_metadata',
  125. 'fa_media_type',
  126. 'fa_major_mime',
  127. 'fa_minor_mime',
  128. 'fa_description',
  129. 'fa_user',
  130. 'fa_user_text',
  131. 'fa_timestamp',
  132. 'fa_deleted' ),
  133. array( 'fa_name' => $this->title->getDBkey() ),
  134. __METHOD__,
  135. array( 'ORDER BY' => 'fa_timestamp DESC' ) );
  136. $ret = $dbr->resultObject( $res );
  137. return $ret;
  138. }
  139. return null;
  140. }
  141. /**
  142. * Fetch (and decompress if necessary) the stored text for the deleted
  143. * revision of the page with the given timestamp.
  144. *
  145. * @return string
  146. * @deprecated Use getRevision() for more flexible information
  147. */
  148. function getRevisionText( $timestamp ) {
  149. $rev = $this->getRevision( $timestamp );
  150. return $rev ? $rev->getText() : null;
  151. }
  152. /**
  153. * Return a Revision object containing data for the deleted revision.
  154. * Note that the result *may* or *may not* have a null page ID.
  155. * @param string $timestamp
  156. * @return Revision
  157. */
  158. function getRevision( $timestamp ) {
  159. $dbr = wfGetDB( DB_SLAVE );
  160. $row = $dbr->selectRow( 'archive',
  161. array(
  162. 'ar_rev_id',
  163. 'ar_text',
  164. 'ar_comment',
  165. 'ar_user',
  166. 'ar_user_text',
  167. 'ar_timestamp',
  168. 'ar_minor_edit',
  169. 'ar_flags',
  170. 'ar_text_id',
  171. 'ar_deleted',
  172. 'ar_len' ),
  173. array( 'ar_namespace' => $this->title->getNamespace(),
  174. 'ar_title' => $this->title->getDBkey(),
  175. 'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
  176. __METHOD__ );
  177. if( $row ) {
  178. return new Revision( array(
  179. 'page' => $this->title->getArticleId(),
  180. 'id' => $row->ar_rev_id,
  181. 'text' => ($row->ar_text_id
  182. ? null
  183. : Revision::getRevisionText( $row, 'ar_' ) ),
  184. 'comment' => $row->ar_comment,
  185. 'user' => $row->ar_user,
  186. 'user_text' => $row->ar_user_text,
  187. 'timestamp' => $row->ar_timestamp,
  188. 'minor_edit' => $row->ar_minor_edit,
  189. 'text_id' => $row->ar_text_id,
  190. 'deleted' => $row->ar_deleted,
  191. 'len' => $row->ar_len) );
  192. } else {
  193. return null;
  194. }
  195. }
  196. /**
  197. * Return the most-previous revision, either live or deleted, against
  198. * the deleted revision given by timestamp.
  199. *
  200. * May produce unexpected results in case of history merges or other
  201. * unusual time issues.
  202. *
  203. * @param string $timestamp
  204. * @return Revision or null
  205. */
  206. function getPreviousRevision( $timestamp ) {
  207. $dbr = wfGetDB( DB_SLAVE );
  208. // Check the previous deleted revision...
  209. $row = $dbr->selectRow( 'archive',
  210. 'ar_timestamp',
  211. array( 'ar_namespace' => $this->title->getNamespace(),
  212. 'ar_title' => $this->title->getDBkey(),
  213. 'ar_timestamp < ' .
  214. $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
  215. __METHOD__,
  216. array(
  217. 'ORDER BY' => 'ar_timestamp DESC',
  218. 'LIMIT' => 1 ) );
  219. $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
  220. $row = $dbr->selectRow( array( 'page', 'revision' ),
  221. array( 'rev_id', 'rev_timestamp' ),
  222. array(
  223. 'page_namespace' => $this->title->getNamespace(),
  224. 'page_title' => $this->title->getDBkey(),
  225. 'page_id = rev_page',
  226. 'rev_timestamp < ' .
  227. $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
  228. __METHOD__,
  229. array(
  230. 'ORDER BY' => 'rev_timestamp DESC',
  231. 'LIMIT' => 1 ) );
  232. $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
  233. $prevLiveId = $row ? intval( $row->rev_id ) : null;
  234. if( $prevLive && $prevLive > $prevDeleted ) {
  235. // Most prior revision was live
  236. return Revision::newFromId( $prevLiveId );
  237. } elseif( $prevDeleted ) {
  238. // Most prior revision was deleted
  239. return $this->getRevision( $prevDeleted );
  240. } else {
  241. // No prior revision on this page.
  242. return null;
  243. }
  244. }
  245. /**
  246. * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
  247. */
  248. function getTextFromRow( $row ) {
  249. if( is_null( $row->ar_text_id ) ) {
  250. // An old row from MediaWiki 1.4 or previous.
  251. // Text is embedded in this row in classic compression format.
  252. return Revision::getRevisionText( $row, "ar_" );
  253. } else {
  254. // New-style: keyed to the text storage backend.
  255. $dbr = wfGetDB( DB_SLAVE );
  256. $text = $dbr->selectRow( 'text',
  257. array( 'old_text', 'old_flags' ),
  258. array( 'old_id' => $row->ar_text_id ),
  259. __METHOD__ );
  260. return Revision::getRevisionText( $text );
  261. }
  262. }
  263. /**
  264. * Fetch (and decompress if necessary) the stored text of the most
  265. * recently edited deleted revision of the page.
  266. *
  267. * If there are no archived revisions for the page, returns NULL.
  268. *
  269. * @return string
  270. */
  271. function getLastRevisionText() {
  272. $dbr = wfGetDB( DB_SLAVE );
  273. $row = $dbr->selectRow( 'archive',
  274. array( 'ar_text', 'ar_flags', 'ar_text_id' ),
  275. array( 'ar_namespace' => $this->title->getNamespace(),
  276. 'ar_title' => $this->title->getDBkey() ),
  277. 'PageArchive::getLastRevisionText',
  278. array( 'ORDER BY' => 'ar_timestamp DESC' ) );
  279. if( $row ) {
  280. return $this->getTextFromRow( $row );
  281. } else {
  282. return NULL;
  283. }
  284. }
  285. /**
  286. * Quick check if any archived revisions are present for the page.
  287. * @return bool
  288. */
  289. function isDeleted() {
  290. $dbr = wfGetDB( DB_SLAVE );
  291. $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
  292. array( 'ar_namespace' => $this->title->getNamespace(),
  293. 'ar_title' => $this->title->getDBkey() ) );
  294. return ($n > 0);
  295. }
  296. /**
  297. * Restore the given (or all) text and file revisions for the page.
  298. * Once restored, the items will be removed from the archive tables.
  299. * The deletion log will be updated with an undeletion notice.
  300. *
  301. * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
  302. * @param string $comment
  303. * @param array $fileVersions
  304. * @param bool $unsuppress
  305. *
  306. * @return array(number of file revisions restored, number of image revisions restored, log message)
  307. * on success, false on failure
  308. */
  309. function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
  310. // If both the set of text revisions and file revisions are empty,
  311. // restore everything. Otherwise, just restore the requested items.
  312. $restoreAll = empty( $timestamps ) && empty( $fileVersions );
  313. $restoreText = $restoreAll || !empty( $timestamps );
  314. $restoreFiles = $restoreAll || !empty( $fileVersions );
  315. if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
  316. $img = wfLocalFile( $this->title );
  317. $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
  318. $filesRestored = $this->fileStatus->successCount;
  319. } else {
  320. $filesRestored = 0;
  321. }
  322. if( $restoreText ) {
  323. $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
  324. if($textRestored === false) // It must be one of UNDELETE_*
  325. return false;
  326. } else {
  327. $textRestored = 0;
  328. }
  329. // Touch the log!
  330. global $wgContLang;
  331. $log = new LogPage( 'delete' );
  332. if( $textRestored && $filesRestored ) {
  333. $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
  334. $wgContLang->formatNum( $textRestored ),
  335. $wgContLang->formatNum( $filesRestored ) );
  336. } elseif( $textRestored ) {
  337. $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
  338. $wgContLang->formatNum( $textRestored ) );
  339. } elseif( $filesRestored ) {
  340. $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
  341. $wgContLang->formatNum( $filesRestored ) );
  342. } else {
  343. wfDebug( "Undelete: nothing undeleted...\n" );
  344. return false;
  345. }
  346. if( trim( $comment ) != '' )
  347. $reason .= ": {$comment}";
  348. $log->addEntry( 'restore', $this->title, $reason );
  349. return array($textRestored, $filesRestored, $reason);
  350. }
  351. /**
  352. * This is the meaty bit -- restores archived revisions of the given page
  353. * to the cur/old tables. If the page currently exists, all revisions will
  354. * be stuffed into old, otherwise the most recent will go into cur.
  355. *
  356. * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
  357. * @param string $comment
  358. * @param array $fileVersions
  359. * @param bool $unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
  360. *
  361. * @return mixed number of revisions restored or false on failure
  362. */
  363. private function undeleteRevisions( $timestamps, $unsuppress = false ) {
  364. if ( wfReadOnly() )
  365. return false;
  366. $restoreAll = empty( $timestamps );
  367. $dbw = wfGetDB( DB_MASTER );
  368. # Does this page already exist? We'll have to update it...
  369. $article = new Article( $this->title );
  370. $options = 'FOR UPDATE';
  371. $page = $dbw->selectRow( 'page',
  372. array( 'page_id', 'page_latest' ),
  373. array( 'page_namespace' => $this->title->getNamespace(),
  374. 'page_title' => $this->title->getDBkey() ),
  375. __METHOD__,
  376. $options );
  377. if( $page ) {
  378. $makepage = false;
  379. # Page already exists. Import the history, and if necessary
  380. # we'll update the latest revision field in the record.
  381. $newid = 0;
  382. $pageId = $page->page_id;
  383. $previousRevId = $page->page_latest;
  384. # Get the time span of this page
  385. $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
  386. array( 'rev_id' => $previousRevId ),
  387. __METHOD__ );
  388. if( $previousTimestamp === false ) {
  389. wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
  390. return 0;
  391. }
  392. } else {
  393. # Have to create a new article...
  394. $makepage = true;
  395. $previousRevId = 0;
  396. $previousTimestamp = 0;
  397. }
  398. if( $restoreAll ) {
  399. $oldones = '1 = 1'; # All revisions...
  400. } else {
  401. $oldts = implode( ',',
  402. array_map( array( &$dbw, 'addQuotes' ),
  403. array_map( array( &$dbw, 'timestamp' ),
  404. $timestamps ) ) );
  405. $oldones = "ar_timestamp IN ( {$oldts} )";
  406. }
  407. /**
  408. * Select each archived revision...
  409. */
  410. $result = $dbw->select( 'archive',
  411. /* fields */ array(
  412. 'ar_rev_id',
  413. 'ar_text',
  414. 'ar_comment',
  415. 'ar_user',
  416. 'ar_user_text',
  417. 'ar_timestamp',
  418. 'ar_minor_edit',
  419. 'ar_flags',
  420. 'ar_text_id',
  421. 'ar_deleted',
  422. 'ar_page_id',
  423. 'ar_len' ),
  424. /* WHERE */ array(
  425. 'ar_namespace' => $this->title->getNamespace(),
  426. 'ar_title' => $this->title->getDBkey(),
  427. $oldones ),
  428. __METHOD__,
  429. /* options */ array( 'ORDER BY' => 'ar_timestamp' )
  430. );
  431. $ret = $dbw->resultObject( $result );
  432. $rev_count = $dbw->numRows( $result );
  433. if( $makepage ) {
  434. $newid = $article->insertOn( $dbw );
  435. $pageId = $newid;
  436. }
  437. $revision = null;
  438. $restored = 0;
  439. while( $row = $ret->fetchObject() ) {
  440. if( $row->ar_text_id ) {
  441. // Revision was deleted in 1.5+; text is in
  442. // the regular text table, use the reference.
  443. // Specify null here so the so the text is
  444. // dereferenced for page length info if needed.
  445. $revText = null;
  446. } else {
  447. // Revision was deleted in 1.4 or earlier.
  448. // Text is squashed into the archive row, and
  449. // a new text table entry will be created for it.
  450. $revText = Revision::getRevisionText( $row, 'ar_' );
  451. }
  452. // Check for key dupes due to shitty archive integrity.
  453. if( $row->ar_rev_id ) {
  454. $exists = $dbw->selectField( 'revision', '1', array('rev_id' => $row->ar_rev_id), __METHOD__ );
  455. if( $exists ) continue; // don't throw DB errors
  456. }
  457. $revision = new Revision( array(
  458. 'page' => $pageId,
  459. 'id' => $row->ar_rev_id,
  460. 'text' => $revText,
  461. 'comment' => $row->ar_comment,
  462. 'user' => $row->ar_user,
  463. 'user_text' => $row->ar_user_text,
  464. 'timestamp' => $row->ar_timestamp,
  465. 'minor_edit' => $row->ar_minor_edit,
  466. 'text_id' => $row->ar_text_id,
  467. 'deleted' => $unsuppress ? 0 : $row->ar_deleted,
  468. 'len' => $row->ar_len
  469. ) );
  470. $revision->insertOn( $dbw );
  471. $restored++;
  472. wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
  473. }
  474. # Now that it's safely stored, take it out of the archive
  475. $dbw->delete( 'archive',
  476. /* WHERE */ array(
  477. 'ar_namespace' => $this->title->getNamespace(),
  478. 'ar_title' => $this->title->getDBkey(),
  479. $oldones ),
  480. __METHOD__ );
  481. // Was anything restored at all?
  482. if( $restored == 0 )
  483. return 0;
  484. if( $revision ) {
  485. // Attach the latest revision to the page...
  486. $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
  487. if( $newid || $wasnew ) {
  488. // Update site stats, link tables, etc
  489. $article->createUpdates( $revision );
  490. // We don't handle well with top revision deleted
  491. if( $revision->getVisibility() ) {
  492. $dbw->update( 'revision',
  493. array( 'rev_deleted' => 0 ),
  494. array( 'rev_id' => $revision->getId() ),
  495. __METHOD__
  496. );
  497. }
  498. }
  499. if( $newid ) {
  500. wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
  501. Article::onArticleCreate( $this->title );
  502. } else {
  503. wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
  504. Article::onArticleEdit( $this->title );
  505. }
  506. if( $this->title->getNamespace() == NS_FILE ) {
  507. $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
  508. $update->doUpdate();
  509. }
  510. } else {
  511. // Revision couldn't be created. This is very weird
  512. return self::UNDELETE_UNKNOWNERR;
  513. }
  514. return $restored;
  515. }
  516. function getFileStatus() { return $this->fileStatus; }
  517. }
  518. /**
  519. * The HTML form for Special:Undelete, which allows users with the appropriate
  520. * permissions to view and restore deleted content.
  521. * @ingroup SpecialPage
  522. */
  523. class UndeleteForm {
  524. var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mTargetObj;
  525. var $mTargetTimestamp, $mAllowed, $mComment, $mToken;
  526. function UndeleteForm( $request, $par = "" ) {
  527. global $wgUser;
  528. $this->mAction = $request->getVal( 'action' );
  529. $this->mTarget = $request->getVal( 'target' );
  530. $this->mSearchPrefix = $request->getText( 'prefix' );
  531. $time = $request->getVal( 'timestamp' );
  532. $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
  533. $this->mFile = $request->getVal( 'file' );
  534. $posted = $request->wasPosted() &&
  535. $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
  536. $this->mRestore = $request->getCheck( 'restore' ) && $posted;
  537. $this->mInvert = $request->getCheck( 'invert' ) && $posted;
  538. $this->mPreview = $request->getCheck( 'preview' ) && $posted;
  539. $this->mDiff = $request->getCheck( 'diff' );
  540. $this->mComment = $request->getText( 'wpComment' );
  541. $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
  542. $this->mToken = $request->getVal( 'token' );
  543. if( $par != "" ) {
  544. $this->mTarget = $par;
  545. }
  546. if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
  547. $this->mAllowed = true;
  548. } else {
  549. $this->mAllowed = false;
  550. $this->mTimestamp = '';
  551. $this->mRestore = false;
  552. }
  553. if ( $this->mTarget !== "" ) {
  554. $this->mTargetObj = Title::newFromURL( $this->mTarget );
  555. } else {
  556. $this->mTargetObj = NULL;
  557. }
  558. if( $this->mRestore || $this->mInvert ) {
  559. $timestamps = array();
  560. $this->mFileVersions = array();
  561. foreach( $_REQUEST as $key => $val ) {
  562. $matches = array();
  563. if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
  564. array_push( $timestamps, $matches[1] );
  565. }
  566. if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
  567. $this->mFileVersions[] = intval( $matches[1] );
  568. }
  569. }
  570. rsort( $timestamps );
  571. $this->mTargetTimestamp = $timestamps;
  572. }
  573. }
  574. function execute() {
  575. global $wgOut, $wgUser;
  576. if ( $this->mAllowed ) {
  577. $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
  578. } else {
  579. $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
  580. }
  581. if( is_null( $this->mTargetObj ) ) {
  582. # Not all users can just browse every deleted page from the list
  583. if( $wgUser->isAllowed( 'browsearchive' ) ) {
  584. $this->showSearchForm();
  585. # List undeletable articles
  586. if( $this->mSearchPrefix ) {
  587. $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
  588. $this->showList( $result );
  589. }
  590. } else {
  591. $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
  592. }
  593. return;
  594. }
  595. if( $this->mTimestamp !== '' ) {
  596. return $this->showRevision( $this->mTimestamp );
  597. }
  598. if( $this->mFile !== null ) {
  599. $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
  600. // Check if user is allowed to see this file
  601. if( !$file->userCan( File::DELETED_FILE ) ) {
  602. $wgOut->permissionRequired( 'suppressrevision' );
  603. return false;
  604. } elseif ( !$wgUser->matchEditToken( $this->mToken, $this->mFile ) ) {
  605. $this->showFileConfirmationForm( $this->mFile );
  606. return false;
  607. } else {
  608. return $this->showFile( $this->mFile );
  609. }
  610. }
  611. if( $this->mRestore && $this->mAction == "submit" ) {
  612. return $this->undelete();
  613. }
  614. if( $this->mInvert && $this->mAction == "submit" ) {
  615. return $this->showHistory( );
  616. }
  617. return $this->showHistory();
  618. }
  619. function showSearchForm() {
  620. global $wgOut, $wgScript;
  621. $wgOut->addWikiMsg( 'undelete-header' );
  622. $wgOut->addHTML(
  623. Xml::openElement( 'form', array(
  624. 'method' => 'get',
  625. 'action' => $wgScript ) ) .
  626. Xml::fieldset( wfMsg( 'undelete-search-box' ) ) .
  627. Xml::hidden( 'title',
  628. SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
  629. Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
  630. 'prefix', 'prefix', 20,
  631. $this->mSearchPrefix ) . ' ' .
  632. Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
  633. Xml::closeElement( 'fieldset' ) .
  634. Xml::closeElement( 'form' )
  635. );
  636. }
  637. // Generic list of deleted pages
  638. private function showList( $result ) {
  639. global $wgLang, $wgContLang, $wgUser, $wgOut;
  640. if( $result->numRows() == 0 ) {
  641. $wgOut->addWikiMsg( 'undelete-no-results' );
  642. return;
  643. }
  644. $wgOut->addWikiMsg( 'undeletepagetext', $wgLang->formatNum( $result->numRows() ) );
  645. $sk = $wgUser->getSkin();
  646. $undelete = SpecialPage::getTitleFor( 'Undelete' );
  647. $wgOut->addHTML( "<ul>\n" );
  648. while( $row = $result->fetchObject() ) {
  649. $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
  650. $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ),
  651. 'target=' . $title->getPrefixedUrl() );
  652. $revs = wfMsgExt( 'undeleterevisions',
  653. array( 'parseinline' ),
  654. $wgLang->formatNum( $row->count ) );
  655. $wgOut->addHTML( "<li>{$link} ({$revs})</li>\n" );
  656. }
  657. $result->free();
  658. $wgOut->addHTML( "</ul>\n" );
  659. return true;
  660. }
  661. private function showRevision( $timestamp ) {
  662. global $wgLang, $wgUser, $wgOut;
  663. $self = SpecialPage::getTitleFor( 'Undelete' );
  664. $skin = $wgUser->getSkin();
  665. if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
  666. $archive = new PageArchive( $this->mTargetObj );
  667. $rev = $archive->getRevision( $timestamp );
  668. if( !$rev ) {
  669. $wgOut->addWikiMsg( 'undeleterevision-missing' );
  670. return;
  671. }
  672. if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
  673. if( !$rev->userCan(Revision::DELETED_TEXT) ) {
  674. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
  675. return;
  676. } else {
  677. $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
  678. $wgOut->addHTML( '<br/>' );
  679. // and we are allowed to see...
  680. }
  681. }
  682. $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
  683. $link = $skin->makeKnownLinkObj(
  684. SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
  685. htmlspecialchars( $this->mTargetObj->getPrefixedText() )
  686. );
  687. if( $this->mDiff ) {
  688. $previousRev = $archive->getPreviousRevision( $timestamp );
  689. if( $previousRev ) {
  690. $this->showDiff( $previousRev, $rev );
  691. if( $wgUser->getOption( 'diffonly' ) ) {
  692. return;
  693. } else {
  694. $wgOut->addHTML( '<hr />' );
  695. }
  696. } else {
  697. $wgOut->addHTML( wfMsgHtml( 'undelete-nodiff' ) );
  698. }
  699. }
  700. // date and time are separate parameters to facilitate localisation.
  701. // $time is kept for backward compat reasons.
  702. $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
  703. $d = htmlspecialchars( $wgLang->date( $timestamp, true ) );
  704. $t = htmlspecialchars( $wgLang->time( $timestamp, true ) );
  705. $user = $skin->revUserTools( $rev );
  706. $wgOut->addHTML( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' );
  707. wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
  708. if( $this->mPreview ) {
  709. $wgOut->addHTML( "<hr />\n" );
  710. //Hide [edit]s
  711. $popts = $wgOut->parserOptions();
  712. $popts->setEditSection( false );
  713. $wgOut->parserOptions( $popts );
  714. $wgOut->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER ), $this->mTargetObj, true );
  715. }
  716. $wgOut->addHTML(
  717. Xml::element( 'textarea', array(
  718. 'readonly' => 'readonly',
  719. 'cols' => intval( $wgUser->getOption( 'cols' ) ),
  720. 'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
  721. $rev->getText( Revision::FOR_THIS_USER ) . "\n" ) .
  722. Xml::openElement( 'div' ) .
  723. Xml::openElement( 'form', array(
  724. 'method' => 'post',
  725. 'action' => $self->getLocalURL( "action=submit" ) ) ) .
  726. Xml::element( 'input', array(
  727. 'type' => 'hidden',
  728. 'name' => 'target',
  729. 'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
  730. Xml::element( 'input', array(
  731. 'type' => 'hidden',
  732. 'name' => 'timestamp',
  733. 'value' => $timestamp ) ) .
  734. Xml::element( 'input', array(
  735. 'type' => 'hidden',
  736. 'name' => 'wpEditToken',
  737. 'value' => $wgUser->editToken() ) ) .
  738. Xml::element( 'input', array(
  739. 'type' => 'submit',
  740. 'name' => 'preview',
  741. 'value' => wfMsg( 'showpreview' ) ) ) .
  742. Xml::element( 'input', array(
  743. 'name' => 'diff',
  744. 'type' => 'submit',
  745. 'value' => wfMsg( 'showdiff' ) ) ) .
  746. Xml::closeElement( 'form' ) .
  747. Xml::closeElement( 'div' ) );
  748. }
  749. /**
  750. * Build a diff display between this and the previous either deleted
  751. * or non-deleted edit.
  752. * @param Revision $previousRev
  753. * @param Revision $currentRev
  754. * @return string HTML
  755. */
  756. function showDiff( $previousRev, $currentRev ) {
  757. global $wgOut, $wgUser;
  758. $diffEngine = new DifferenceEngine();
  759. $diffEngine->showDiffStyle();
  760. $wgOut->addHTML(
  761. "<div>" .
  762. "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
  763. "<col class='diff-marker' />" .
  764. "<col class='diff-content' />" .
  765. "<col class='diff-marker' />" .
  766. "<col class='diff-content' />" .
  767. "<tr>" .
  768. "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
  769. $this->diffHeader( $previousRev, 'o' ) .
  770. "</td>\n" .
  771. "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
  772. $this->diffHeader( $currentRev, 'n' ) .
  773. "</td>\n" .
  774. "</tr>" .
  775. $diffEngine->generateDiffBody(
  776. $previousRev->getText(), $currentRev->getText() ) .
  777. "</table>" .
  778. "</div>\n" );
  779. }
  780. private function diffHeader( $rev, $prefix ) {
  781. global $wgUser, $wgLang, $wgLang;
  782. $sk = $wgUser->getSkin();
  783. $isDeleted = !( $rev->getId() && $rev->getTitle() );
  784. if( $isDeleted ) {
  785. /// @fixme $rev->getTitle() is null for deleted revs...?
  786. $targetPage = SpecialPage::getTitleFor( 'Undelete' );
  787. $targetQuery = 'target=' .
  788. $this->mTargetObj->getPrefixedUrl() .
  789. '&timestamp=' .
  790. wfTimestamp( TS_MW, $rev->getTimestamp() );
  791. } else {
  792. /// @fixme getId() may return non-zero for deleted revs...
  793. $targetPage = $rev->getTitle();
  794. $targetQuery = 'oldid=' . $rev->getId();
  795. }
  796. return
  797. '<div id="mw-diff-'.$prefix.'title1"><strong>' .
  798. $sk->makeLinkObj( $targetPage,
  799. wfMsgHtml( 'revisionasof',
  800. $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
  801. $targetQuery ) .
  802. ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
  803. '</strong></div>' .
  804. '<div id="mw-diff-'.$prefix.'title2">' .
  805. $sk->revUserTools( $rev ) . '<br/>' .
  806. '</div>' .
  807. '<div id="mw-diff-'.$prefix.'title3">' .
  808. $sk->revComment( $rev ) . '<br/>' .
  809. '</div>';
  810. }
  811. /**
  812. * Show a form confirming whether a tokenless user really wants to see a file
  813. */
  814. private function showFileConfirmationForm( $key ) {
  815. global $wgOut, $wgUser, $wgLang;
  816. $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
  817. $wgOut->addWikiMsg( 'undelete-show-file-confirm',
  818. $this->mTargetObj->getText(),
  819. $wgLang->date( $file->getTimestamp() ),
  820. $wgLang->time( $file->getTimestamp() ) );
  821. $wgOut->addHTML(
  822. Xml::openElement( 'form', array(
  823. 'method' => 'POST',
  824. 'action' => SpecialPage::getTitleFor( 'Undelete' )->getLocalUrl(
  825. 'target=' . urlencode( $this->mTarget ) .
  826. '&file=' . urlencode( $key ) .
  827. '&token=' . urlencode( $wgUser->editToken( $key ) ) )
  828. )
  829. ) .
  830. Xml::submitButton( wfMsg( 'undelete-show-file-submit' ) ) .
  831. '</form>'
  832. );
  833. }
  834. /**
  835. * Show a deleted file version requested by the visitor.
  836. */
  837. private function showFile( $key ) {
  838. global $wgOut, $wgRequest;
  839. $wgOut->disable();
  840. # We mustn't allow the output to be Squid cached, otherwise
  841. # if an admin previews a deleted image, and it's cached, then
  842. # a user without appropriate permissions can toddle off and
  843. # nab the image, and Squid will serve it
  844. $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
  845. $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
  846. $wgRequest->response()->header( 'Pragma: no-cache' );
  847. $store = FileStore::get( 'deleted' );
  848. $store->stream( $key );
  849. }
  850. private function showHistory( ) {
  851. global $wgLang, $wgUser, $wgOut;
  852. $sk = $wgUser->getSkin();
  853. if( $this->mAllowed ) {
  854. $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
  855. } else {
  856. $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
  857. }
  858. $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
  859. $archive = new PageArchive( $this->mTargetObj );
  860. /*
  861. $text = $archive->getLastRevisionText();
  862. if( is_null( $text ) ) {
  863. $wgOut->addWikiMsg( "nohistory" );
  864. return;
  865. }
  866. */
  867. if ( $this->mAllowed ) {
  868. $wgOut->addWikiMsg( "undeletehistory" );
  869. $wgOut->addWikiMsg( "undeleterevdel" );
  870. } else {
  871. $wgOut->addWikiMsg( "undeletehistorynoadmin" );
  872. }
  873. # List all stored revisions
  874. $revisions = $archive->listRevisions();
  875. $files = $archive->listFiles();
  876. $haveRevisions = $revisions && $revisions->numRows() > 0;
  877. $haveFiles = $files && $files->numRows() > 0;
  878. # Batch existence check on user and talk pages
  879. if( $haveRevisions ) {
  880. $batch = new LinkBatch();
  881. while( $row = $revisions->fetchObject() ) {
  882. $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
  883. $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
  884. }
  885. $batch->execute();
  886. $revisions->seek( 0 );
  887. }
  888. if( $haveFiles ) {
  889. $batch = new LinkBatch();
  890. while( $row = $files->fetchObject() ) {
  891. $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
  892. $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
  893. }
  894. $batch->execute();
  895. $files->seek( 0 );
  896. }
  897. if ( $this->mAllowed ) {
  898. $titleObj = SpecialPage::getTitleFor( "Undelete" );
  899. $action = $titleObj->getLocalURL( "action=submit" );
  900. # Start the form here
  901. $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
  902. $wgOut->addHTML( $top );
  903. }
  904. # Show relevant lines from the deletion log:
  905. $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
  906. LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() );
  907. # Show relevant lines from the suppression log:
  908. if( $wgUser->isAllowed( 'suppressionlog' ) ) {
  909. $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'suppress' ) ) . "\n" );
  910. LogEventsList::showLogExtract( $wgOut, 'suppress', $this->mTargetObj->getPrefixedText() );
  911. }
  912. if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
  913. # Format the user-visible controls (comment field, submission button)
  914. # in a nice little table
  915. if( $wgUser->isAllowed( 'suppressrevision' ) ) {
  916. $unsuppressBox =
  917. "<tr>
  918. <td>&nbsp;</td>
  919. <td class='mw-input'>" .
  920. Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
  921. 'mw-undelete-unsuppress', $this->mUnsuppress ).
  922. "</td>
  923. </tr>";
  924. } else {
  925. $unsuppressBox = "";
  926. }
  927. $table =
  928. Xml::fieldset( wfMsg( 'undelete-fieldset-title' ) ) .
  929. Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
  930. "<tr>
  931. <td colspan='2'>" .
  932. wfMsgWikiHtml( 'undeleteextrahelp' ) .
  933. "</td>
  934. </tr>
  935. <tr>
  936. <td class='mw-label'>" .
  937. Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
  938. "</td>
  939. <td class='mw-input'>" .
  940. Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
  941. "</td>
  942. </tr>
  943. <tr>
  944. <td>&nbsp;</td>
  945. <td class='mw-submit'>" .
  946. Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . ' ' .
  947. Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) . ' ' .
  948. Xml::submitButton( wfMsg( 'undeleteinvert' ), array( 'name' => 'invert', 'id' => 'mw-undelete-invert' ) ) .
  949. "</td>
  950. </tr>" .
  951. $unsuppressBox .
  952. Xml::closeElement( 'table' ) .
  953. Xml::closeElement( 'fieldset' );
  954. $wgOut->addHTML( $table );
  955. }
  956. $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
  957. if( $haveRevisions ) {
  958. # The page's stored (deleted) history:
  959. $wgOut->addHTML("<ul>");
  960. $target = urlencode( $this->mTarget );
  961. $remaining = $revisions->numRows();
  962. $earliestLiveTime = $this->mTargetObj->getEarliestRevTime();
  963. while( $row = $revisions->fetchObject() ) {
  964. $remaining--;
  965. $wgOut->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) );
  966. }
  967. $revisions->free();
  968. $wgOut->addHTML("</ul>");
  969. } else {
  970. $wgOut->addWikiMsg( "nohistory" );
  971. }
  972. if( $haveFiles ) {
  973. $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
  974. $wgOut->addHTML( "<ul>" );
  975. while( $row = $files->fetchObject() ) {
  976. $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
  977. }
  978. $files->free();
  979. $wgOut->addHTML( "</ul>" );
  980. }
  981. if ( $this->mAllowed ) {
  982. # Slip in the hidden controls here
  983. $misc = Xml::hidden( 'target', $this->mTarget );
  984. $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
  985. $misc .= Xml::closeElement( 'form' );
  986. $wgOut->addHTML( $misc );
  987. }
  988. return true;
  989. }
  990. private function formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) {
  991. global $wgUser, $wgLang;
  992. $rev = new Revision( array(
  993. 'page' => $this->mTargetObj->getArticleId(),
  994. 'comment' => $row->ar_comment,
  995. 'user' => $row->ar_user,
  996. 'user_text' => $row->ar_user_text,
  997. 'timestamp' => $row->ar_timestamp,
  998. 'minor_edit' => $row->ar_minor_edit,
  999. 'deleted' => $row->ar_deleted,
  1000. 'len' => $row->ar_len ) );
  1001. $stxt = '';
  1002. $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
  1003. if( $this->mAllowed ) {
  1004. if( $this->mInvert){
  1005. if( in_array( $ts, $this->mTargetTimestamp ) ) {
  1006. $checkBox = Xml::check( "ts$ts");
  1007. } else {
  1008. $checkBox = Xml::check( "ts$ts", true );
  1009. }
  1010. } else {
  1011. $checkBox = Xml::check( "ts$ts" );
  1012. }
  1013. $titleObj = SpecialPage::getTitleFor( "Undelete" );
  1014. $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
  1015. # Last link
  1016. if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
  1017. $last = wfMsgHtml('diff');
  1018. } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
  1019. $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
  1020. "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
  1021. } else {
  1022. $last = wfMsgHtml('diff');
  1023. }
  1024. } else {
  1025. $checkBox = '';
  1026. $pageLink = $wgLang->timeanddate( $ts, true );
  1027. $last = wfMsgHtml('diff');
  1028. }
  1029. $userLink = $sk->revUserTools( $rev );
  1030. if(!is_null($size = $row->ar_len)) {
  1031. $stxt = $sk->formatRevisionSize( $size );
  1032. }
  1033. $comment = $sk->revComment( $rev );
  1034. $revdlink = '';
  1035. if( $wgUser->isAllowed( 'deleterevision' ) ) {
  1036. if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
  1037. // If revision was hidden from sysops
  1038. $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' );
  1039. } else {
  1040. $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(),
  1041. 'artimestamp[]' => $ts
  1042. );
  1043. $revdlink = $sk->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
  1044. }
  1045. }
  1046. return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
  1047. }
  1048. private function formatFileRow( $row, $sk ) {
  1049. global $wgUser, $wgLang;
  1050. $file = ArchivedFile::newFromRow( $row );
  1051. $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
  1052. if( $this->mAllowed && $row->fa_storage_key ) {
  1053. $checkBox = Xml::check( "fileid" . $row->fa_id );
  1054. $key = urlencode( $row->fa_storage_key );
  1055. $target = urlencode( $this->mTarget );
  1056. $titleObj = SpecialPage::getTitleFor( "Undelete" );
  1057. $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
  1058. } else {
  1059. $checkBox = '';
  1060. $pageLink = $wgLang->timeanddate( $ts, true );
  1061. }
  1062. $userLink = $this->getFileUser( $file, $sk );
  1063. $data =
  1064. wfMsg( 'widthheight',
  1065. $wgLang->formatNum( $row->fa_width ),
  1066. $wgLang->formatNum( $row->fa_height ) ) .
  1067. ' (' .
  1068. wfMsg( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
  1069. ')';
  1070. $data = htmlspecialchars( $data );
  1071. $comment = $this->getFileComment( $file, $sk );
  1072. $revdlink = '';
  1073. if( $wgUser->isAllowed( 'deleterevision' ) ) {
  1074. if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
  1075. // If revision was hidden from sysops
  1076. $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' );
  1077. } else {
  1078. $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(),
  1079. 'fileid' => $row->fa_id
  1080. );
  1081. $revdlink = $sk->revDeleteLink( $query, $file->isDeleted( File::DELETED_RESTRICTED ) );
  1082. }
  1083. }
  1084. return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
  1085. }
  1086. /**
  1087. * Fetch revision text link if it's available to all users
  1088. * @return string
  1089. */
  1090. function getPageLink( $rev, $titleObj, $ts, $sk ) {
  1091. global $wgLang;
  1092. if( !$rev->userCan(Revision::DELETED_TEXT) ) {
  1093. return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
  1094. } else {
  1095. $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
  1096. "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
  1097. if( $rev->isDeleted(Revision::DELETED_TEXT) )
  1098. $link = '<span class="history-deleted">' . $link . '</span>';
  1099. return $link;
  1100. }
  1101. }
  1102. /**
  1103. * Fetch image view link if it's available to all users
  1104. * @return string
  1105. */
  1106. function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
  1107. global $wgLang, $wgUser;
  1108. if( !$file->userCan(File::DELETED_FILE) ) {
  1109. return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
  1110. } else {
  1111. $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
  1112. "target=".$this->mTargetObj->getPrefixedUrl().
  1113. "&file=$key" .
  1114. "&token=" . urlencode( $wgUser->editToken( $key ) ) );
  1115. if( $file->isDeleted(File::DELETED_FILE) )
  1116. $link = '<span class="history-deleted">' . $link . '</span>';
  1117. return $link;
  1118. }
  1119. }
  1120. /**
  1121. * Fetch file's user id if it's available to this user
  1122. * @return string
  1123. */
  1124. function getFileUser( $file, $sk ) {
  1125. if( !$file->userCan(File::DELETED_USER) ) {
  1126. return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
  1127. } else {
  1128. $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) .
  1129. $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
  1130. if( $file->isDeleted(File::DELETED_USER) )
  1131. $link = '<span class="history-deleted">' . $link . '</span>';
  1132. return $link;
  1133. }
  1134. }
  1135. /**
  1136. * Fetch file upload comment if it's available to this user
  1137. * @return string
  1138. */
  1139. function getFileComment( $file, $sk ) {
  1140. if( !$file->userCan(File::DELETED_COMMENT) ) {
  1141. return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
  1142. } else {
  1143. $link = $sk->commentBlock( $file->getRawDescription() );
  1144. if( $file->isDeleted(File::DELETED_COMMENT) )
  1145. $link = '<span class="history-deleted">' . $link . '</span>';
  1146. return $link;
  1147. }
  1148. }
  1149. function undelete() {
  1150. global $wgOut, $wgUser;
  1151. if ( wfReadOnly() ) {
  1152. $wgOut->readOnlyPage();
  1153. return;
  1154. }
  1155. if( !is_null( $this->mTargetObj ) ) {
  1156. $archive = new PageArchive( $this->mTargetObj );
  1157. $ok = $archive->undelete(
  1158. $this->mTargetTimestamp,
  1159. $this->mComment,
  1160. $this->mFileVersions,
  1161. $this->mUnsuppress );
  1162. if( is_array($ok) ) {
  1163. if ( $ok[1] ) // Undeleted file count
  1164. wfRunHooks( 'FileUndeleteComplete', array(
  1165. $this->mTargetObj, $this->mFileVersions,
  1166. $wgUser, $this->mComment) );
  1167. $skin = $wgUser->getSkin();
  1168. $link = $skin->makeKnownLinkObj( $this->mTargetObj );
  1169. $wgOut->addHTML( wfMsgWikiHtml( 'undeletedpage', $link ) );
  1170. } else {
  1171. $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
  1172. $wgOut->addHTML( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
  1173. }
  1174. // Show file deletion warnings and errors
  1175. $status = $archive->getFileStatus();
  1176. if( $status && !$status->isGood() ) {
  1177. $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
  1178. }
  1179. } else {
  1180. $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
  1181. }
  1182. return false;
  1183. }
  1184. }