SpecialFileDuplicateSearch.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. /**
  4. * Implements Special:FileDuplicateSearch
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License along
  17. * with this program; if not, write to the Free Software Foundation, Inc.,
  18. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. * http://www.gnu.org/copyleft/gpl.html
  20. *
  21. * @file
  22. * @ingroup SpecialPage
  23. * @author Raimond Spekking, based on Special:MIMESearch by Ævar Arnfjörð Bjarmason
  24. */
  25. /**
  26. * Searches the database for files of the requested hash, comparing this with the
  27. * 'img_sha1' field in the image table.
  28. *
  29. * @ingroup SpecialPage
  30. */
  31. class SpecialFileDuplicateSearch extends QueryPage {
  32. protected $hash = '', $filename = '';
  33. /**
  34. * @var File $file selected reference file, if present
  35. */
  36. protected $file = null;
  37. function __construct( $name = 'FileDuplicateSearch' ) {
  38. parent::__construct( $name );
  39. }
  40. function isSyndicated() {
  41. return false;
  42. }
  43. function isCacheable() {
  44. return false;
  45. }
  46. public function isCached() {
  47. return false;
  48. }
  49. function linkParameters() {
  50. return [ 'filename' => $this->filename ];
  51. }
  52. /**
  53. * Fetch dupes from all connected file repositories.
  54. *
  55. * @return array Array of File objects
  56. */
  57. function getDupes() {
  58. return RepoGroup::singleton()->findBySha1( $this->hash );
  59. }
  60. /**
  61. *
  62. * @param array $dupes Array of File objects
  63. */
  64. function showList( $dupes ) {
  65. $html = [];
  66. $html[] = $this->openList( 0 );
  67. foreach ( $dupes as $dupe ) {
  68. $line = $this->formatResult( null, $dupe );
  69. $html[] = "<li>" . $line . "</li>";
  70. }
  71. $html[] = $this->closeList();
  72. $this->getOutput()->addHTML( implode( "\n", $html ) );
  73. }
  74. public function getQueryInfo() {
  75. $imgQuery = LocalFile::getQueryInfo();
  76. return [
  77. 'tables' => $imgQuery['tables'],
  78. 'fields' => [
  79. 'title' => 'img_name',
  80. 'value' => 'img_sha1',
  81. 'img_user_text' => $imgQuery['fields']['img_user_text'],
  82. 'img_timestamp'
  83. ],
  84. 'conds' => [ 'img_sha1' => $this->hash ],
  85. 'join_conds' => $imgQuery['joins'],
  86. ];
  87. }
  88. public function execute( $par ) {
  89. $this->setHeaders();
  90. $this->outputHeader();
  91. $this->filename = $par ?? $this->getRequest()->getText( 'filename' );
  92. $this->file = null;
  93. $this->hash = '';
  94. $title = Title::newFromText( $this->filename, NS_FILE );
  95. if ( $title && $title->getText() != '' ) {
  96. $this->file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
  97. }
  98. $out = $this->getOutput();
  99. # Create the input form
  100. $formFields = [
  101. 'filename' => [
  102. 'type' => 'text',
  103. 'name' => 'filename',
  104. 'label-message' => 'fileduplicatesearch-filename',
  105. 'id' => 'filename',
  106. 'size' => 50,
  107. 'default' => $this->filename,
  108. ],
  109. ];
  110. $hiddenFields = [
  111. 'title' => $this->getPageTitle()->getPrefixedDBkey(),
  112. ];
  113. $htmlForm = HTMLForm::factory( 'ooui', $formFields, $this->getContext() );
  114. $htmlForm->addHiddenFields( $hiddenFields );
  115. $htmlForm->setAction( wfScript() );
  116. $htmlForm->setMethod( 'get' );
  117. $htmlForm->setSubmitTextMsg( $this->msg( 'fileduplicatesearch-submit' ) );
  118. // The form should be visible always, even if it was submitted (e.g. to perform another action).
  119. // To bypass the callback validation of HTMLForm, use prepareForm() and displayForm().
  120. $htmlForm->prepareForm()->displayForm( false );
  121. if ( $this->file ) {
  122. $this->hash = $this->file->getSha1();
  123. } elseif ( $this->filename !== '' ) {
  124. $out->wrapWikiMsg(
  125. "<p class='mw-fileduplicatesearch-noresults'>\n$1\n</p>",
  126. [ 'fileduplicatesearch-noresults', wfEscapeWikiText( $this->filename ) ]
  127. );
  128. }
  129. if ( $this->hash != '' ) {
  130. # Show a thumbnail of the file
  131. $img = $this->file;
  132. if ( $img ) {
  133. $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] );
  134. if ( $thumb ) {
  135. $out->addModuleStyles( 'mediawiki.special' );
  136. $out->addHTML( '<div id="mw-fileduplicatesearch-icon">' .
  137. $thumb->toHtml( [ 'desc-link' => false ] ) . '<br />' .
  138. $this->msg( 'fileduplicatesearch-info' )->numParams(
  139. $img->getWidth(), $img->getHeight() )->params(
  140. $this->getLanguage()->formatSize( $img->getSize() ),
  141. $img->getMimeType() )->parseAsBlock() .
  142. '</div>' );
  143. }
  144. }
  145. $dupes = $this->getDupes();
  146. $numRows = count( $dupes );
  147. # Show a short summary
  148. if ( $numRows == 1 ) {
  149. $out->wrapWikiMsg(
  150. "<p class='mw-fileduplicatesearch-result-1'>\n$1\n</p>",
  151. [ 'fileduplicatesearch-result-1', wfEscapeWikiText( $this->filename ) ]
  152. );
  153. } elseif ( $numRows ) {
  154. $out->wrapWikiMsg(
  155. "<p class='mw-fileduplicatesearch-result-n'>\n$1\n</p>",
  156. [ 'fileduplicatesearch-result-n', wfEscapeWikiText( $this->filename ),
  157. $this->getLanguage()->formatNum( $numRows - 1 ) ]
  158. );
  159. }
  160. $this->doBatchLookups( $dupes );
  161. $this->showList( $dupes );
  162. }
  163. }
  164. function doBatchLookups( $list ) {
  165. $batch = new LinkBatch();
  166. /** @var File $file */
  167. foreach ( $list as $file ) {
  168. $batch->addObj( $file->getTitle() );
  169. if ( $file->isLocal() ) {
  170. $userName = $file->getUser( 'text' );
  171. $batch->add( NS_USER, $userName );
  172. $batch->add( NS_USER_TALK, $userName );
  173. }
  174. }
  175. $batch->execute();
  176. }
  177. /**
  178. *
  179. * @param Skin $skin
  180. * @param File $result
  181. * @return string HTML
  182. */
  183. function formatResult( $skin, $result ) {
  184. $linkRenderer = $this->getLinkRenderer();
  185. $nt = $result->getTitle();
  186. $text = MediaWikiServices::getInstance()->getContentLanguage()->convert(
  187. htmlspecialchars( $nt->getText() )
  188. );
  189. $plink = $linkRenderer->makeLink(
  190. $nt,
  191. new HtmlArmor( $text )
  192. );
  193. $userText = $result->getUser( 'text' );
  194. if ( $result->isLocal() ) {
  195. $userId = $result->getUser( 'id' );
  196. $user = Linker::userLink( $userId, $userText );
  197. $user .= '<span style="white-space: nowrap;">';
  198. $user .= Linker::userToolLinks( $userId, $userText );
  199. $user .= '</span>';
  200. } else {
  201. $user = htmlspecialchars( $userText );
  202. }
  203. $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate(
  204. $result->getTimestamp(), $this->getUser() ) );
  205. return "$plink . . $user . . $time";
  206. }
  207. /**
  208. * Return an array of subpages beginning with $search that this special page will accept.
  209. *
  210. * @param string $search Prefix to search for
  211. * @param int $limit Maximum number of results to return (usually 10)
  212. * @param int $offset Number of results to skip (usually 0)
  213. * @return string[] Matching subpages
  214. */
  215. public function prefixSearchSubpages( $search, $limit, $offset ) {
  216. $title = Title::newFromText( $search, NS_FILE );
  217. if ( !$title || $title->getNamespace() !== NS_FILE ) {
  218. // No prefix suggestion outside of file namespace
  219. return [];
  220. }
  221. $searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
  222. $searchEngine->setLimitOffset( $limit, $offset );
  223. // Autocomplete subpage the same as a normal search, but just for files
  224. $searchEngine->setNamespaces( [ NS_FILE ] );
  225. $result = $searchEngine->defaultPrefixSearch( $search );
  226. return array_map( function ( Title $t ) {
  227. // Remove namespace in search suggestion
  228. return $t->getText();
  229. }, $result );
  230. }
  231. protected function getGroupName() {
  232. return 'media';
  233. }
  234. }