SpecialRevisiondelete.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517
  1. <?php
  2. /**
  3. * Special page allowing users with the appropriate permissions to view
  4. * and hide revisions. Log items can also be hidden.
  5. *
  6. * @file
  7. * @ingroup SpecialPage
  8. */
  9. class SpecialRevisionDelete extends UnlistedSpecialPage {
  10. public function __construct() {
  11. parent::__construct( 'Revisiondelete', 'deleterevision' );
  12. $this->includable( false );
  13. }
  14. public function execute( $par ) {
  15. global $wgOut, $wgUser, $wgRequest;
  16. if( wfReadOnly() ) {
  17. $wgOut->readOnlyPage();
  18. return;
  19. }
  20. if( !$wgUser->isAllowed( 'deleterevision' ) ) {
  21. $wgOut->permissionRequired( 'deleterevision' );
  22. return;
  23. }
  24. $this->skin =& $wgUser->getSkin();
  25. # Set title and such
  26. $this->setHeaders();
  27. $this->outputHeader();
  28. $this->wasPosted = $wgRequest->wasPosted();
  29. # Handle our many different possible input types
  30. $this->target = $wgRequest->getText( 'target' );
  31. $this->oldids = $wgRequest->getArray( 'oldid' );
  32. $this->artimestamps = $wgRequest->getArray( 'artimestamp' );
  33. $this->logids = $wgRequest->getArray( 'logid' );
  34. $this->oldimgs = $wgRequest->getArray( 'oldimage' );
  35. $this->fileids = $wgRequest->getArray( 'fileid' );
  36. # For reviewing deleted files...
  37. $this->file = $wgRequest->getVal( 'file' );
  38. # Only one target set at a time please!
  39. $i = (bool)$this->file + (bool)$this->oldids + (bool)$this->logids
  40. + (bool)$this->artimestamps + (bool)$this->fileids + (bool)$this->oldimgs;
  41. # No targets?
  42. if( $i == 0 ) {
  43. $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
  44. return;
  45. }
  46. # Too many targets?
  47. if( $i !== 1 ) {
  48. $wgOut->showErrorPage( 'revdelete-toomanytargets-title', 'revdelete-toomanytargets-text' );
  49. return;
  50. }
  51. $this->page = Title::newFromUrl( $this->target );
  52. # If we have revisions, get the title from the first one
  53. # since they should all be from the same page. This allows
  54. # for more flexibility with page moves...
  55. if( count($this->oldids) > 0 ) {
  56. $rev = Revision::newFromId( $this->oldids[0] );
  57. $this->page = $rev ? $rev->getTitle() : $this->page;
  58. }
  59. # We need a target page!
  60. if( is_null($this->page) ) {
  61. $wgOut->addWikiMsg( 'undelete-header' );
  62. return;
  63. }
  64. # Logs must have a type given
  65. if( $this->logids && !strpos($this->page->getDBKey(),'/') ) {
  66. $wgOut->showErrorPage( 'revdelete-nologtype-title', 'revdelete-nologtype-text' );
  67. return;
  68. }
  69. # For reviewing deleted files...show it now if allowed
  70. if( $this->file ) {
  71. $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->page, $this->file );
  72. $oimage->load();
  73. // Check if user is allowed to see this file
  74. if( !$oimage->userCan(File::DELETED_FILE) ) {
  75. $wgOut->permissionRequired( 'suppressrevision' );
  76. } else {
  77. $this->showFile( $this->file );
  78. }
  79. return;
  80. }
  81. # Give a link to the logs/hist for this page
  82. if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
  83. $links = array();
  84. $logtitle = SpecialPage::getTitleFor( 'Log' );
  85. $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
  86. wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) );
  87. # Give a link to the page history
  88. $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ),
  89. wfArrayToCGI( array( 'action' => 'history' ) ) );
  90. # Link to deleted edits
  91. if( $wgUser->isAllowed('undelete') ) {
  92. $undelete = SpecialPage::getTitleFor( 'Undelete' );
  93. $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ),
  94. wfArrayToCGI( array( 'target' => $this->page->getPrefixedDBkey() ) ) );
  95. }
  96. # Logs themselves don't have histories or archived revisions
  97. $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
  98. }
  99. # Lock the operation and the form context
  100. $this->secureOperation();
  101. # Either submit or create our form
  102. if( $this->wasPosted ) {
  103. $this->submit( $wgRequest );
  104. } else if( $this->deleteKey == 'oldid' || $this->deleteKey == 'artimestamp' ) {
  105. $this->showRevs();
  106. } else if( $this->deleteKey == 'fileid' || $this->deleteKey == 'oldimage' ) {
  107. $this->showImages();
  108. } else if( $this->deleteKey == 'logid' ) {
  109. $this->showLogItems();
  110. }
  111. # Show relevant lines from the deletion log. This will show even if said ID
  112. # does not exist...might be helpful
  113. $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
  114. LogEventsList::showLogExtract( $wgOut, 'delete', $this->page->getPrefixedText() );
  115. if( $wgUser->isAllowed( 'suppressionlog' ) ){
  116. $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
  117. LogEventsList::showLogExtract( $wgOut, 'suppress', $this->page->getPrefixedText() );
  118. }
  119. }
  120. private function secureOperation() {
  121. global $wgUser;
  122. $this->deleteKey = '';
  123. // At this point, we should only have one of these
  124. if( $this->oldids ) {
  125. $this->revisions = $this->oldids;
  126. $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
  127. $this->deleteKey = 'oldid';
  128. } else if( $this->artimestamps ) {
  129. $this->archrevs = $this->artimestamps;
  130. $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
  131. $this->deleteKey = 'artimestamp';
  132. } else if( $this->oldimgs ) {
  133. $this->ofiles = $this->oldimgs;
  134. $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
  135. $this->deleteKey = 'oldimage';
  136. } else if( $this->fileids ) {
  137. $this->afiles = $this->fileids;
  138. $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
  139. $this->deleteKey = 'fileid';
  140. } else if( $this->logids ) {
  141. $this->events = $this->logids;
  142. $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION );
  143. $this->deleteKey = 'logid';
  144. }
  145. // Our checkbox messages depends one what we are doing,
  146. // e.g. we don't hide "text" for logs or images
  147. $this->checks = array(
  148. $hide_content_name,
  149. array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
  150. array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER )
  151. );
  152. if( $wgUser->isAllowed('suppressrevision') ) {
  153. $this->checks[] = array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED );
  154. }
  155. }
  156. /**
  157. * Show a deleted file version requested by the visitor.
  158. */
  159. private function showFile( $key ) {
  160. global $wgOut, $wgRequest;
  161. $wgOut->disable();
  162. # We mustn't allow the output to be Squid cached, otherwise
  163. # if an admin previews a deleted image, and it's cached, then
  164. # a user without appropriate permissions can toddle off and
  165. # nab the image, and Squid will serve it
  166. $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
  167. $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
  168. $wgRequest->response()->header( 'Pragma: no-cache' );
  169. $store = FileStore::get( 'deleted' );
  170. $store->stream( $key );
  171. }
  172. /**
  173. * This lets a user set restrictions for live and archived revisions
  174. */
  175. private function showRevs() {
  176. global $wgOut, $wgUser;
  177. $UserAllowed = true;
  178. $count = ($this->deleteKey=='oldid') ?
  179. count($this->revisions) : count($this->archrevs);
  180. $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
  181. $bitfields = 0;
  182. $wgOut->addHTML( "<ul>" );
  183. $where = $revObjs = array();
  184. $dbr = wfGetDB( DB_MASTER );
  185. $revisions = 0;
  186. // Live revisions...
  187. if( $this->deleteKey=='oldid' ) {
  188. // Run through and pull all our data in one query
  189. foreach( $this->revisions as $revid ) {
  190. $where[] = intval($revid);
  191. }
  192. $result = $dbr->select( array('revision','page'), '*',
  193. array(
  194. 'rev_page' => $this->page->getArticleID(),
  195. 'rev_id' => $where,
  196. 'rev_page = page_id' ),
  197. __METHOD__ );
  198. while( $row = $dbr->fetchObject( $result ) ) {
  199. $revObjs[$row->rev_id] = new Revision( $row );
  200. }
  201. foreach( $this->revisions as $revid ) {
  202. // Hiding top revisison is bad
  203. if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
  204. continue;
  205. } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
  206. // If a rev is hidden from sysops
  207. if( !$this->wasPosted ) {
  208. $wgOut->permissionRequired( 'suppressrevision' );
  209. return;
  210. }
  211. $UserAllowed = false;
  212. }
  213. $revisions++;
  214. $wgOut->addHTML( $this->historyLine( $revObjs[$revid] ) );
  215. $bitfields |= $revObjs[$revid]->mDeleted;
  216. }
  217. // The archives...
  218. } else {
  219. // Run through and pull all our data in one query
  220. foreach( $this->archrevs as $timestamp ) {
  221. $where[] = $dbr->timestamp( $timestamp );
  222. }
  223. $result = $dbr->select( 'archive', '*',
  224. array(
  225. 'ar_namespace' => $this->page->getNamespace(),
  226. 'ar_title' => $this->page->getDBKey(),
  227. 'ar_timestamp' => $where ),
  228. __METHOD__ );
  229. while( $row = $dbr->fetchObject( $result ) ) {
  230. $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
  231. $revObjs[$timestamp] = new Revision( array(
  232. 'page' => $this->page->getArticleId(),
  233. 'id' => $row->ar_rev_id,
  234. 'text' => $row->ar_text_id,
  235. 'comment' => $row->ar_comment,
  236. 'user' => $row->ar_user,
  237. 'user_text' => $row->ar_user_text,
  238. 'timestamp' => $timestamp,
  239. 'minor_edit' => $row->ar_minor_edit,
  240. 'text_id' => $row->ar_text_id,
  241. 'deleted' => $row->ar_deleted,
  242. 'len' => $row->ar_len) );
  243. }
  244. foreach( $this->archrevs as $timestamp ) {
  245. if( !isset($revObjs[$timestamp]) ) {
  246. continue;
  247. } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
  248. // If a rev is hidden from sysops
  249. if( !$this->wasPosted ) {
  250. $wgOut->permissionRequired( 'suppressrevision' );
  251. return;
  252. }
  253. $UserAllowed = false;
  254. }
  255. $revisions++;
  256. $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) );
  257. $bitfields |= $revObjs[$timestamp]->mDeleted;
  258. }
  259. }
  260. if( !$revisions ) {
  261. $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
  262. return;
  263. }
  264. $wgOut->addHTML( "</ul>" );
  265. // Explanation text
  266. $this->addUsageText();
  267. // Normal sysops can always see what they did, but can't always change it
  268. if( !$UserAllowed ) return;
  269. $items = array(
  270. Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
  271. Xml::submitButton( wfMsg( 'revdelete-submit' ) )
  272. );
  273. $hidden = array(
  274. Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
  275. Xml::hidden( 'target', $this->page->getPrefixedText() ),
  276. Xml::hidden( 'type', $this->deleteKey )
  277. );
  278. if( $this->deleteKey=='oldid' ) {
  279. foreach( $revObjs as $rev )
  280. $hidden[] = Xml::hidden( 'oldid[]', $rev->getId() );
  281. } else {
  282. foreach( $revObjs as $rev )
  283. $hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() );
  284. }
  285. $special = SpecialPage::getTitleFor( 'Revisiondelete' );
  286. $wgOut->addHTML(
  287. Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
  288. 'id' => 'mw-revdel-form-revisions' ) ) .
  289. Xml::openElement( 'fieldset' ) .
  290. xml::element( 'legend', null, wfMsg( 'revdelete-legend' ) )
  291. );
  292. $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
  293. foreach( $items as $item ) {
  294. $wgOut->addHTML( Xml::tags( 'p', null, $item ) );
  295. }
  296. foreach( $hidden as $item ) {
  297. $wgOut->addHTML( $item );
  298. }
  299. $wgOut->addHTML(
  300. Xml::closeElement( 'fieldset' ) .
  301. Xml::closeElement( 'form' ) . "\n"
  302. );
  303. }
  304. /**
  305. * This lets a user set restrictions for archived images
  306. */
  307. private function showImages() {
  308. global $wgOut, $wgUser, $wgLang;
  309. $UserAllowed = true;
  310. $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles);
  311. $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(),
  312. $wgLang->formatNum($count) );
  313. $bitfields = 0;
  314. $wgOut->addHTML( "<ul>" );
  315. $where = $filesObjs = array();
  316. $dbr = wfGetDB( DB_MASTER );
  317. // Live old revisions...
  318. $revisions = 0;
  319. if( $this->deleteKey=='oldimage' ) {
  320. // Run through and pull all our data in one query
  321. foreach( $this->ofiles as $timestamp ) {
  322. $where[] = $timestamp.'!'.$this->page->getDBKey();
  323. }
  324. $result = $dbr->select( 'oldimage', '*',
  325. array(
  326. 'oi_name' => $this->page->getDBKey(),
  327. 'oi_archive_name' => $where ),
  328. __METHOD__ );
  329. while( $row = $dbr->fetchObject( $result ) ) {
  330. $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
  331. $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
  332. $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
  333. }
  334. // Check through our images
  335. foreach( $this->ofiles as $timestamp ) {
  336. $archivename = $timestamp.'!'.$this->page->getDBKey();
  337. if( !isset($filesObjs[$archivename]) ) {
  338. continue;
  339. } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
  340. // If a rev is hidden from sysops
  341. if( !$this->wasPosted ) {
  342. $wgOut->permissionRequired( 'suppressrevision' );
  343. return;
  344. }
  345. $UserAllowed = false;
  346. }
  347. $revisions++;
  348. // Inject history info
  349. $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) );
  350. $bitfields |= $filesObjs[$archivename]->deleted;
  351. }
  352. // Archived files...
  353. } else {
  354. // Run through and pull all our data in one query
  355. foreach( $this->afiles as $id ) {
  356. $where[] = intval($id);
  357. }
  358. $result = $dbr->select( 'filearchive', '*',
  359. array(
  360. 'fa_name' => $this->page->getDBKey(),
  361. 'fa_id' => $where ),
  362. __METHOD__ );
  363. while( $row = $dbr->fetchObject( $result ) ) {
  364. $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
  365. }
  366. foreach( $this->afiles as $fileid ) {
  367. if( !isset($filesObjs[$fileid]) ) {
  368. continue;
  369. } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
  370. // If a rev is hidden from sysops
  371. if( !$this->wasPosted ) {
  372. $wgOut->permissionRequired( 'suppressrevision' );
  373. return;
  374. }
  375. $UserAllowed = false;
  376. }
  377. $revisions++;
  378. // Inject history info
  379. $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) );
  380. $bitfields |= $filesObjs[$fileid]->deleted;
  381. }
  382. }
  383. if( !$revisions ) {
  384. $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
  385. return;
  386. }
  387. $wgOut->addHTML( "</ul>" );
  388. // Explanation text
  389. $this->addUsageText();
  390. // Normal sysops can always see what they did, but can't always change it
  391. if( !$UserAllowed ) return;
  392. $items = array(
  393. Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
  394. Xml::submitButton( wfMsg( 'revdelete-submit' ) )
  395. );
  396. $hidden = array(
  397. Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
  398. Xml::hidden( 'target', $this->page->getPrefixedText() ),
  399. Xml::hidden( 'type', $this->deleteKey )
  400. );
  401. if( $this->deleteKey=='oldimage' ) {
  402. foreach( $this->ofiles as $filename )
  403. $hidden[] = Xml::hidden( 'oldimage[]', $filename );
  404. } else {
  405. foreach( $this->afiles as $fileid )
  406. $hidden[] = Xml::hidden( 'fileid[]', $fileid );
  407. }
  408. $special = SpecialPage::getTitleFor( 'Revisiondelete' );
  409. $wgOut->addHTML(
  410. Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
  411. 'id' => 'mw-revdel-form-filerevisions' ) ) .
  412. Xml::fieldset( wfMsg( 'revdelete-legend' ) )
  413. );
  414. $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
  415. foreach( $items as $item ) {
  416. $wgOut->addHTML( "<p>$item</p>" );
  417. }
  418. foreach( $hidden as $item ) {
  419. $wgOut->addHTML( $item );
  420. }
  421. $wgOut->addHTML(
  422. Xml::closeElement( 'fieldset' ) .
  423. Xml::closeElement( 'form' ) . "\n"
  424. );
  425. }
  426. /**
  427. * This lets a user set restrictions for log items
  428. */
  429. private function showLogItems() {
  430. global $wgOut, $wgUser, $wgMessageCache, $wgLang;
  431. $UserAllowed = true;
  432. $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
  433. $bitfields = 0;
  434. $wgOut->addHTML( "<ul>" );
  435. $where = $logRows = array();
  436. $dbr = wfGetDB( DB_MASTER );
  437. // Run through and pull all our data in one query
  438. $logItems = 0;
  439. foreach( $this->events as $logid ) {
  440. $where[] = intval($logid);
  441. }
  442. list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
  443. $result = $dbr->select( 'logging', '*',
  444. array(
  445. 'log_type' => $logtype,
  446. 'log_id' => $where ),
  447. __METHOD__ );
  448. while( $row = $dbr->fetchObject( $result ) ) {
  449. $logRows[$row->log_id] = $row;
  450. }
  451. $wgMessageCache->loadAllMessages();
  452. foreach( $this->events as $logid ) {
  453. // Don't hide from oversight log!!!
  454. if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) {
  455. continue;
  456. } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
  457. // If an event is hidden from sysops
  458. if( !$this->wasPosted ) {
  459. $wgOut->permissionRequired( 'suppressrevision' );
  460. return;
  461. }
  462. $UserAllowed = false;
  463. }
  464. $logItems++;
  465. $wgOut->addHTML( $this->logLine( $logRows[$logid] ) );
  466. $bitfields |= $logRows[$logid]->log_deleted;
  467. }
  468. if( !$logItems ) {
  469. $wgOut->showErrorPage( 'revdelete-nologid-title', 'revdelete-nologid-text' );
  470. return;
  471. }
  472. $wgOut->addHTML( "</ul>" );
  473. // Explanation text
  474. $this->addUsageText();
  475. // Normal sysops can always see what they did, but can't always change it
  476. if( !$UserAllowed ) return;
  477. $items = array(
  478. Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
  479. Xml::submitButton( wfMsg( 'revdelete-submit' ) ) );
  480. $hidden = array(
  481. Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
  482. Xml::hidden( 'target', $this->page->getPrefixedText() ),
  483. Xml::hidden( 'type', $this->deleteKey ) );
  484. foreach( $this->events as $logid ) {
  485. $hidden[] = Xml::hidden( 'logid[]', $logid );
  486. }
  487. $special = SpecialPage::getTitleFor( 'Revisiondelete' );
  488. $wgOut->addHTML(
  489. Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ),
  490. 'id' => 'mw-revdel-form-logs' ) ) .
  491. Xml::fieldset( wfMsg( 'revdelete-legend' ) )
  492. );
  493. $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
  494. foreach( $items as $item ) {
  495. $wgOut->addHTML( "<p>$item</p>" );
  496. }
  497. foreach( $hidden as $item ) {
  498. $wgOut->addHTML( $item );
  499. }
  500. $wgOut->addHTML(
  501. Xml::closeElement( 'fieldset' ) .
  502. Xml::closeElement( 'form' ) . "\n"
  503. );
  504. }
  505. private function addUsageText() {
  506. global $wgOut, $wgUser;
  507. $wgOut->addWikiMsg( 'revdelete-text' );
  508. if( $wgUser->isAllowed( 'suppressrevision' ) ) {
  509. $wgOut->addWikiMsg( 'revdelete-suppress-text' );
  510. }
  511. }
  512. /**
  513. * @param int $bitfields, aggregate bitfield of all the bitfields
  514. * @returns string HTML
  515. */
  516. private function buildCheckBoxes( $bitfields ) {
  517. $html = '';
  518. // FIXME: all items checked for just one rev are checked, even if not set for the others
  519. foreach( $this->checks as $item ) {
  520. list( $message, $name, $field ) = $item;
  521. $line = Xml::tags( 'div', null, Xml::checkLabel( wfMsg($message), $name, $name,
  522. $bitfields & $field ) );
  523. if( $field == Revision::DELETED_RESTRICTED ) $line = "<b>$line</b>";
  524. $html .= $line;
  525. }
  526. return $html;
  527. }
  528. /**
  529. * @param Revision $rev
  530. * @returns string
  531. */
  532. private function historyLine( $rev ) {
  533. global $wgLang, $wgUser;
  534. $date = $wgLang->timeanddate( $rev->getTimestamp() );
  535. $difflink = $del = '';
  536. // Live revisions
  537. if( $this->deleteKey=='oldid' ) {
  538. $tokenParams = '&unhide=1&token='.urlencode( $wgUser->editToken( $rev->getId() ) );
  539. $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid='.$rev->getId() . $tokenParams );
  540. $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
  541. 'diff=' . $rev->getId() . '&oldid=prev' . $tokenParams ) . ')';
  542. // Archived revisions
  543. } else {
  544. $undelete = SpecialPage::getTitleFor( 'Undelete' );
  545. $target = $this->page->getPrefixedText();
  546. $revlink = $this->skin->makeLinkObj( $undelete, $date,
  547. "target=$target&timestamp=" . $rev->getTimestamp() );
  548. $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
  549. "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
  550. }
  551. // Check permissions; items may be "suppressed"
  552. if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
  553. $revlink = '<span class="history-deleted">'.$revlink.'</span>';
  554. $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
  555. if( !$rev->userCan(Revision::DELETED_TEXT) ) {
  556. $revlink = '<span class="history-deleted">'.$date.'</span>';
  557. $difflink = '(' . wfMsgHtml('diff') . ')';
  558. }
  559. }
  560. $userlink = $this->skin->revUserLink( $rev );
  561. $comment = $this->skin->revComment( $rev );
  562. return "<li>$difflink $revlink $userlink $comment{$del}</li>";
  563. }
  564. /**
  565. * @param File $file
  566. * @returns string
  567. */
  568. private function fileLine( $file ) {
  569. global $wgLang, $wgTitle;
  570. $target = $this->page->getPrefixedText();
  571. $date = $wgLang->timeanddate( $file->getTimestamp(), true );
  572. $del = '';
  573. # Hidden files...
  574. if( $file->isDeleted(File::DELETED_FILE) ) {
  575. $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
  576. if( !$file->userCan(File::DELETED_FILE) ) {
  577. $pageLink = $date;
  578. } else {
  579. $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
  580. "target=$target&file=$file->sha1.".$file->getExtension() );
  581. }
  582. $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
  583. # Regular files...
  584. } else {
  585. $url = $file->getUrlRel();
  586. $pageLink = "<a href=\"{$url}\">{$date}</a>";
  587. }
  588. $data = wfMsg( 'widthheight',
  589. $wgLang->formatNum( $file->getWidth() ),
  590. $wgLang->formatNum( $file->getHeight() ) ) .
  591. ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
  592. $data = htmlspecialchars( $data );
  593. return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
  594. }
  595. /**
  596. * @param ArchivedFile $file
  597. * @returns string
  598. */
  599. private function archivedfileLine( $file ) {
  600. global $wgLang;
  601. $target = $this->page->getPrefixedText();
  602. $date = $wgLang->timeanddate( $file->getTimestamp(), true );
  603. $undelete = SpecialPage::getTitleFor( 'Undelete' );
  604. $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
  605. $del = '';
  606. if( $file->isDeleted(File::DELETED_FILE) ) {
  607. $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
  608. }
  609. $data = wfMsg( 'widthheight',
  610. $wgLang->formatNum( $file->getWidth() ),
  611. $wgLang->formatNum( $file->getHeight() ) ) .
  612. ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
  613. $data = htmlspecialchars( $data );
  614. return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
  615. }
  616. /**
  617. * @param Array $row row
  618. * @returns string
  619. */
  620. private function logLine( $row ) {
  621. global $wgLang;
  622. $date = $wgLang->timeanddate( $row->log_timestamp );
  623. $paramArray = LogPage::extractParams( $row->log_params );
  624. $title = Title::makeTitle( $row->log_namespace, $row->log_title );
  625. $logtitle = SpecialPage::getTitleFor( 'Log' );
  626. $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ),
  627. wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) );
  628. // Action text
  629. if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) {
  630. $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
  631. } else {
  632. $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
  633. $this->skin, $paramArray, true, true );
  634. if( $row->log_deleted & LogPage::DELETED_ACTION )
  635. $action = '<span class="history-deleted">' . $action . '</span>';
  636. }
  637. // User links
  638. $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) );
  639. if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) {
  640. $userLink = '<span class="history-deleted">' . $userLink . '</span>';
  641. }
  642. // Comment
  643. $comment = $wgLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
  644. if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) {
  645. $comment = '<span class="history-deleted">' . $comment . '</span>';
  646. }
  647. return "<li>($loglink) $date $userLink $action $comment</li>";
  648. }
  649. /**
  650. * Generate a user tool link cluster if the current user is allowed to view it
  651. * @param ArchivedFile $file
  652. * @return string HTML
  653. */
  654. private function fileUserTools( $file ) {
  655. if( $file->userCan( Revision::DELETED_USER ) ) {
  656. $link = $this->skin->userLink( $file->user, $file->user_text ) .
  657. $this->skin->userToolLinks( $file->user, $file->user_text );
  658. } else {
  659. $link = wfMsgHtml( 'rev-deleted-user' );
  660. }
  661. if( $file->isDeleted( Revision::DELETED_USER ) ) {
  662. return '<span class="history-deleted">' . $link . '</span>';
  663. }
  664. return $link;
  665. }
  666. /**
  667. * Wrap and format the given file's comment block, if the current
  668. * user is allowed to view it.
  669. *
  670. * @param ArchivedFile $file
  671. * @return string HTML
  672. */
  673. private function fileComment( $file ) {
  674. if( $file->userCan( File::DELETED_COMMENT ) ) {
  675. $block = $this->skin->commentBlock( $file->description );
  676. } else {
  677. $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
  678. }
  679. if( $file->isDeleted( File::DELETED_COMMENT ) ) {
  680. return "<span class=\"history-deleted\">$block</span>";
  681. }
  682. return $block;
  683. }
  684. /**
  685. * @param WebRequest $request
  686. */
  687. private function submit( $request ) {
  688. global $wgUser, $wgOut;
  689. # Check edit token on submission
  690. if( $this->wasPosted && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) {
  691. $wgOut->addWikiMsg( 'sessionfailure' );
  692. return false;
  693. }
  694. $bitfield = $this->extractBitfield( $request );
  695. $comment = $request->getText( 'wpReason' );
  696. # Can the user set this field?
  697. if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
  698. $wgOut->permissionRequired( 'suppressrevision' );
  699. return false;
  700. }
  701. # If the save went through, go to success message. Otherwise
  702. # bounce back to form...
  703. if( $this->save( $bitfield, $comment, $this->page ) ) {
  704. $this->success();
  705. } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
  706. return $this->showRevs();
  707. } else if( $request->getCheck( 'logid' ) ) {
  708. return $this->showLogs();
  709. } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
  710. return $this->showImages();
  711. }
  712. }
  713. private function success() {
  714. global $wgOut;
  715. $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
  716. $wrap = '<span class="success">$1</span>';
  717. if( $this->deleteKey=='logid' ) {
  718. $wgOut->wrapWikiMsg( $wrap, 'logdelete-success' );
  719. $this->showLogItems();
  720. } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) {
  721. $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
  722. $this->showRevs();
  723. } else if( $this->deleteKey=='fileid' ) {
  724. $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
  725. $this->showImages();
  726. } else if( $this->deleteKey=='oldimage' ) {
  727. $wgOut->wrapWikiMsg( $wrap, 'revdelete-success' );
  728. $this->showImages();
  729. }
  730. }
  731. /**
  732. * Put together a rev_deleted bitfield from the submitted checkboxes
  733. * @param WebRequest $request
  734. * @return int
  735. */
  736. private function extractBitfield( $request ) {
  737. $bitfield = 0;
  738. foreach( $this->checks as $item ) {
  739. list( /* message */ , $name, $field ) = $item;
  740. if( $request->getCheck( $name ) ) {
  741. $bitfield |= $field;
  742. }
  743. }
  744. return $bitfield;
  745. }
  746. private function save( $bitfield, $reason, $title ) {
  747. $dbw = wfGetDB( DB_MASTER );
  748. // Don't allow simply locking the interface for no reason
  749. if( $bitfield == Revision::DELETED_RESTRICTED ) {
  750. $bitfield = 0;
  751. }
  752. $deleter = new RevisionDeleter( $dbw );
  753. // By this point, only one of the below should be set
  754. if( isset($this->revisions) ) {
  755. return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
  756. } else if( isset($this->archrevs) ) {
  757. return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
  758. } else if( isset($this->ofiles) ) {
  759. return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
  760. } else if( isset($this->afiles) ) {
  761. return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
  762. } else if( isset($this->events) ) {
  763. return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason );
  764. }
  765. }
  766. }
  767. /**
  768. * Implements the actions for Revision Deletion.
  769. * @ingroup SpecialPage
  770. */
  771. class RevisionDeleter {
  772. function __construct( $db ) {
  773. $this->dbw = $db;
  774. }
  775. /**
  776. * @param $title, the page these events apply to
  777. * @param array $items list of revision ID numbers
  778. * @param int $bitfield new rev_deleted value
  779. * @param string $comment Comment for log records
  780. */
  781. function setRevVisibility( $title, $items, $bitfield, $comment ) {
  782. global $wgOut;
  783. $userAllowedAll = $success = true;
  784. $revIDs = array();
  785. $revCount = 0;
  786. // Run through and pull all our data in one query
  787. foreach( $items as $revid ) {
  788. $where[] = intval($revid);
  789. }
  790. $result = $this->dbw->select( 'revision', '*',
  791. array(
  792. 'rev_page' => $title->getArticleID(),
  793. 'rev_id' => $where ),
  794. __METHOD__ );
  795. while( $row = $this->dbw->fetchObject( $result ) ) {
  796. $revObjs[$row->rev_id] = new Revision( $row );
  797. }
  798. // To work!
  799. foreach( $items as $revid ) {
  800. if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
  801. $success = false;
  802. continue; // Must exist
  803. } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
  804. $userAllowedAll=false;
  805. continue;
  806. }
  807. // For logging, maintain a count of revisions
  808. if( $revObjs[$revid]->mDeleted != $bitfield ) {
  809. $revCount++;
  810. $revIDs[]=$revid;
  811. $this->updateRevision( $revObjs[$revid], $bitfield );
  812. $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
  813. }
  814. }
  815. // Clear caches...
  816. // Don't log or touch if nothing changed
  817. if( $revCount > 0 ) {
  818. $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
  819. $comment, $title, 'oldid', $revIDs );
  820. $this->updatePage( $title );
  821. }
  822. // Where all revs allowed to be set?
  823. if( !$userAllowedAll ) {
  824. //FIXME: still might be confusing???
  825. $wgOut->permissionRequired( 'suppressrevision' );
  826. return false;
  827. }
  828. return $success;
  829. }
  830. /**
  831. * @param $title, the page these events apply to
  832. * @param array $items list of revision ID numbers
  833. * @param int $bitfield new rev_deleted value
  834. * @param string $comment Comment for log records
  835. */
  836. function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
  837. global $wgOut;
  838. $userAllowedAll = $success = true;
  839. $count = 0;
  840. $Id_set = array();
  841. // Run through and pull all our data in one query
  842. foreach( $items as $timestamp ) {
  843. $where[] = $this->dbw->timestamp( $timestamp );
  844. }
  845. $result = $this->dbw->select( 'archive', '*',
  846. array(
  847. 'ar_namespace' => $title->getNamespace(),
  848. 'ar_title' => $title->getDBKey(),
  849. 'ar_timestamp' => $where ),
  850. __METHOD__ );
  851. while( $row = $this->dbw->fetchObject( $result ) ) {
  852. $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
  853. $revObjs[$timestamp] = new Revision( array(
  854. 'page' => $title->getArticleId(),
  855. 'id' => $row->ar_rev_id,
  856. 'text' => $row->ar_text_id,
  857. 'comment' => $row->ar_comment,
  858. 'user' => $row->ar_user,
  859. 'user_text' => $row->ar_user_text,
  860. 'timestamp' => $timestamp,
  861. 'minor_edit' => $row->ar_minor_edit,
  862. 'text_id' => $row->ar_text_id,
  863. 'deleted' => $row->ar_deleted,
  864. 'len' => $row->ar_len) );
  865. }
  866. // To work!
  867. foreach( $items as $timestamp ) {
  868. // This will only select the first revision with this timestamp.
  869. // Since they are all selected/deleted at once, we can just check the
  870. // permissions of one. UPDATE is done via timestamp, so all revs are set.
  871. if( !is_object($revObjs[$timestamp]) ) {
  872. $success = false;
  873. continue; // Must exist
  874. } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
  875. $userAllowedAll=false;
  876. continue;
  877. }
  878. // Which revisions did we change anything about?
  879. if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
  880. $Id_set[]=$timestamp;
  881. $count++;
  882. $this->updateArchive( $revObjs[$timestamp], $title, $bitfield );
  883. }
  884. }
  885. // For logging, maintain a count of revisions
  886. if( $count > 0 ) {
  887. $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
  888. $comment, $title, 'artimestamp', $Id_set );
  889. }
  890. // Where all revs allowed to be set?
  891. if( !$userAllowedAll ) {
  892. $wgOut->permissionRequired( 'suppressrevision' );
  893. return false;
  894. }
  895. return $success;
  896. }
  897. /**
  898. * @param $title, the page these events apply to
  899. * @param array $items list of revision ID numbers
  900. * @param int $bitfield new rev_deleted value
  901. * @param string $comment Comment for log records
  902. */
  903. function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
  904. global $wgOut;
  905. $userAllowedAll = $success = true;
  906. $count = 0;
  907. $set = array();
  908. // Run through and pull all our data in one query
  909. foreach( $items as $timestamp ) {
  910. $where[] = $timestamp.'!'.$title->getDBKey();
  911. }
  912. $result = $this->dbw->select( 'oldimage', '*',
  913. array(
  914. 'oi_name' => $title->getDBKey(),
  915. 'oi_archive_name' => $where ),
  916. __METHOD__ );
  917. while( $row = $this->dbw->fetchObject( $result ) ) {
  918. $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
  919. $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
  920. $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
  921. }
  922. // To work!
  923. foreach( $items as $timestamp ) {
  924. $archivename = $timestamp.'!'.$title->getDBKey();
  925. if( !isset($filesObjs[$archivename]) ) {
  926. $success = false;
  927. continue; // Must exist
  928. } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
  929. $userAllowedAll=false;
  930. continue;
  931. }
  932. $transaction = true;
  933. // Which revisions did we change anything about?
  934. if( $filesObjs[$archivename]->deleted != $bitfield ) {
  935. $count++;
  936. $this->dbw->begin();
  937. $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
  938. // If this image is currently hidden...
  939. if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
  940. if( $bitfield & File::DELETED_FILE ) {
  941. # Leave it alone if we are not changing this...
  942. $set[]=$archivename;
  943. $transaction = true;
  944. } else {
  945. # We are moving this out
  946. $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
  947. $set[]=$transaction;
  948. }
  949. // Is it just now becoming hidden?
  950. } else if( $bitfield & File::DELETED_FILE ) {
  951. $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
  952. $set[]=$transaction;
  953. } else {
  954. $set[]=$timestamp;
  955. }
  956. // If our file operations fail, then revert back the db
  957. if( $transaction==false ) {
  958. $this->dbw->rollback();
  959. return false;
  960. }
  961. $this->dbw->commit();
  962. }
  963. }
  964. // Log if something was changed
  965. if( $count > 0 ) {
  966. $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
  967. $comment, $title, 'oldimage', $set );
  968. # Purge page/history
  969. $file = wfLocalFile( $title );
  970. $file->purgeCache();
  971. $file->purgeHistory();
  972. # Invalidate cache for all pages using this file
  973. $update = new HTMLCacheUpdate( $title, 'imagelinks' );
  974. $update->doUpdate();
  975. }
  976. // Where all revs allowed to be set?
  977. if( !$userAllowedAll ) {
  978. $wgOut->permissionRequired( 'suppressrevision' );
  979. return false;
  980. }
  981. return $success;
  982. }
  983. /**
  984. * @param $title, the page these events apply to
  985. * @param array $items list of revision ID numbers
  986. * @param int $bitfield new rev_deleted value
  987. * @param string $comment Comment for log records
  988. */
  989. function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
  990. global $wgOut;
  991. $userAllowedAll = $success = true;
  992. $count = 0;
  993. $Id_set = array();
  994. // Run through and pull all our data in one query
  995. foreach( $items as $id ) {
  996. $where[] = intval($id);
  997. }
  998. $result = $this->dbw->select( 'filearchive', '*',
  999. array( 'fa_name' => $title->getDBKey(),
  1000. 'fa_id' => $where ),
  1001. __METHOD__ );
  1002. while( $row = $this->dbw->fetchObject( $result ) ) {
  1003. $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
  1004. }
  1005. // To work!
  1006. foreach( $items as $fileid ) {
  1007. if( !isset($filesObjs[$fileid]) ) {
  1008. $success = false;
  1009. continue; // Must exist
  1010. } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
  1011. $userAllowedAll=false;
  1012. continue;
  1013. }
  1014. // Which revisions did we change anything about?
  1015. if( $filesObjs[$fileid]->deleted != $bitfield ) {
  1016. $Id_set[]=$fileid;
  1017. $count++;
  1018. $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
  1019. }
  1020. }
  1021. // Log if something was changed
  1022. if( $count > 0 ) {
  1023. $this->updateLog( $title, $count, $bitfield, $comment,
  1024. $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
  1025. }
  1026. // Where all revs allowed to be set?
  1027. if( !$userAllowedAll ) {
  1028. $wgOut->permissionRequired( 'suppressrevision' );
  1029. return false;
  1030. }
  1031. return $success;
  1032. }
  1033. /**
  1034. * @param $title, the log page these events apply to
  1035. * @param array $items list of log ID numbers
  1036. * @param int $bitfield new log_deleted value
  1037. * @param string $comment Comment for log records
  1038. */
  1039. function setEventVisibility( $title, $items, $bitfield, $comment ) {
  1040. global $wgOut;
  1041. $userAllowedAll = $success = true;
  1042. $count = 0;
  1043. $log_Ids = array();
  1044. // Run through and pull all our data in one query
  1045. foreach( $items as $logid ) {
  1046. $where[] = intval($logid);
  1047. }
  1048. list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
  1049. $result = $this->dbw->select( 'logging', '*',
  1050. array(
  1051. 'log_type' => $logtype,
  1052. 'log_id' => $where ),
  1053. __METHOD__ );
  1054. while( $row = $this->dbw->fetchObject( $result ) ) {
  1055. $logRows[$row->log_id] = $row;
  1056. }
  1057. // To work!
  1058. foreach( $items as $logid ) {
  1059. if( !isset($logRows[$logid]) ) {
  1060. $success = false;
  1061. continue; // Must exist
  1062. } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED)
  1063. || $logRows[$logid]->log_type == 'suppress' ) {
  1064. // Don't hide from oversight log!!!
  1065. $userAllowedAll=false;
  1066. continue;
  1067. }
  1068. // Which logs did we change anything about?
  1069. if( $logRows[$logid]->log_deleted != $bitfield ) {
  1070. $log_Ids[]=$logid;
  1071. $count++;
  1072. $this->updateLogs( $logRows[$logid], $bitfield );
  1073. $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
  1074. }
  1075. }
  1076. // Don't log or touch if nothing changed
  1077. if( $count > 0 ) {
  1078. $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted,
  1079. $comment, $title, 'logid', $log_Ids );
  1080. }
  1081. // Were all revs allowed to be set?
  1082. if( !$userAllowedAll ) {
  1083. $wgOut->permissionRequired( 'suppressrevision' );
  1084. return false;
  1085. }
  1086. return $success;
  1087. }
  1088. /**
  1089. * Moves an image to a safe private location
  1090. * Caller is responsible for clearing caches
  1091. * @param File $oimage
  1092. * @returns mixed, timestamp string on success, false on failure
  1093. */
  1094. function makeOldImagePrivate( $oimage ) {
  1095. $transaction = new FSTransaction();
  1096. if( !FileStore::lock() ) {
  1097. wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
  1098. return false;
  1099. }
  1100. $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
  1101. // Dupe the file into the file store
  1102. if( file_exists( $oldpath ) ) {
  1103. // Is our directory configured?
  1104. if( $store = FileStore::get( 'deleted' ) ) {
  1105. if( !$oimage->sha1 ) {
  1106. $oimage->upgradeRow(); // sha1 may be missing
  1107. }
  1108. $key = $oimage->sha1 . '.' . $oimage->getExtension();
  1109. $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
  1110. } else {
  1111. $group = null;
  1112. $key = null;
  1113. $transaction = false; // Return an error and do nothing
  1114. }
  1115. } else {
  1116. wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
  1117. $group = null;
  1118. $key = '';
  1119. $transaction = new FSTransaction(); // empty
  1120. }
  1121. if( $transaction === false ) {
  1122. // Fail to restore?
  1123. wfDebug( __METHOD__.": import to file store failed, aborting\n" );
  1124. throw new MWException( "Could not archive and delete file $oldpath" );
  1125. return false;
  1126. }
  1127. wfDebug( __METHOD__.": set db items, applying file transactions\n" );
  1128. $transaction->commit();
  1129. FileStore::unlock();
  1130. $m = explode('!',$oimage->archive_name,2);
  1131. $timestamp = $m[0];
  1132. return $timestamp;
  1133. }
  1134. /**
  1135. * Moves an image from a safe private location
  1136. * Caller is responsible for clearing caches
  1137. * @param File $oimage
  1138. * @returns mixed, string timestamp on success, false on failure
  1139. */
  1140. function makeOldImagePublic( $oimage ) {
  1141. $transaction = new FSTransaction();
  1142. if( !FileStore::lock() ) {
  1143. wfDebug( __METHOD__." could not acquire filestore lock\n" );
  1144. return false;
  1145. }
  1146. $store = FileStore::get( 'deleted' );
  1147. if( !$store ) {
  1148. wfDebug( __METHOD__.": skipping row with no file.\n" );
  1149. return false;
  1150. }
  1151. $key = $oimage->sha1.'.'.$oimage->getExtension();
  1152. $destDir = $oimage->getArchivePath();
  1153. if( !is_dir( $destDir ) ) {
  1154. wfMkdirParents( $destDir );
  1155. }
  1156. $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
  1157. // Check if any other stored revisions use this file;
  1158. // if so, we shouldn't remove the file from the hidden
  1159. // archives so they will still work. Check hidden files first.
  1160. $useCount = $this->dbw->selectField( 'oldimage', '1',
  1161. array( 'oi_sha1' => $oimage->sha1,
  1162. 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
  1163. __METHOD__, array( 'FOR UPDATE' ) );
  1164. // Check the rest of the deleted archives too.
  1165. // (these are the ones that don't show in the image history)
  1166. if( !$useCount ) {
  1167. $useCount = $this->dbw->selectField( 'filearchive', '1',
  1168. array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
  1169. __METHOD__, array( 'FOR UPDATE' ) );
  1170. }
  1171. if( $useCount == 0 ) {
  1172. wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
  1173. $flags = FileStore::DELETE_ORIGINAL;
  1174. } else {
  1175. $flags = 0;
  1176. }
  1177. $transaction->add( $store->export( $key, $destPath, $flags ) );
  1178. wfDebug( __METHOD__.": set db items, applying file transactions\n" );
  1179. $transaction->commit();
  1180. FileStore::unlock();
  1181. $m = explode('!',$oimage->archive_name,2);
  1182. $timestamp = $m[0];
  1183. return $timestamp;
  1184. }
  1185. /**
  1186. * Update the revision's rev_deleted field
  1187. * @param Revision $rev
  1188. * @param int $bitfield new rev_deleted bitfield value
  1189. */
  1190. function updateRevision( $rev, $bitfield ) {
  1191. $this->dbw->update( 'revision',
  1192. array( 'rev_deleted' => $bitfield ),
  1193. array( 'rev_id' => $rev->getId(),
  1194. 'rev_page' => $rev->getPage() ),
  1195. __METHOD__ );
  1196. }
  1197. /**
  1198. * Update the revision's rev_deleted field
  1199. * @param Revision $rev
  1200. * @param Title $title
  1201. * @param int $bitfield new rev_deleted bitfield value
  1202. */
  1203. function updateArchive( $rev, $title, $bitfield ) {
  1204. $this->dbw->update( 'archive',
  1205. array( 'ar_deleted' => $bitfield ),
  1206. array( 'ar_namespace' => $title->getNamespace(),
  1207. 'ar_title' => $title->getDBKey(),
  1208. 'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ),
  1209. 'ar_rev_id' => $rev->getId() ),
  1210. __METHOD__ );
  1211. }
  1212. /**
  1213. * Update the images's oi_deleted field
  1214. * @param File $file
  1215. * @param int $bitfield new rev_deleted bitfield value
  1216. */
  1217. function updateOldFiles( $file, $bitfield ) {
  1218. $this->dbw->update( 'oldimage',
  1219. array( 'oi_deleted' => $bitfield ),
  1220. array( 'oi_name' => $file->getName(),
  1221. 'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ),
  1222. __METHOD__ );
  1223. }
  1224. /**
  1225. * Update the images's fa_deleted field
  1226. * @param ArchivedFile $file
  1227. * @param int $bitfield new rev_deleted bitfield value
  1228. */
  1229. function updateArchFiles( $file, $bitfield ) {
  1230. $this->dbw->update( 'filearchive',
  1231. array( 'fa_deleted' => $bitfield ),
  1232. array( 'fa_id' => $file->getId() ),
  1233. __METHOD__ );
  1234. }
  1235. /**
  1236. * Update the logging log_deleted field
  1237. * @param Row $row
  1238. * @param int $bitfield new rev_deleted bitfield value
  1239. */
  1240. function updateLogs( $row, $bitfield ) {
  1241. $this->dbw->update( 'logging',
  1242. array( 'log_deleted' => $bitfield ),
  1243. array( 'log_id' => $row->log_id ),
  1244. __METHOD__ );
  1245. }
  1246. /**
  1247. * Update the revision's recentchanges record if fields have been hidden
  1248. * @param Revision $rev
  1249. * @param int $bitfield new rev_deleted bitfield value
  1250. */
  1251. function updateRecentChangesEdits( $rev, $bitfield ) {
  1252. $this->dbw->update( 'recentchanges',
  1253. array( 'rc_deleted' => $bitfield,
  1254. 'rc_patrolled' => 1 ),
  1255. array( 'rc_this_oldid' => $rev->getId(),
  1256. 'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ),
  1257. __METHOD__ );
  1258. }
  1259. /**
  1260. * Update the revision's recentchanges record if fields have been hidden
  1261. * @param Row $row
  1262. * @param int $bitfield new rev_deleted bitfield value
  1263. */
  1264. function updateRecentChangesLog( $row, $bitfield ) {
  1265. $this->dbw->update( 'recentchanges',
  1266. array( 'rc_deleted' => $bitfield,
  1267. 'rc_patrolled' => 1 ),
  1268. array( 'rc_logid' => $row->log_id,
  1269. 'rc_timestamp' => $row->log_timestamp ),
  1270. __METHOD__ );
  1271. }
  1272. /**
  1273. * Touch the page's cache invalidation timestamp; this forces cached
  1274. * history views to refresh, so any newly hidden or shown fields will
  1275. * update properly.
  1276. * @param Title $title
  1277. */
  1278. function updatePage( $title ) {
  1279. $title->invalidateCache();
  1280. $title->purgeSquid();
  1281. $title->touchLinks();
  1282. // Extensions that require referencing previous revisions may need this
  1283. wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
  1284. }
  1285. /**
  1286. * Checks for a change in the bitfield for a certain option and updates the
  1287. * provided array accordingly.
  1288. *
  1289. * @param String $desc Description to add to the array if the option was
  1290. * enabled / disabled.
  1291. * @param int $field The bitmask describing the single option.
  1292. * @param int $diff The xor of the old and new bitfields.
  1293. * @param array $arr The array to update.
  1294. */
  1295. function checkItem ( $desc, $field, $diff, $new, &$arr ) {
  1296. if ( $diff & $field ) {
  1297. $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc;
  1298. }
  1299. }
  1300. /**
  1301. * Gets an array describing the changes made to the visibilit of the revision.
  1302. * If the resulting array is $arr, then $arr[0] will contain an array of strings
  1303. * describing the items that were hidden, $arr[2] will contain an array of strings
  1304. * describing the items that were unhidden, and $arr[3] will contain an array with
  1305. * a single string, which can be one of "applied restrictions to sysops",
  1306. * "removed restrictions from sysops", or null.
  1307. *
  1308. * @param int $n The new bitfield.
  1309. * @param int $o The old bitfield.
  1310. * @return An array as described above.
  1311. */
  1312. function getChanges ( $n, $o ) {
  1313. $diff = $n ^ $o;
  1314. $ret = array ( 0 => array(), 1 => array(), 2 => array() );
  1315. $this->checkItem ( wfMsgForContent ( 'revdelete-content' ),
  1316. Revision::DELETED_TEXT, $diff, $n, $ret );
  1317. $this->checkItem ( wfMsgForContent ( 'revdelete-summary' ),
  1318. Revision::DELETED_COMMENT, $diff, $n, $ret );
  1319. $this->checkItem ( wfMsgForContent ( 'revdelete-uname' ),
  1320. Revision::DELETED_USER, $diff, $n, $ret );
  1321. // Restriction application to sysops
  1322. if ( $diff & Revision::DELETED_RESTRICTED ) {
  1323. if ( $n & Revision::DELETED_RESTRICTED )
  1324. $ret[2][] = wfMsgForContent ( 'revdelete-restricted' );
  1325. else
  1326. $ret[2][] = wfMsgForContent ( 'revdelete-unrestricted' );
  1327. }
  1328. return $ret;
  1329. }
  1330. /**
  1331. * Gets a log message to describe the given revision visibility change. This
  1332. * message will be of the form "[hid {content, edit summary, username}];
  1333. * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
  1334. *
  1335. * @param int $count The number of effected revisions.
  1336. * @param int $nbitfield The new bitfield for the revision.
  1337. * @param int $obitfield The old bitfield for the revision.
  1338. * @param string $comment The comment associated with the change.
  1339. * @param bool $isForLog
  1340. */
  1341. function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) {
  1342. global $wgContLang;
  1343. $s = '';
  1344. $changes = $this->getChanges( $nbitfield, $obitfield );
  1345. if ( count ( $changes[0] ) ) {
  1346. $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
  1347. }
  1348. if ( count ( $changes[1] ) ) {
  1349. if ($s) $s .= '; ';
  1350. $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
  1351. }
  1352. if ( count ( $changes[2] )) {
  1353. if ($s)
  1354. $s .= ' (' . $changes[2][0] . ')';
  1355. else
  1356. $s = $changes[2][0];
  1357. }
  1358. $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
  1359. $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
  1360. $s, $wgContLang->formatNum( $count ) );
  1361. if ( $comment )
  1362. $ret .= ": $comment";
  1363. return $ret;
  1364. }
  1365. /**
  1366. * Record a log entry on the action
  1367. * @param Title $title, page where item was removed from
  1368. * @param int $count the number of revisions altered for this page
  1369. * @param int $nbitfield the new _deleted value
  1370. * @param int $obitfield the old _deleted value
  1371. * @param string $comment
  1372. * @param Title $target, the relevant page
  1373. * @param string $param, URL param
  1374. * @param Array $items
  1375. */
  1376. function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
  1377. // Put things hidden from sysops in the oversight log
  1378. $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'suppress' : 'delete';
  1379. $log = new LogPage( $logtype );
  1380. $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
  1381. if( $param == 'logid' ) {
  1382. $params = array( implode( ',', $items) );
  1383. $log->addEntry( 'event', $title, $reason, $params );
  1384. } else {
  1385. // Add params for effected page and ids
  1386. $params = array( $param, implode( ',', $items) );
  1387. $log->addEntry( 'revision', $title, $reason, $params );
  1388. }
  1389. }
  1390. }