ImagePage.php 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. <?php
  2. if( !defined( 'MEDIAWIKI' ) )
  3. die( 1 );
  4. /**
  5. * Special handling for image description pages
  6. *
  7. * @ingroup Media
  8. */
  9. class ImagePage extends Article {
  10. /* private */ var $img; // Image object
  11. /* private */ var $displayImg;
  12. /* private */ var $repo;
  13. /* private */ var $fileLoaded;
  14. var $mExtraDescription = false;
  15. var $dupes;
  16. function __construct( $title ) {
  17. parent::__construct( $title );
  18. $this->dupes = null;
  19. $this->repo = null;
  20. }
  21. public function setFile( $file ) {
  22. $this->displayImg = $file;
  23. $this->img = $file;
  24. $this->fileLoaded = true;
  25. }
  26. protected function loadFile() {
  27. if( $this->fileLoaded ) {
  28. return true;
  29. }
  30. $this->fileLoaded = true;
  31. $this->displayImg = $this->img = false;
  32. wfRunHooks( 'ImagePageFindFile', array( $this, &$this->img, &$this->displayImg ) );
  33. if( !$this->img ) {
  34. $this->img = wfFindFile( $this->mTitle );
  35. if( !$this->img ) {
  36. $this->img = wfLocalFile( $this->mTitle );
  37. }
  38. }
  39. if( !$this->displayImg ) {
  40. $this->displayImg = $this->img;
  41. }
  42. $this->repo = $this->img->getRepo();
  43. }
  44. /**
  45. * Handler for action=render
  46. * Include body text only; none of the image extras
  47. */
  48. public function render() {
  49. global $wgOut;
  50. $wgOut->setArticleBodyOnly( true );
  51. parent::view();
  52. }
  53. public function view() {
  54. global $wgOut, $wgShowEXIF, $wgRequest, $wgUser;
  55. $this->loadFile();
  56. if( $this->mTitle->getNamespace() == NS_FILE && $this->img->getRedirected() ) {
  57. if( $this->mTitle->getDBkey() == $this->img->getName() ) {
  58. // mTitle is the same as the redirect target so ask Article
  59. // to perform the redirect for us.
  60. return Article::view();
  61. } else {
  62. // mTitle is not the same as the redirect target so it is
  63. // probably the redirect page itself. Fake the redirect symbol
  64. $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
  65. $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->img->getName() ),
  66. /* $appendSubtitle */ true, /* $forceKnown */ true ) );
  67. $this->viewUpdates();
  68. return;
  69. }
  70. }
  71. $diff = $wgRequest->getVal( 'diff' );
  72. $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
  73. if( $this->mTitle->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) )
  74. return Article::view();
  75. if( $wgShowEXIF && $this->displayImg->exists() ) {
  76. // FIXME: bad interface, see note on MediaHandler::formatMetadata().
  77. $formattedMetadata = $this->displayImg->formatMetadata();
  78. $showmeta = $formattedMetadata !== false;
  79. } else {
  80. $showmeta = false;
  81. }
  82. if( !$diff && $this->displayImg->exists() )
  83. $wgOut->addHTML( $this->showTOC($showmeta) );
  84. if( !$diff )
  85. $this->openShowImage();
  86. # No need to display noarticletext, we use our own message, output in openShowImage()
  87. if( $this->getID() ) {
  88. Article::view();
  89. } else {
  90. # Just need to set the right headers
  91. $wgOut->setArticleFlag( true );
  92. $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
  93. $this->viewUpdates();
  94. }
  95. # Show shared description, if needed
  96. if( $this->mExtraDescription ) {
  97. $fol = wfMsgNoTrans( 'shareddescriptionfollows' );
  98. if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) {
  99. $wgOut->addWikiText( $fol );
  100. }
  101. $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' );
  102. }
  103. $this->closeShowImage();
  104. $this->imageHistory();
  105. // TODO: Cleanup the following
  106. $wgOut->addHTML( Xml::element( 'h2',
  107. array( 'id' => 'filelinks' ),
  108. wfMsg( 'imagelinks' ) ) . "\n" );
  109. $this->imageDupes();
  110. # TODO! FIXME! For some freaky reason, we can't redirect to foreign images.
  111. # Yet we return metadata about the target. Definitely an issue in the FileRepo
  112. $this->imageRedirects();
  113. $this->imageLinks();
  114. if( $showmeta ) {
  115. global $wgStylePath, $wgStyleVersion;
  116. $expand = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-expand' ) ) );
  117. $collapse = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-collapse' ) ) );
  118. $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" );
  119. $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
  120. $wgOut->addScriptFile( 'metadata.js' );
  121. $wgOut->addHTML(
  122. "<script type=\"text/javascript\">attachMetadataToggle('mw_metadata', '$expand', '$collapse');</script>\n" );
  123. }
  124. }
  125. public function getRedirectTarget() {
  126. $this->loadFile();
  127. if( $this->img->isLocal() ) {
  128. return parent::getRedirectTarget();
  129. }
  130. // Foreign image page
  131. $from = $this->img->getRedirected();
  132. $to = $this->img->getName();
  133. if( $from == $to ) {
  134. return null;
  135. }
  136. return $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to );
  137. }
  138. public function followRedirect() {
  139. $this->loadFile();
  140. if( $this->img->isLocal() ) {
  141. return parent::followRedirect();
  142. }
  143. $from = $this->img->getRedirected();
  144. $to = $this->img->getName();
  145. if( $from == $to ) {
  146. return false;
  147. }
  148. return Title::makeTitle( NS_FILE, $to );
  149. }
  150. public function isRedirect( $text = false ) {
  151. $this->loadFile();
  152. if( $this->img->isLocal() )
  153. return parent::isRedirect( $text );
  154. return (bool)$this->img->getRedirected();
  155. }
  156. public function isLocal() {
  157. $this->loadFile();
  158. return $this->img->isLocal();
  159. }
  160. public function getFile() {
  161. $this->loadFile();
  162. return $this->img;
  163. }
  164. public function getDisplayedFile() {
  165. $this->loadFile();
  166. return $this->displayImg;
  167. }
  168. public function getDuplicates() {
  169. $this->loadFile();
  170. if( !is_null($this->dupes) ) {
  171. return $this->dupes;
  172. }
  173. if( !( $hash = $this->img->getSha1() ) ) {
  174. return $this->dupes = array();
  175. }
  176. $dupes = RepoGroup::singleton()->findBySha1( $hash );
  177. // Remove duplicates with self and non matching file sizes
  178. $self = $this->img->getRepoName().':'.$this->img->getName();
  179. $size = $this->img->getSize();
  180. foreach ( $dupes as $index => $file ) {
  181. $key = $file->getRepoName().':'.$file->getName();
  182. if( $key == $self )
  183. unset( $dupes[$index] );
  184. if( $file->getSize() != $size )
  185. unset( $dupes[$index] );
  186. }
  187. return $this->dupes = $dupes;
  188. }
  189. /**
  190. * Create the TOC
  191. *
  192. * @param bool $metadata Whether or not to show the metadata link
  193. * @return string
  194. */
  195. protected function showTOC( $metadata ) {
  196. global $wgLang;
  197. $r = '<ul id="filetoc">
  198. <li><a href="#file">' . $wgLang->getNsText( NS_FILE ) . '</a></li>
  199. <li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>
  200. <li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>' .
  201. ($metadata ? ' <li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . '
  202. </ul>';
  203. return $r;
  204. }
  205. /**
  206. * Make a table with metadata to be shown in the output page.
  207. *
  208. * FIXME: bad interface, see note on MediaHandler::formatMetadata().
  209. *
  210. * @param array $exif The array containing the EXIF data
  211. * @return string
  212. */
  213. protected function makeMetadataTable( $metadata ) {
  214. $r = wfMsg( 'metadata-help' ) . "\n\n";
  215. $r .= "{| id=mw_metadata class=mw_metadata\n";
  216. foreach ( $metadata as $type => $stuff ) {
  217. foreach ( $stuff as $v ) {
  218. # FIXME, why is this using escapeId for a class?!
  219. $class = Sanitizer::escapeId( $v['id'] );
  220. if( $type == 'collapsed' ) {
  221. $class .= ' collapsable';
  222. }
  223. $r .= "|- class=\"$class\"\n";
  224. $r .= "!| {$v['name']}\n";
  225. $r .= "|| {$v['value']}\n";
  226. }
  227. }
  228. $r .= '|}';
  229. return $r;
  230. }
  231. /**
  232. * Overloading Article's getContent method.
  233. *
  234. * Omit noarticletext if sharedupload; text will be fetched from the
  235. * shared upload server if possible.
  236. */
  237. public function getContent() {
  238. $this->loadFile();
  239. if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) {
  240. return '';
  241. }
  242. return Article::getContent();
  243. }
  244. protected function openShowImage() {
  245. global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang;
  246. $this->loadFile();
  247. $full_url = $this->displayImg->getURL();
  248. $linkAttribs = false;
  249. $sizeSel = intval( $wgUser->getOption( 'imagesize') );
  250. if( !isset( $wgImageLimits[$sizeSel] ) ) {
  251. $sizeSel = User::getDefaultOption( 'imagesize' );
  252. // The user offset might still be incorrect, specially if
  253. // $wgImageLimits got changed (see bug #8858).
  254. if( !isset( $wgImageLimits[$sizeSel] ) ) {
  255. // Default to the first offset in $wgImageLimits
  256. $sizeSel = 0;
  257. }
  258. }
  259. $max = $wgImageLimits[$sizeSel];
  260. $maxWidth = $max[0];
  261. $maxHeight = $max[1];
  262. $sk = $wgUser->getSkin();
  263. $dirmark = $wgContLang->getDirMark();
  264. if( $this->displayImg->exists() ) {
  265. # image
  266. $page = $wgRequest->getIntOrNull( 'page' );
  267. if( is_null( $page ) ) {
  268. $params = array();
  269. $page = 1;
  270. } else {
  271. $params = array( 'page' => $page );
  272. }
  273. $width_orig = $this->displayImg->getWidth();
  274. $width = $width_orig;
  275. $height_orig = $this->displayImg->getHeight();
  276. $height = $height_orig;
  277. $mime = $this->displayImg->getMimeType();
  278. $showLink = false;
  279. $linkAttribs = array( 'href' => $full_url );
  280. $longDesc = $this->displayImg->getLongDesc();
  281. wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ;
  282. if( $this->displayImg->allowInlineDisplay() ) {
  283. # image
  284. # "Download high res version" link below the image
  285. #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->displayImg->getSize() ), $mime );
  286. # We'll show a thumbnail of this image
  287. if( $width > $maxWidth || $height > $maxHeight ) {
  288. # Calculate the thumbnail size.
  289. # First case, the limiting factor is the width, not the height.
  290. if( $width / $height >= $maxWidth / $maxHeight ) {
  291. $height = round( $height * $maxWidth / $width);
  292. $width = $maxWidth;
  293. # Note that $height <= $maxHeight now.
  294. } else {
  295. $newwidth = floor( $width * $maxHeight / $height);
  296. $height = round( $height * $newwidth / $width );
  297. $width = $newwidth;
  298. # Note that $height <= $maxHeight now, but might not be identical
  299. # because of rounding.
  300. }
  301. $msgbig = wfMsgHtml( 'show-big-image' );
  302. $msgsmall = wfMsgExt( 'show-big-image-thumb', 'parseinline',
  303. $wgLang->formatNum( $width ),
  304. $wgLang->formatNum( $height )
  305. );
  306. } else {
  307. # Image is small enough to show full size on image page
  308. $msgbig = htmlspecialchars( $this->displayImg->getName() );
  309. $msgsmall = wfMsgExt( 'file-nohires', array( 'parseinline' ) );
  310. }
  311. $params['width'] = $width;
  312. $thumbnail = $this->displayImg->transform( $params );
  313. $anchorclose = "<br />";
  314. if( $this->displayImg->mustRender() ) {
  315. $showLink = true;
  316. } else {
  317. $anchorclose .=
  318. $msgsmall .
  319. '<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc;
  320. }
  321. if( $this->displayImg->isMultipage() ) {
  322. $wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
  323. }
  324. if( $thumbnail ) {
  325. $options = array(
  326. 'alt' => $this->displayImg->getTitle()->getPrefixedText(),
  327. 'file-link' => true,
  328. );
  329. $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
  330. $thumbnail->toHtml( $options ) .
  331. $anchorclose . '</div>' );
  332. }
  333. if( $this->displayImg->isMultipage() ) {
  334. $count = $this->displayImg->pageCount();
  335. if( $page > 1 ) {
  336. $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
  337. $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
  338. $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
  339. array( 'page' => $page - 1 ) );
  340. } else {
  341. $thumb1 = '';
  342. }
  343. if( $page < $count ) {
  344. $label = wfMsg( 'imgmultipagenext' );
  345. $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
  346. $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
  347. array( 'page' => $page + 1 ) );
  348. } else {
  349. $thumb2 = '';
  350. }
  351. global $wgScript;
  352. $formParams = array(
  353. 'name' => 'pageselector',
  354. 'action' => $wgScript,
  355. 'onchange' => 'document.pageselector.submit();',
  356. );
  357. $option = array();
  358. for ( $i=1; $i <= $count; $i++ ) {
  359. $options[] = Xml::option( $wgLang->formatNum($i), $i, $i == $page );
  360. }
  361. $select = Xml::tags( 'select',
  362. array( 'id' => 'pageselector', 'name' => 'page' ),
  363. implode( "\n", $options ) );
  364. $wgOut->addHTML(
  365. '</td><td><div class="multipageimagenavbox">' .
  366. Xml::openElement( 'form', $formParams ) .
  367. Xml::hidden( 'title', $this->getTitle()->getPrefixedDbKey() ) .
  368. wfMsgExt( 'imgmultigoto', array( 'parseinline', 'replaceafter' ), $select ) .
  369. Xml::submitButton( wfMsg( 'imgmultigo' ) ) .
  370. Xml::closeElement( 'form' ) .
  371. "<hr />$thumb1\n$thumb2<br clear=\"all\" /></div></td></tr></table>"
  372. );
  373. }
  374. } else {
  375. #if direct link is allowed but it's not a renderable image, show an icon.
  376. if( $this->displayImg->isSafeFile() ) {
  377. $icon= $this->displayImg->iconThumb();
  378. $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
  379. $icon->toHtml( array( 'desc-link' => true ) ) .
  380. '</div>' );
  381. }
  382. $showLink = true;
  383. }
  384. if($showLink) {
  385. $filename = wfEscapeWikiText( $this->displayImg->getName() );
  386. if( !$this->displayImg->isSafeFile() ) {
  387. $warning = wfMsgNoTrans( 'mediawarning' );
  388. $wgOut->addWikiText( <<<EOT
  389. <div class="fullMedia">
  390. <span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark
  391. <span class="fileInfo"> $longDesc</span>
  392. </div>
  393. <div class="mediaWarning">$warning</div>
  394. EOT
  395. );
  396. } else {
  397. $wgOut->addWikiText( <<<EOT
  398. <div class="fullMedia">
  399. [[Media:$filename|$filename]]$dirmark <span class="fileInfo"> $longDesc</span>
  400. </div>
  401. EOT
  402. );
  403. }
  404. }
  405. if( !$this->displayImg->isLocal() ) {
  406. $this->printSharedImageText();
  407. }
  408. } else {
  409. # Image does not exist
  410. $title = SpecialPage::getTitleFor( 'Upload' );
  411. $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'),
  412. 'wpDestFile=' . urlencode( $this->displayImg->getName() ) );
  413. $wgOut->setRobotPolicy( 'noindex,nofollow' );
  414. $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) );
  415. }
  416. }
  417. /**
  418. * Show a notice that the file is from a shared repository
  419. */
  420. protected function printSharedImageText() {
  421. global $wgOut, $wgUser;
  422. $this->loadFile();
  423. $descUrl = $this->img->getDescriptionUrl();
  424. $descText = $this->img->getDescriptionText();
  425. $msg = '';
  426. if( $descUrl ) {
  427. $sk = $wgUser->getSkin();
  428. $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) );
  429. $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki';
  430. $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link );
  431. if( $msg == '-' ) {
  432. $msg = '';
  433. }
  434. }
  435. $s = "<div class='sharedUploadNotice'>";
  436. $s .= wfMsgWikiHtml( 'sharedupload', $this->img->getRepo()->getDisplayName(), $msg );
  437. $s .= "</div>";
  438. $wgOut->addHTML( $s );
  439. if( $descText ) {
  440. $this->mExtraDescription = $descText;
  441. }
  442. }
  443. public function getUploadUrl() {
  444. $this->loadFile();
  445. $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
  446. return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) . '&wpForReUpload=1' );
  447. }
  448. /**
  449. * Print out the various links at the bottom of the image page, e.g. reupload,
  450. * external editing (and instructions link) etc.
  451. */
  452. protected function uploadLinksBox() {
  453. global $wgUser, $wgOut;
  454. $this->loadFile();
  455. if( !$this->img->isLocal() )
  456. return;
  457. $sk = $wgUser->getSkin();
  458. $wgOut->addHTML( '<br /><ul>' );
  459. # "Upload a new version of this file" link
  460. if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) {
  461. $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
  462. $wgOut->addHTML( "<li><div class='plainlinks'>{$ulink}</div></li>" );
  463. }
  464. # External editing link
  465. $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' );
  466. $wgOut->addHTML( '<li>' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '</small></li>' );
  467. $wgOut->addHTML( '</ul>' );
  468. }
  469. protected function closeShowImage() {} # For overloading
  470. /**
  471. * If the page we've just displayed is in the "Image" namespace,
  472. * we follow it with an upload history of the image and its usage.
  473. */
  474. protected function imageHistory() {
  475. global $wgOut, $wgUseExternalEditor;
  476. $this->loadFile();
  477. $pager = new ImageHistoryPseudoPager( $this );
  478. $wgOut->addHTML( $pager->getBody() );
  479. $this->img->resetHistory(); // free db resources
  480. # Exist check because we don't want to show this on pages where an image
  481. # doesn't exist along with the noimage message, that would suck. -ævar
  482. if( $wgUseExternalEditor && $this->img->exists() ) {
  483. $this->uploadLinksBox();
  484. }
  485. }
  486. protected function imageLinks() {
  487. global $wgUser, $wgOut, $wgLang;
  488. $limit = 100;
  489. $dbr = wfGetDB( DB_SLAVE );
  490. $res = $dbr->select(
  491. array( 'imagelinks', 'page' ),
  492. array( 'page_namespace', 'page_title' ),
  493. array( 'il_to' => $this->mTitle->getDBkey(), 'il_from = page_id' ),
  494. __METHOD__,
  495. array( 'LIMIT' => $limit + 1)
  496. );
  497. $count = $dbr->numRows( $res );
  498. if( $count == 0 ) {
  499. $wgOut->addHTML( "<div id='mw-imagepage-nolinkstoimage'>\n" );
  500. $wgOut->addWikiMsg( 'nolinkstoimage' );
  501. $wgOut->addHTML( "</div>\n" );
  502. return;
  503. }
  504. $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
  505. if( $count <= $limit - 1 ) {
  506. $wgOut->addWikiMsg( 'linkstoimage', $count );
  507. } else {
  508. // More links than the limit. Add a link to [[Special:Whatlinkshere]]
  509. $wgOut->addWikiMsg( 'linkstoimage-more',
  510. $wgLang->formatNum( $limit ),
  511. $this->mTitle->getPrefixedDBkey()
  512. );
  513. }
  514. $wgOut->addHTML( "<ul class='mw-imagepage-linkstoimage'>\n" );
  515. $sk = $wgUser->getSkin();
  516. $count = 0;
  517. while ( $s = $res->fetchObject() ) {
  518. $count++;
  519. if( $count <= $limit ) {
  520. // We have not yet reached the extra one that tells us there is more to fetch
  521. $name = Title::makeTitle( $s->page_namespace, $s->page_title );
  522. $link = $sk->makeKnownLinkObj( $name, "" );
  523. $wgOut->addHTML( "<li>{$link}</li>\n" );
  524. }
  525. }
  526. $wgOut->addHTML( "</ul></div>\n" );
  527. $res->free();
  528. // Add a links to [[Special:Whatlinkshere]]
  529. if( $count > $limit )
  530. $wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() );
  531. }
  532. protected function imageRedirects() {
  533. global $wgUser, $wgOut, $wgLang;
  534. $redirects = $this->getTitle()->getRedirectsHere( NS_FILE );
  535. if( count( $redirects ) == 0 ) return;
  536. $wgOut->addHTML( "<div id='mw-imagepage-section-redirectstofile'>\n" );
  537. $wgOut->addWikiMsg( 'redirectstofile',
  538. $wgLang->formatNum( count( $redirects ) )
  539. );
  540. $wgOut->addHTML( "<ul class='mw-imagepage-redirectstofile'>\n" );
  541. $sk = $wgUser->getSkin();
  542. foreach ( $redirects as $title ) {
  543. $link = $sk->makeKnownLinkObj( $title, "", "redirect=no" );
  544. $wgOut->addHTML( "<li>{$link}</li>\n" );
  545. }
  546. $wgOut->addHTML( "</ul></div>\n" );
  547. }
  548. protected function imageDupes() {
  549. global $wgOut, $wgUser, $wgLang;
  550. $this->loadFile();
  551. $dupes = $this->getDuplicates();
  552. if( count( $dupes ) == 0 ) return;
  553. $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
  554. $wgOut->addWikiMsg( 'duplicatesoffile',
  555. $wgLang->formatNum( count( $dupes ) ), $this->mTitle->getDBkey()
  556. );
  557. $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
  558. $sk = $wgUser->getSkin();
  559. foreach ( $dupes as $file ) {
  560. $fromSrc = '';
  561. if( $file->isLocal() )
  562. $link = $sk->makeKnownLinkObj( $file->getTitle(), "" );
  563. else {
  564. $link = $sk->makeExternalLink( $file->getDescriptionUrl(),
  565. $file->getTitle()->getPrefixedText() );
  566. $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() );
  567. }
  568. $wgOut->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
  569. }
  570. $wgOut->addHTML( "</ul></div>\n" );
  571. }
  572. /**
  573. * Delete the file, or an earlier version of it
  574. */
  575. public function delete() {
  576. $this->loadFile();
  577. if( !$this->img->exists() || !$this->img->isLocal() || $this->img->getRedirected() ) {
  578. // Standard article deletion
  579. Article::delete();
  580. return;
  581. }
  582. $deleter = new FileDeleteForm( $this->img );
  583. $deleter->execute();
  584. }
  585. /**
  586. * Revert the file to an earlier version
  587. */
  588. public function revert() {
  589. $this->loadFile();
  590. $reverter = new FileRevertForm( $this->img );
  591. $reverter->execute();
  592. }
  593. /**
  594. * Override handling of action=purge
  595. */
  596. public function doPurge() {
  597. $this->loadFile();
  598. if( $this->img->exists() ) {
  599. wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" );
  600. $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
  601. $update->doUpdate();
  602. $this->img->upgradeRow();
  603. $this->img->purgeCache();
  604. } else {
  605. wfDebug( "ImagePage::doPurge no image\n" );
  606. }
  607. parent::doPurge();
  608. }
  609. /**
  610. * Display an error with a wikitext description
  611. */
  612. function showError( $description ) {
  613. global $wgOut;
  614. $wgOut->setPageTitle( wfMsg( "internalerror" ) );
  615. $wgOut->setRobotPolicy( "noindex,nofollow" );
  616. $wgOut->setArticleRelated( false );
  617. $wgOut->enableClientCache( false );
  618. $wgOut->addWikiText( $description );
  619. }
  620. }
  621. /**
  622. * Builds the image revision log shown on image pages
  623. *
  624. * @ingroup Media
  625. */
  626. class ImageHistoryList {
  627. protected $imagePage, $img, $skin, $title, $repo;
  628. public function __construct( $imagePage ) {
  629. global $wgUser;
  630. $this->skin = $wgUser->getSkin();
  631. $this->current = $imagePage->getFile();
  632. $this->img = $imagePage->getDisplayedFile();
  633. $this->title = $imagePage->getTitle();
  634. $this->imagePage = $imagePage;
  635. }
  636. public function getImagePage() {
  637. return $this->imagePage;
  638. }
  639. public function getSkin() {
  640. return $this->skin;
  641. }
  642. public function getFile() {
  643. return $this->img;
  644. }
  645. public function beginImageHistoryList( $navLinks = '' ) {
  646. global $wgOut, $wgUser;
  647. return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) )
  648. . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
  649. . $navLinks
  650. . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
  651. . '<tr><td></td>'
  652. . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' )
  653. . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
  654. . '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>'
  655. . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
  656. . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
  657. . '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
  658. . "</tr>\n";
  659. }
  660. public function endImageHistoryList( $navLinks = '' ) {
  661. return "</table>\n$navLinks\n";
  662. }
  663. public function imageHistoryLine( $iscur, $file ) {
  664. global $wgUser, $wgLang, $wgContLang, $wgTitle;
  665. $timestamp = wfTimestamp(TS_MW, $file->getTimestamp());
  666. $img = $iscur ? $file->getName() : $file->getArchiveName();
  667. $user = $file->getUser('id');
  668. $usertext = $file->getUser('text');
  669. $size = $file->getSize();
  670. $description = $file->getDescription();
  671. $dims = $file->getDimensionsString();
  672. $sha1 = $file->getSha1();
  673. $local = $this->current->isLocal();
  674. $row = $css = $selected = '';
  675. // Deletion link
  676. if( $local && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ) {
  677. $row .= '<td>';
  678. # Link to remove from history
  679. if( $wgUser->isAllowed( 'delete' ) ) {
  680. $q = array();
  681. $q[] = 'action=delete';
  682. if( !$iscur )
  683. $q[] = 'oldimage=' . urlencode( $img );
  684. $row .= $this->skin->makeKnownLinkObj(
  685. $this->title,
  686. wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
  687. implode( '&', $q )
  688. );
  689. }
  690. # Link to hide content
  691. if( $wgUser->isAllowed( 'deleterevision' ) ) {
  692. if( $wgUser->isAllowed('delete') ) {
  693. $row .= '<br/>';
  694. }
  695. $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
  696. // If file is top revision or locked from this user, don't link
  697. if( $iscur || !$file->userCan(File::DELETED_RESTRICTED) ) {
  698. $del = wfMsgHtml( 'rev-delundel' );
  699. } else {
  700. // If the file was hidden, link to sha-1
  701. list($ts,$name) = explode('!',$img,2);
  702. $del = $this->skin->makeKnownLinkObj( $revdel, wfMsg( 'rev-delundel' ),
  703. 'target=' . urlencode( $wgTitle->getPrefixedText() ) .
  704. '&oldimage=' . urlencode( $ts ) );
  705. // Bolden oversighted content
  706. if( $file->isDeleted(File::DELETED_RESTRICTED) )
  707. $del = "<strong>$del</strong>";
  708. }
  709. $row .= "<tt style='white-space: nowrap;'><small>$del</small></tt>";
  710. }
  711. $row .= '</td>';
  712. }
  713. // Reversion link/current indicator
  714. $row .= '<td>';
  715. if( $iscur ) {
  716. $row .= wfMsgHtml( 'filehist-current' );
  717. } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
  718. if( $file->isDeleted(File::DELETED_FILE) ) {
  719. $row .= wfMsgHtml('filehist-revert');
  720. } else {
  721. $q = array();
  722. $q[] = 'action=revert';
  723. $q[] = 'oldimage=' . urlencode( $img );
  724. $q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) );
  725. $row .= $this->skin->makeKnownLinkObj( $this->title,
  726. wfMsgHtml( 'filehist-revert' ),
  727. implode( '&', $q ) );
  728. }
  729. }
  730. $row .= '</td>';
  731. // Date/time and image link
  732. if( $file->getTimestamp() === $this->img->getTimestamp() ) {
  733. $selected = "class='filehistory-selected'";
  734. }
  735. $row .= "<td $selected style='white-space: nowrap;'>";
  736. if( !$file->userCan(File::DELETED_FILE) ) {
  737. # Don't link to unviewable files
  738. $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>';
  739. } else if( $file->isDeleted(File::DELETED_FILE) ) {
  740. $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
  741. # Make a link to review the image
  742. $url = $this->skin->makeKnownLinkObj( $revdel, $wgLang->timeAndDate( $timestamp, true ),
  743. "target=".$wgTitle->getPrefixedText()."&file=$sha1.".$this->current->getExtension() );
  744. $row .= '<span class="history-deleted">'.$url.'</span>';
  745. } else {
  746. $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img );
  747. $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) );
  748. }
  749. // Thumbnail
  750. if( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
  751. $params = array(
  752. 'width' => '120',
  753. 'height' => '120',
  754. );
  755. $thumbnail = $file->transform( $params );
  756. $options = array(
  757. 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ),
  758. 'file-link' => true,
  759. );
  760. $row .= '</td><td>' . ( $thumbnail ? $thumbnail->toHtml( $options ) :
  761. wfMsgHtml( 'filehist-nothumb' ) );
  762. } else {
  763. $row .= '</td><td>' . wfMsgHtml( 'filehist-nothumb' );
  764. }
  765. $row .= "</td><td>";
  766. // Image dimensions
  767. $row .= htmlspecialchars( $dims );
  768. // File size
  769. $row .= " <span style='white-space: nowrap;'>(" . $this->skin->formatSize( $size ) . ')</span>';
  770. // Uploading user
  771. $row .= '</td><td>';
  772. if( $local ) {
  773. // Hide deleted usernames
  774. if( $file->isDeleted(File::DELETED_USER) ) {
  775. $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
  776. } else {
  777. $row .= $this->skin->userLink( $user, $usertext ) . " <span style='white-space: nowrap;'>" .
  778. $this->skin->userToolLinks( $user, $usertext ) . "</span>";
  779. }
  780. } else {
  781. $row .= htmlspecialchars( $usertext );
  782. }
  783. $row .= '</td><td>';
  784. // Don't show deleted descriptions
  785. if( $file->isDeleted(File::DELETED_COMMENT) ) {
  786. $row .= '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
  787. } else {
  788. $row .= $this->skin->commentBlock( $description, $this->title );
  789. }
  790. $row .= '</td>';
  791. wfRunHooks( 'ImagePageFileHistoryLine', array( $this, $file, &$row, &$rowClass ) );
  792. $classAttr = $rowClass ? " class='$rowClass'" : "";
  793. return "<tr{$classAttr}>{$row}</tr>\n";
  794. }
  795. }
  796. class ImageHistoryPseudoPager extends ReverseChronologicalPager {
  797. function __construct( $imagePage ) {
  798. parent::__construct();
  799. $this->mImagePage = $imagePage;
  800. $this->mTitle = clone( $imagePage->getTitle() );
  801. $this->mTitle->setFragment( '#filehistory' );
  802. $this->mImg = NULL;
  803. $this->mHist = array();
  804. $this->mRange = array( 0, 0 ); // display range
  805. }
  806. function getTitle() {
  807. return $this->mTitle;
  808. }
  809. function getQueryInfo() {
  810. return false;
  811. }
  812. function getIndexField() {
  813. return '';
  814. }
  815. function formatRow( $row ) {
  816. return '';
  817. }
  818. function getBody() {
  819. $s = '';
  820. $this->doQuery();
  821. if( count($this->mHist) ) {
  822. $list = new ImageHistoryList( $this->mImagePage );
  823. # Generate prev/next links
  824. $navLink = $this->getNavigationBar();
  825. $s = $list->beginImageHistoryList($navLink);
  826. // Skip rows there just for paging links
  827. for( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
  828. $file = $this->mHist[$i];
  829. $s .= $list->imageHistoryLine( !$file->isOld(), $file );
  830. }
  831. $s .= $list->endImageHistoryList($navLink);
  832. }
  833. return $s;
  834. }
  835. function doQuery() {
  836. if( $this->mQueryDone ) return;
  837. $this->mImg = $this->mImagePage->getFile(); // ensure loading
  838. if( !$this->mImg->exists() ) {
  839. return;
  840. }
  841. $queryLimit = $this->mLimit + 1; // limit plus extra row
  842. if( $this->mIsBackwards ) {
  843. // Fetch the file history
  844. $this->mHist = $this->mImg->getHistory($queryLimit,null,$this->mOffset,false);
  845. // The current rev may not meet the offset/limit
  846. $numRows = count($this->mHist);
  847. if( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) {
  848. $this->mHist = array_merge( array($this->mImg), $this->mHist );
  849. }
  850. } else {
  851. // The current rev may not meet the offset
  852. if( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) {
  853. $this->mHist[] = $this->mImg;
  854. }
  855. // Old image versions (fetch extra row for nav links)
  856. $oiLimit = count($this->mHist) ? $this->mLimit : $this->mLimit+1;
  857. // Fetch the file history
  858. $this->mHist = array_merge( $this->mHist,
  859. $this->mImg->getHistory($oiLimit,$this->mOffset,null,false) );
  860. }
  861. $numRows = count($this->mHist); // Total number of query results
  862. if( $numRows ) {
  863. # Index value of top item in the list
  864. $firstIndex = $this->mIsBackwards ?
  865. $this->mHist[$numRows-1]->getTimestamp() : $this->mHist[0]->getTimestamp();
  866. # Discard the extra result row if there is one
  867. if( $numRows > $this->mLimit && $numRows > 1 ) {
  868. if( $this->mIsBackwards ) {
  869. # Index value of item past the index
  870. $this->mPastTheEndIndex = $this->mHist[0]->getTimestamp();
  871. # Index value of bottom item in the list
  872. $lastIndex = $this->mHist[1]->getTimestamp();
  873. # Display range
  874. $this->mRange = array( 1, $numRows-1 );
  875. } else {
  876. # Index value of item past the index
  877. $this->mPastTheEndIndex = $this->mHist[$numRows-1]->getTimestamp();
  878. # Index value of bottom item in the list
  879. $lastIndex = $this->mHist[$numRows-2]->getTimestamp();
  880. # Display range
  881. $this->mRange = array( 0, $numRows-2 );
  882. }
  883. } else {
  884. # Setting indexes to an empty string means that they will be
  885. # omitted if they would otherwise appear in URLs. It just so
  886. # happens that this is the right thing to do in the standard
  887. # UI, in all the relevant cases.
  888. $this->mPastTheEndIndex = '';
  889. # Index value of bottom item in the list
  890. $lastIndex = $this->mIsBackwards ?
  891. $this->mHist[0]->getTimestamp() : $this->mHist[$numRows-1]->getTimestamp();
  892. # Display range
  893. $this->mRange = array( 0, $numRows-1 );
  894. }
  895. } else {
  896. $firstIndex = '';
  897. $lastIndex = '';
  898. $this->mPastTheEndIndex = '';
  899. }
  900. if( $this->mIsBackwards ) {
  901. $this->mIsFirst = ( $numRows < $queryLimit );
  902. $this->mIsLast = ( $this->mOffset == '' );
  903. $this->mLastShown = $firstIndex;
  904. $this->mFirstShown = $lastIndex;
  905. } else {
  906. $this->mIsFirst = ( $this->mOffset == '' );
  907. $this->mIsLast = ( $numRows < $queryLimit );
  908. $this->mLastShown = $lastIndex;
  909. $this->mFirstShown = $firstIndex;
  910. }
  911. $this->mQueryDone = true;
  912. }
  913. }