Linker.php 72 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174
  1. <?php
  2. /**
  3. * Methods to make links and related items.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\Linker\LinkTarget;
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * Some internal bits split of from Skin.php. These functions are used
  26. * for primarily page content: links, embedded images, table of contents. Links
  27. * are also used in the skin.
  28. *
  29. * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
  30. *
  31. * @ingroup Skins
  32. */
  33. class Linker {
  34. /**
  35. * Flags for userToolLinks()
  36. */
  37. const TOOL_LINKS_NOBLOCK = 1;
  38. const TOOL_LINKS_EMAIL = 2;
  39. /**
  40. * Return the CSS colour of a known link
  41. *
  42. * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead
  43. *
  44. * @since 1.16.3
  45. * @param LinkTarget $t
  46. * @param int $threshold User defined threshold
  47. * @return string CSS class
  48. */
  49. public static function getLinkColour( LinkTarget $t, $threshold ) {
  50. wfDeprecated( __METHOD__, '1.28' );
  51. $services = MediaWikiServices::getInstance();
  52. $linkRenderer = $services->getLinkRenderer();
  53. if ( $threshold !== $linkRenderer->getStubThreshold() ) {
  54. // Need to create a new instance with the right stub threshold...
  55. $linkRenderer = $services->getLinkRendererFactory()->create();
  56. $linkRenderer->setStubThreshold( $threshold );
  57. }
  58. return $linkRenderer->getLinkClasses( $t );
  59. }
  60. /**
  61. * This function returns an HTML link to the given target. It serves a few
  62. * purposes:
  63. * 1) If $target is a Title, the correct URL to link to will be figured
  64. * out automatically.
  65. * 2) It automatically adds the usual classes for various types of link
  66. * targets: "new" for red links, "stub" for short articles, etc.
  67. * 3) It escapes all attribute values safely so there's no risk of XSS.
  68. * 4) It provides a default tooltip if the target is a Title (the page
  69. * name of the target).
  70. * link() replaces the old functions in the makeLink() family.
  71. *
  72. * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
  73. * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead
  74. *
  75. * @param LinkTarget $target Can currently only be a LinkTarget, but this may
  76. * change to support Images, literal URLs, etc.
  77. * @param string $html The HTML contents of the <a> element, i.e.,
  78. * the link text. This is raw HTML and will not be escaped. If null,
  79. * defaults to the prefixed text of the Title; or if the Title is just a
  80. * fragment, the contents of the fragment.
  81. * @param array $customAttribs A key => value array of extra HTML attributes,
  82. * such as title and class. (href is ignored.) Classes will be
  83. * merged with the default classes, while other attributes will replace
  84. * default attributes. All passed attribute values will be HTML-escaped.
  85. * A false attribute value means to suppress that attribute.
  86. * @param array $query The query string to append to the URL
  87. * you're linking to, in key => value array form. Query keys and values
  88. * will be URL-encoded.
  89. * @param string|array $options String or array of strings:
  90. * 'known': Page is known to exist, so don't check if it does.
  91. * 'broken': Page is known not to exist, so don't check if it does.
  92. * 'noclasses': Don't add any classes automatically (includes "new",
  93. * "stub", "mw-redirect", "extiw"). Only use the class attribute
  94. * provided, if any, so you get a simple blue link with no funny i-
  95. * cons.
  96. * 'forcearticlepath': Use the article path always, even with a querystring.
  97. * Has compatibility issues on some setups, so avoid wherever possible.
  98. * 'http': Force a full URL with http:// as the scheme.
  99. * 'https': Force a full URL with https:// as the scheme.
  100. * 'stubThreshold' => (int): Stub threshold to use when determining link classes.
  101. * @return string HTML <a> attribute
  102. */
  103. public static function link(
  104. $target, $html = null, $customAttribs = [], $query = [], $options = []
  105. ) {
  106. if ( !$target instanceof LinkTarget ) {
  107. wfWarn( __METHOD__ . ': Requires $target to be a LinkTarget object.', 2 );
  108. return "<!-- ERROR -->$html";
  109. }
  110. if ( is_string( $query ) ) {
  111. // some functions withing core using this still hand over query strings
  112. wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
  113. $query = wfCgiToArray( $query );
  114. }
  115. $services = MediaWikiServices::getInstance();
  116. $options = (array)$options;
  117. if ( $options ) {
  118. // Custom options, create new LinkRenderer
  119. if ( !isset( $options['stubThreshold'] ) ) {
  120. $defaultLinkRenderer = $services->getLinkRenderer();
  121. $options['stubThreshold'] = $defaultLinkRenderer->getStubThreshold();
  122. }
  123. $linkRenderer = $services->getLinkRendererFactory()
  124. ->createFromLegacyOptions( $options );
  125. } else {
  126. $linkRenderer = $services->getLinkRenderer();
  127. }
  128. if ( $html !== null ) {
  129. $text = new HtmlArmor( $html );
  130. } else {
  131. $text = $html; // null
  132. }
  133. if ( in_array( 'known', $options, true ) ) {
  134. return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
  135. } elseif ( in_array( 'broken', $options, true ) ) {
  136. return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
  137. } elseif ( in_array( 'noclasses', $options, true ) ) {
  138. return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query );
  139. } else {
  140. return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
  141. }
  142. }
  143. /**
  144. * Identical to link(), except $options defaults to 'known'.
  145. *
  146. * @since 1.16.3
  147. * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead
  148. * @see Linker::link
  149. * @param Title $target
  150. * @param string $html
  151. * @param array $customAttribs
  152. * @param array $query
  153. * @param string|array $options
  154. * @return string
  155. */
  156. public static function linkKnown(
  157. $target, $html = null, $customAttribs = [],
  158. $query = [], $options = [ 'known' ]
  159. ) {
  160. return self::link( $target, $html, $customAttribs, $query, $options );
  161. }
  162. /**
  163. * Make appropriate markup for a link to the current article. This is since
  164. * MediaWiki 1.29.0 rendered as an <a> tag without an href and with a class
  165. * showing the link text. The calling sequence is the same as for the other
  166. * make*LinkObj static functions, but $query is not used.
  167. *
  168. * @since 1.16.3
  169. * @param Title $nt
  170. * @param string $html [optional]
  171. * @param string $query [optional]
  172. * @param string $trail [optional]
  173. * @param string $prefix [optional]
  174. *
  175. * @return string
  176. */
  177. public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
  178. $ret = "<a class=\"mw-selflink selflink\">{$prefix}{$html}</a>{$trail}";
  179. if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) {
  180. return $ret;
  181. }
  182. if ( $html == '' ) {
  183. $html = htmlspecialchars( $nt->getPrefixedText() );
  184. }
  185. list( $inside, $trail ) = self::splitTrail( $trail );
  186. return "<a class=\"mw-selflink selflink\">{$prefix}{$html}{$inside}</a>{$trail}";
  187. }
  188. /**
  189. * Get a message saying that an invalid title was encountered.
  190. * This should be called after a method like Title::makeTitleSafe() returned
  191. * a value indicating that the title object is invalid.
  192. *
  193. * @param IContextSource $context Context to use to get the messages
  194. * @param int $namespace Namespace number
  195. * @param string $title Text of the title, without the namespace part
  196. * @return string
  197. */
  198. public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
  199. global $wgContLang;
  200. // First we check whether the namespace exists or not.
  201. if ( MWNamespace::exists( $namespace ) ) {
  202. if ( $namespace == NS_MAIN ) {
  203. $name = $context->msg( 'blanknamespace' )->text();
  204. } else {
  205. $name = $wgContLang->getFormattedNsText( $namespace );
  206. }
  207. return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
  208. } else {
  209. return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
  210. }
  211. }
  212. /**
  213. * @since 1.16.3
  214. * @param LinkTarget $target
  215. * @return LinkTarget
  216. */
  217. public static function normaliseSpecialPage( LinkTarget $target ) {
  218. if ( $target->getNamespace() == NS_SPECIAL && !$target->isExternal() ) {
  219. list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $target->getDBkey() );
  220. if ( !$name ) {
  221. return $target;
  222. }
  223. $ret = SpecialPage::getTitleValueFor( $name, $subpage, $target->getFragment() );
  224. return $ret;
  225. } else {
  226. return $target;
  227. }
  228. }
  229. /**
  230. * Returns the filename part of an url.
  231. * Used as alternative text for external images.
  232. *
  233. * @param string $url
  234. *
  235. * @return string
  236. */
  237. private static function fnamePart( $url ) {
  238. $basename = strrchr( $url, '/' );
  239. if ( false === $basename ) {
  240. $basename = $url;
  241. } else {
  242. $basename = substr( $basename, 1 );
  243. }
  244. return $basename;
  245. }
  246. /**
  247. * Return the code for images which were added via external links,
  248. * via Parser::maybeMakeExternalImage().
  249. *
  250. * @since 1.16.3
  251. * @param string $url
  252. * @param string $alt
  253. *
  254. * @return string
  255. */
  256. public static function makeExternalImage( $url, $alt = '' ) {
  257. if ( $alt == '' ) {
  258. $alt = self::fnamePart( $url );
  259. }
  260. $img = '';
  261. $success = Hooks::run( 'LinkerMakeExternalImage', [ &$url, &$alt, &$img ] );
  262. if ( !$success ) {
  263. wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
  264. . "with url {$url} and alt text {$alt} to {$img}\n", true );
  265. return $img;
  266. }
  267. return Html::element( 'img',
  268. [
  269. 'src' => $url,
  270. 'alt' => $alt ] );
  271. }
  272. /**
  273. * Given parameters derived from [[Image:Foo|options...]], generate the
  274. * HTML that that syntax inserts in the page.
  275. *
  276. * @param Parser $parser
  277. * @param Title $title Title object of the file (not the currently viewed page)
  278. * @param File $file File object, or false if it doesn't exist
  279. * @param array $frameParams Associative array of parameters external to the media handler.
  280. * Boolean parameters are indicated by presence or absence, the value is arbitrary and
  281. * will often be false.
  282. * thumbnail If present, downscale and frame
  283. * manualthumb Image name to use as a thumbnail, instead of automatic scaling
  284. * framed Shows image in original size in a frame
  285. * frameless Downscale but don't frame
  286. * upright If present, tweak default sizes for portrait orientation
  287. * upright_factor Fudge factor for "upright" tweak (default 0.75)
  288. * border If present, show a border around the image
  289. * align Horizontal alignment (left, right, center, none)
  290. * valign Vertical alignment (baseline, sub, super, top, text-top, middle,
  291. * bottom, text-bottom)
  292. * alt Alternate text for image (i.e. alt attribute). Plain text.
  293. * class HTML for image classes. Plain text.
  294. * caption HTML for image caption.
  295. * link-url URL to link to
  296. * link-title Title object to link to
  297. * link-target Value for the target attribute, only with link-url
  298. * no-link Boolean, suppress description link
  299. *
  300. * @param array $handlerParams Associative array of media handler parameters, to be passed
  301. * to transform(). Typical keys are "width" and "page".
  302. * @param string|bool $time Timestamp of the file, set as false for current
  303. * @param string $query Query params for desc url
  304. * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
  305. * @since 1.20
  306. * @return string HTML for an image, with links, wrappers, etc.
  307. */
  308. public static function makeImageLink( Parser $parser, Title $title,
  309. $file, $frameParams = [], $handlerParams = [], $time = false,
  310. $query = "", $widthOption = null
  311. ) {
  312. $res = null;
  313. $dummy = new DummyLinker;
  314. if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
  315. &$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
  316. return $res;
  317. }
  318. if ( $file && !$file->allowInlineDisplay() ) {
  319. wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
  320. return self::link( $title );
  321. }
  322. // Clean up parameters
  323. $page = isset( $handlerParams['page'] ) ? $handlerParams['page'] : false;
  324. if ( !isset( $frameParams['align'] ) ) {
  325. $frameParams['align'] = '';
  326. }
  327. if ( !isset( $frameParams['alt'] ) ) {
  328. $frameParams['alt'] = '';
  329. }
  330. if ( !isset( $frameParams['title'] ) ) {
  331. $frameParams['title'] = '';
  332. }
  333. if ( !isset( $frameParams['class'] ) ) {
  334. $frameParams['class'] = '';
  335. }
  336. $prefix = $postfix = '';
  337. if ( 'center' == $frameParams['align'] ) {
  338. $prefix = '<div class="center">';
  339. $postfix = '</div>';
  340. $frameParams['align'] = 'none';
  341. }
  342. if ( $file && !isset( $handlerParams['width'] ) ) {
  343. if ( isset( $handlerParams['height'] ) && $file->isVectorized() ) {
  344. // If its a vector image, and user only specifies height
  345. // we don't want it to be limited by its "normal" width.
  346. global $wgSVGMaxSize;
  347. $handlerParams['width'] = $wgSVGMaxSize;
  348. } else {
  349. $handlerParams['width'] = $file->getWidth( $page );
  350. }
  351. if ( isset( $frameParams['thumbnail'] )
  352. || isset( $frameParams['manualthumb'] )
  353. || isset( $frameParams['framed'] )
  354. || isset( $frameParams['frameless'] )
  355. || !$handlerParams['width']
  356. ) {
  357. global $wgThumbLimits, $wgThumbUpright;
  358. if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
  359. $widthOption = User::getDefaultOption( 'thumbsize' );
  360. }
  361. // Reduce width for upright images when parameter 'upright' is used
  362. if ( isset( $frameParams['upright'] ) && $frameParams['upright'] == 0 ) {
  363. $frameParams['upright'] = $wgThumbUpright;
  364. }
  365. // For caching health: If width scaled down due to upright
  366. // parameter, round to full __0 pixel to avoid the creation of a
  367. // lot of odd thumbs.
  368. $prefWidth = isset( $frameParams['upright'] ) ?
  369. round( $wgThumbLimits[$widthOption] * $frameParams['upright'], -1 ) :
  370. $wgThumbLimits[$widthOption];
  371. // Use width which is smaller: real image width or user preference width
  372. // Unless image is scalable vector.
  373. if ( !isset( $handlerParams['height'] ) && ( $handlerParams['width'] <= 0 ||
  374. $prefWidth < $handlerParams['width'] || $file->isVectorized() ) ) {
  375. $handlerParams['width'] = $prefWidth;
  376. }
  377. }
  378. }
  379. if ( isset( $frameParams['thumbnail'] ) || isset( $frameParams['manualthumb'] )
  380. || isset( $frameParams['framed'] )
  381. ) {
  382. # Create a thumbnail. Alignment depends on the writing direction of
  383. # the page content language (right-aligned for LTR languages,
  384. # left-aligned for RTL languages)
  385. # If a thumbnail width has not been provided, it is set
  386. # to the default user option as specified in Language*.php
  387. if ( $frameParams['align'] == '' ) {
  388. $frameParams['align'] = $parser->getTargetLanguage()->alignEnd();
  389. }
  390. return $prefix .
  391. self::makeThumbLink2( $title, $file, $frameParams, $handlerParams, $time, $query ) .
  392. $postfix;
  393. }
  394. if ( $file && isset( $frameParams['frameless'] ) ) {
  395. $srcWidth = $file->getWidth( $page );
  396. # For "frameless" option: do not present an image bigger than the
  397. # source (for bitmap-style images). This is the same behavior as the
  398. # "thumb" option does it already.
  399. if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
  400. $handlerParams['width'] = $srcWidth;
  401. }
  402. }
  403. if ( $file && isset( $handlerParams['width'] ) ) {
  404. # Create a resized image, without the additional thumbnail features
  405. $thumb = $file->transform( $handlerParams );
  406. } else {
  407. $thumb = false;
  408. }
  409. if ( !$thumb ) {
  410. $s = self::makeBrokenImageLinkObj( $title, $frameParams['title'], '', '', '', $time == true );
  411. } else {
  412. self::processResponsiveImages( $file, $thumb, $handlerParams );
  413. $params = [
  414. 'alt' => $frameParams['alt'],
  415. 'title' => $frameParams['title'],
  416. 'valign' => isset( $frameParams['valign'] ) ? $frameParams['valign'] : false,
  417. 'img-class' => $frameParams['class'] ];
  418. if ( isset( $frameParams['border'] ) ) {
  419. $params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
  420. }
  421. $params = self::getImageLinkMTOParams( $frameParams, $query, $parser ) + $params;
  422. $s = $thumb->toHtml( $params );
  423. }
  424. if ( $frameParams['align'] != '' ) {
  425. $s = "<div class=\"float{$frameParams['align']}\">{$s}</div>";
  426. }
  427. return str_replace( "\n", ' ', $prefix . $s . $postfix );
  428. }
  429. /**
  430. * Get the link parameters for MediaTransformOutput::toHtml() from given
  431. * frame parameters supplied by the Parser.
  432. * @param array $frameParams The frame parameters
  433. * @param string $query An optional query string to add to description page links
  434. * @param Parser|null $parser
  435. * @return array
  436. */
  437. private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
  438. $mtoParams = [];
  439. if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
  440. $mtoParams['custom-url-link'] = $frameParams['link-url'];
  441. if ( isset( $frameParams['link-target'] ) ) {
  442. $mtoParams['custom-target-link'] = $frameParams['link-target'];
  443. }
  444. if ( $parser ) {
  445. $extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
  446. foreach ( $extLinkAttrs as $name => $val ) {
  447. // Currently could include 'rel' and 'target'
  448. $mtoParams['parser-extlink-' . $name] = $val;
  449. }
  450. }
  451. } elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
  452. $mtoParams['custom-title-link'] = Title::newFromLinkTarget(
  453. self::normaliseSpecialPage( $frameParams['link-title'] )
  454. );
  455. } elseif ( !empty( $frameParams['no-link'] ) ) {
  456. // No link
  457. } else {
  458. $mtoParams['desc-link'] = true;
  459. $mtoParams['desc-query'] = $query;
  460. }
  461. return $mtoParams;
  462. }
  463. /**
  464. * Make HTML for a thumbnail including image, border and caption
  465. * @param Title $title
  466. * @param File|bool $file File object or false if it doesn't exist
  467. * @param string $label
  468. * @param string $alt
  469. * @param string $align
  470. * @param array $params
  471. * @param bool $framed
  472. * @param string $manualthumb
  473. * @return string
  474. */
  475. public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
  476. $align = 'right', $params = [], $framed = false, $manualthumb = ""
  477. ) {
  478. $frameParams = [
  479. 'alt' => $alt,
  480. 'caption' => $label,
  481. 'align' => $align
  482. ];
  483. if ( $framed ) {
  484. $frameParams['framed'] = true;
  485. }
  486. if ( $manualthumb ) {
  487. $frameParams['manualthumb'] = $manualthumb;
  488. }
  489. return self::makeThumbLink2( $title, $file, $frameParams, $params );
  490. }
  491. /**
  492. * @param Title $title
  493. * @param File $file
  494. * @param array $frameParams
  495. * @param array $handlerParams
  496. * @param bool $time
  497. * @param string $query
  498. * @return string
  499. */
  500. public static function makeThumbLink2( Title $title, $file, $frameParams = [],
  501. $handlerParams = [], $time = false, $query = ""
  502. ) {
  503. $exists = $file && $file->exists();
  504. $page = isset( $handlerParams['page'] ) ? $handlerParams['page'] : false;
  505. if ( !isset( $frameParams['align'] ) ) {
  506. $frameParams['align'] = 'right';
  507. }
  508. if ( !isset( $frameParams['alt'] ) ) {
  509. $frameParams['alt'] = '';
  510. }
  511. if ( !isset( $frameParams['title'] ) ) {
  512. $frameParams['title'] = '';
  513. }
  514. if ( !isset( $frameParams['caption'] ) ) {
  515. $frameParams['caption'] = '';
  516. }
  517. if ( empty( $handlerParams['width'] ) ) {
  518. // Reduce width for upright images when parameter 'upright' is used
  519. $handlerParams['width'] = isset( $frameParams['upright'] ) ? 130 : 180;
  520. }
  521. $thumb = false;
  522. $noscale = false;
  523. $manualthumb = false;
  524. if ( !$exists ) {
  525. $outerWidth = $handlerParams['width'] + 2;
  526. } else {
  527. if ( isset( $frameParams['manualthumb'] ) ) {
  528. # Use manually specified thumbnail
  529. $manual_title = Title::makeTitleSafe( NS_FILE, $frameParams['manualthumb'] );
  530. if ( $manual_title ) {
  531. $manual_img = wfFindFile( $manual_title );
  532. if ( $manual_img ) {
  533. $thumb = $manual_img->getUnscaledThumb( $handlerParams );
  534. $manualthumb = true;
  535. } else {
  536. $exists = false;
  537. }
  538. }
  539. } elseif ( isset( $frameParams['framed'] ) ) {
  540. // Use image dimensions, don't scale
  541. $thumb = $file->getUnscaledThumb( $handlerParams );
  542. $noscale = true;
  543. } else {
  544. # Do not present an image bigger than the source, for bitmap-style images
  545. # This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
  546. $srcWidth = $file->getWidth( $page );
  547. if ( $srcWidth && !$file->mustRender() && $handlerParams['width'] > $srcWidth ) {
  548. $handlerParams['width'] = $srcWidth;
  549. }
  550. $thumb = $file->transform( $handlerParams );
  551. }
  552. if ( $thumb ) {
  553. $outerWidth = $thumb->getWidth() + 2;
  554. } else {
  555. $outerWidth = $handlerParams['width'] + 2;
  556. }
  557. }
  558. # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
  559. # So we don't need to pass it here in $query. However, the URL for the
  560. # zoom icon still needs it, so we make a unique query for it. See T16771
  561. $url = $title->getLocalURL( $query );
  562. if ( $page ) {
  563. $url = wfAppendQuery( $url, [ 'page' => $page ] );
  564. }
  565. if ( $manualthumb
  566. && !isset( $frameParams['link-title'] )
  567. && !isset( $frameParams['link-url'] )
  568. && !isset( $frameParams['no-link'] ) ) {
  569. $frameParams['link-url'] = $url;
  570. }
  571. $s = "<div class=\"thumb t{$frameParams['align']}\">"
  572. . "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
  573. if ( !$exists ) {
  574. $s .= self::makeBrokenImageLinkObj( $title, $frameParams['title'], '', '', '', $time == true );
  575. $zoomIcon = '';
  576. } elseif ( !$thumb ) {
  577. $s .= wfMessage( 'thumbnail_error', '' )->escaped();
  578. $zoomIcon = '';
  579. } else {
  580. if ( !$noscale && !$manualthumb ) {
  581. self::processResponsiveImages( $file, $thumb, $handlerParams );
  582. }
  583. $params = [
  584. 'alt' => $frameParams['alt'],
  585. 'title' => $frameParams['title'],
  586. 'img-class' => ( isset( $frameParams['class'] ) && $frameParams['class'] !== ''
  587. ? $frameParams['class'] . ' '
  588. : '' ) . 'thumbimage'
  589. ];
  590. $params = self::getImageLinkMTOParams( $frameParams, $query ) + $params;
  591. $s .= $thumb->toHtml( $params );
  592. if ( isset( $frameParams['framed'] ) ) {
  593. $zoomIcon = "";
  594. } else {
  595. $zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
  596. Html::rawElement( 'a', [
  597. 'href' => $url,
  598. 'class' => 'internal',
  599. 'title' => wfMessage( 'thumbnail-more' )->text() ],
  600. "" ) );
  601. }
  602. }
  603. $s .= ' <div class="thumbcaption">' . $zoomIcon . $frameParams['caption'] . "</div></div></div>";
  604. return str_replace( "\n", ' ', $s );
  605. }
  606. /**
  607. * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
  608. * applicable.
  609. *
  610. * @param File $file
  611. * @param MediaTransformOutput $thumb
  612. * @param array $hp Image parameters
  613. */
  614. public static function processResponsiveImages( $file, $thumb, $hp ) {
  615. global $wgResponsiveImages;
  616. if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
  617. $hp15 = $hp;
  618. $hp15['width'] = round( $hp['width'] * 1.5 );
  619. $hp20 = $hp;
  620. $hp20['width'] = $hp['width'] * 2;
  621. if ( isset( $hp['height'] ) ) {
  622. $hp15['height'] = round( $hp['height'] * 1.5 );
  623. $hp20['height'] = $hp['height'] * 2;
  624. }
  625. $thumb15 = $file->transform( $hp15 );
  626. $thumb20 = $file->transform( $hp20 );
  627. if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
  628. $thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
  629. }
  630. if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
  631. $thumb->responsiveUrls['2'] = $thumb20->getUrl();
  632. }
  633. }
  634. }
  635. /**
  636. * Make a "broken" link to an image
  637. *
  638. * @since 1.16.3
  639. * @param Title $title
  640. * @param string $label Link label (plain text)
  641. * @param string $query Query string
  642. * @param string $unused1 Unused parameter kept for b/c
  643. * @param string $unused2 Unused parameter kept for b/c
  644. * @param bool $time A file of a certain timestamp was requested
  645. * @return string
  646. */
  647. public static function makeBrokenImageLinkObj( $title, $label = '',
  648. $query = '', $unused1 = '', $unused2 = '', $time = false
  649. ) {
  650. if ( !$title instanceof Title ) {
  651. wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
  652. return "<!-- ERROR -->" . htmlspecialchars( $label );
  653. }
  654. global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
  655. if ( $label == '' ) {
  656. $label = $title->getPrefixedText();
  657. }
  658. $encLabel = htmlspecialchars( $label );
  659. $currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
  660. if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
  661. && !$currentExists
  662. ) {
  663. $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
  664. if ( $redir ) {
  665. // We already know it's a redirect, so mark it
  666. // accordingly
  667. return self::link(
  668. $title,
  669. $encLabel,
  670. [ 'class' => 'mw-redirect' ],
  671. wfCgiToArray( $query ),
  672. [ 'known', 'noclasses' ]
  673. );
  674. }
  675. $href = self::getUploadUrl( $title, $query );
  676. return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
  677. htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
  678. $encLabel . '</a>';
  679. }
  680. return self::link( $title, $encLabel, [], wfCgiToArray( $query ), [ 'known', 'noclasses' ] );
  681. }
  682. /**
  683. * Get the URL to upload a certain file
  684. *
  685. * @since 1.16.3
  686. * @param Title $destFile Title object of the file to upload
  687. * @param string $query Urlencoded query string to prepend
  688. * @return string Urlencoded URL
  689. */
  690. protected static function getUploadUrl( $destFile, $query = '' ) {
  691. global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
  692. $q = 'wpDestFile=' . $destFile->getPartialURL();
  693. if ( $query != '' ) {
  694. $q .= '&' . $query;
  695. }
  696. if ( $wgUploadMissingFileUrl ) {
  697. return wfAppendQuery( $wgUploadMissingFileUrl, $q );
  698. } elseif ( $wgUploadNavigationUrl ) {
  699. return wfAppendQuery( $wgUploadNavigationUrl, $q );
  700. } else {
  701. $upload = SpecialPage::getTitleFor( 'Upload' );
  702. return $upload->getLocalURL( $q );
  703. }
  704. }
  705. /**
  706. * Create a direct link to a given uploaded file.
  707. *
  708. * @since 1.16.3
  709. * @param Title $title
  710. * @param string $html Pre-sanitized HTML
  711. * @param string $time MW timestamp of file creation time
  712. * @return string HTML
  713. */
  714. public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
  715. $img = wfFindFile( $title, [ 'time' => $time ] );
  716. return self::makeMediaLinkFile( $title, $img, $html );
  717. }
  718. /**
  719. * Create a direct link to a given uploaded file.
  720. * This will make a broken link if $file is false.
  721. *
  722. * @since 1.16.3
  723. * @param Title $title
  724. * @param File|bool $file File object or false
  725. * @param string $html Pre-sanitized HTML
  726. * @return string HTML
  727. *
  728. * @todo Handle invalid or missing images better.
  729. */
  730. public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
  731. if ( $file && $file->exists() ) {
  732. $url = $file->getUrl();
  733. $class = 'internal';
  734. } else {
  735. $url = self::getUploadUrl( $title );
  736. $class = 'new';
  737. }
  738. $alt = $title->getText();
  739. if ( $html == '' ) {
  740. $html = $alt;
  741. }
  742. $ret = '';
  743. $attribs = [
  744. 'href' => $url,
  745. 'class' => $class,
  746. 'title' => $alt
  747. ];
  748. if ( !Hooks::run( 'LinkerMakeMediaLinkFile',
  749. [ $title, $file, &$html, &$attribs, &$ret ] ) ) {
  750. wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
  751. . "with url {$url} and text {$html} to {$ret}\n", true );
  752. return $ret;
  753. }
  754. return Html::rawElement( 'a', $attribs, $html );
  755. }
  756. /**
  757. * Make a link to a special page given its name and, optionally,
  758. * a message key from the link text.
  759. * Usage example: Linker::specialLink( 'Recentchanges' )
  760. *
  761. * @since 1.16.3
  762. * @param string $name
  763. * @param string $key
  764. * @return string
  765. */
  766. public static function specialLink( $name, $key = '' ) {
  767. if ( $key == '' ) {
  768. $key = strtolower( $name );
  769. }
  770. return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
  771. }
  772. /**
  773. * Make an external link
  774. * @since 1.16.3. $title added in 1.21
  775. * @param string $url URL to link to
  776. * @param string $text Text of link
  777. * @param bool $escape Do we escape the link text?
  778. * @param string $linktype Type of external link. Gets added to the classes
  779. * @param array $attribs Array of extra attributes to <a>
  780. * @param Title|null $title Title object used for title specific link attributes
  781. * @return string
  782. */
  783. public static function makeExternalLink( $url, $text, $escape = true,
  784. $linktype = '', $attribs = [], $title = null
  785. ) {
  786. global $wgTitle;
  787. $class = "external";
  788. if ( $linktype ) {
  789. $class .= " $linktype";
  790. }
  791. if ( isset( $attribs['class'] ) && $attribs['class'] ) {
  792. $class .= " {$attribs['class']}";
  793. }
  794. $attribs['class'] = $class;
  795. if ( $escape ) {
  796. $text = htmlspecialchars( $text );
  797. }
  798. if ( !$title ) {
  799. $title = $wgTitle;
  800. }
  801. $newRel = Parser::getExternalLinkRel( $url, $title );
  802. if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) {
  803. $attribs['rel'] = $newRel;
  804. } elseif ( $newRel !== '' ) {
  805. // Merge the rel attributes.
  806. $newRels = explode( ' ', $newRel );
  807. $oldRels = explode( ' ', $attribs['rel'] );
  808. $combined = array_unique( array_merge( $newRels, $oldRels ) );
  809. $attribs['rel'] = implode( ' ', $combined );
  810. }
  811. $link = '';
  812. $success = Hooks::run( 'LinkerMakeExternalLink',
  813. [ &$url, &$text, &$link, &$attribs, $linktype ] );
  814. if ( !$success ) {
  815. wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
  816. . "with url {$url} and text {$text} to {$link}\n", true );
  817. return $link;
  818. }
  819. $attribs['href'] = $url;
  820. return Html::rawElement( 'a', $attribs, $text );
  821. }
  822. /**
  823. * Make user link (or user contributions for unregistered users)
  824. * @param int $userId User id in database.
  825. * @param string $userName User name in database.
  826. * @param string $altUserName Text to display instead of the user name (optional)
  827. * @return string HTML fragment
  828. * @since 1.16.3. $altUserName was added in 1.19.
  829. */
  830. public static function userLink( $userId, $userName, $altUserName = false ) {
  831. $classes = 'mw-userlink';
  832. $page = null;
  833. if ( $userId == 0 ) {
  834. $page = ExternalUserNames::getUserLinkTitle( $userName );
  835. if ( ExternalUserNames::isExternal( $userName ) ) {
  836. $classes .= ' mw-extuserlink';
  837. } elseif ( $altUserName === false ) {
  838. $altUserName = IP::prettifyIP( $userName );
  839. }
  840. $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
  841. } else {
  842. $page = Title::makeTitle( NS_USER, $userName );
  843. }
  844. // Wrap the output with <bdi> tags for directionality isolation
  845. $linkText =
  846. '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
  847. return $page
  848. ? self::link( $page, $linkText, [ 'class' => $classes ] )
  849. : Html::rawElement( 'span', [ 'class' => $classes ], $linkText );
  850. }
  851. /**
  852. * Generate standard user tool links (talk, contributions, block link, etc.)
  853. *
  854. * @since 1.16.3
  855. * @param int $userId User identifier
  856. * @param string $userText User name or IP address
  857. * @param bool $redContribsWhenNoEdits Should the contributions link be
  858. * red if the user has no edits?
  859. * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
  860. * and Linker::TOOL_LINKS_EMAIL).
  861. * @param int $edits User edit count (optional, for performance)
  862. * @return string HTML fragment
  863. */
  864. public static function userToolLinks(
  865. $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
  866. ) {
  867. global $wgUser, $wgDisableAnonTalk, $wgLang;
  868. $talkable = !( $wgDisableAnonTalk && 0 == $userId );
  869. $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
  870. $addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
  871. if ( $userId == 0 && ExternalUserNames::isExternal( $userText ) ) {
  872. // No tools for an external user
  873. return '';
  874. }
  875. $items = [];
  876. if ( $talkable ) {
  877. $items[] = self::userTalkLink( $userId, $userText );
  878. }
  879. if ( $userId ) {
  880. // check if the user has an edit
  881. $attribs = [];
  882. $attribs['class'] = 'mw-usertoollinks-contribs';
  883. if ( $redContribsWhenNoEdits ) {
  884. if ( intval( $edits ) === 0 && $edits !== 0 ) {
  885. $user = User::newFromId( $userId );
  886. $edits = $user->getEditCount();
  887. }
  888. if ( $edits === 0 ) {
  889. $attribs['class'] .= ' new';
  890. }
  891. }
  892. $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
  893. $items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
  894. }
  895. if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
  896. $items[] = self::blockLink( $userId, $userText );
  897. }
  898. if ( $addEmailLink && $wgUser->canSendEmail() ) {
  899. $items[] = self::emailLink( $userId, $userText );
  900. }
  901. Hooks::run( 'UserToolLinksEdit', [ $userId, $userText, &$items ] );
  902. if ( $items ) {
  903. return wfMessage( 'word-separator' )->escaped()
  904. . '<span class="mw-usertoollinks">'
  905. . wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
  906. . '</span>';
  907. } else {
  908. return '';
  909. }
  910. }
  911. /**
  912. * Alias for userToolLinks( $userId, $userText, true );
  913. * @since 1.16.3
  914. * @param int $userId User identifier
  915. * @param string $userText User name or IP address
  916. * @param int $edits User edit count (optional, for performance)
  917. * @return string
  918. */
  919. public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
  920. return self::userToolLinks( $userId, $userText, true, 0, $edits );
  921. }
  922. /**
  923. * @since 1.16.3
  924. * @param int $userId User id in database.
  925. * @param string $userText User name in database.
  926. * @return string HTML fragment with user talk link
  927. */
  928. public static function userTalkLink( $userId, $userText ) {
  929. $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
  930. $moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
  931. $userTalkLink = self::link( $userTalkPage,
  932. wfMessage( 'talkpagelinktext' )->escaped(),
  933. $moreLinkAttribs );
  934. return $userTalkLink;
  935. }
  936. /**
  937. * @since 1.16.3
  938. * @param int $userId
  939. * @param string $userText User name in database.
  940. * @return string HTML fragment with block link
  941. */
  942. public static function blockLink( $userId, $userText ) {
  943. $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
  944. $moreLinkAttribs['class'] = 'mw-usertoollinks-block';
  945. $blockLink = self::link( $blockPage,
  946. wfMessage( 'blocklink' )->escaped(),
  947. $moreLinkAttribs );
  948. return $blockLink;
  949. }
  950. /**
  951. * @param int $userId
  952. * @param string $userText User name in database.
  953. * @return string HTML fragment with e-mail user link
  954. */
  955. public static function emailLink( $userId, $userText ) {
  956. $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
  957. $moreLinkAttribs['class'] = 'mw-usertoollinks-mail';
  958. $emailLink = self::link( $emailPage,
  959. wfMessage( 'emaillink' )->escaped(),
  960. $moreLinkAttribs );
  961. return $emailLink;
  962. }
  963. /**
  964. * Generate a user link if the current user is allowed to view it
  965. * @since 1.16.3
  966. * @param Revision $rev
  967. * @param bool $isPublic Show only if all users can see it
  968. * @return string HTML fragment
  969. */
  970. public static function revUserLink( $rev, $isPublic = false ) {
  971. if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
  972. $link = wfMessage( 'rev-deleted-user' )->escaped();
  973. } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
  974. $link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
  975. $rev->getUserText( Revision::FOR_THIS_USER ) );
  976. } else {
  977. $link = wfMessage( 'rev-deleted-user' )->escaped();
  978. }
  979. if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
  980. return '<span class="history-deleted">' . $link . '</span>';
  981. }
  982. return $link;
  983. }
  984. /**
  985. * Generate a user tool link cluster if the current user is allowed to view it
  986. * @since 1.16.3
  987. * @param Revision $rev
  988. * @param bool $isPublic Show only if all users can see it
  989. * @return string HTML
  990. */
  991. public static function revUserTools( $rev, $isPublic = false ) {
  992. if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
  993. $link = wfMessage( 'rev-deleted-user' )->escaped();
  994. } elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
  995. $userId = $rev->getUser( Revision::FOR_THIS_USER );
  996. $userText = $rev->getUserText( Revision::FOR_THIS_USER );
  997. $link = self::userLink( $userId, $userText )
  998. . self::userToolLinks( $userId, $userText );
  999. } else {
  1000. $link = wfMessage( 'rev-deleted-user' )->escaped();
  1001. }
  1002. if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
  1003. return ' <span class="history-deleted">' . $link . '</span>';
  1004. }
  1005. return $link;
  1006. }
  1007. /**
  1008. * This function is called by all recent changes variants, by the page history,
  1009. * and by the user contributions list. It is responsible for formatting edit
  1010. * summaries. It escapes any HTML in the summary, but adds some CSS to format
  1011. * auto-generated comments (from section editing) and formats [[wikilinks]].
  1012. *
  1013. * @author Erik Moeller <moeller@scireview.de>
  1014. * @since 1.16.3. $wikiId added in 1.26
  1015. *
  1016. * Note: there's not always a title to pass to this function.
  1017. * Since you can't set a default parameter for a reference, I've turned it
  1018. * temporarily to a value pass. Should be adjusted further. --brion
  1019. *
  1020. * @param string $comment
  1021. * @param Title|null $title Title object (to generate link to the section in autocomment)
  1022. * or null
  1023. * @param bool $local Whether section links should refer to local page
  1024. * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
  1025. * For use with external changes.
  1026. *
  1027. * @return mixed|string
  1028. */
  1029. public static function formatComment(
  1030. $comment, $title = null, $local = false, $wikiId = null
  1031. ) {
  1032. # Sanitize text a bit:
  1033. $comment = str_replace( "\n", " ", $comment );
  1034. # Allow HTML entities (for T15815)
  1035. $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
  1036. # Render autocomments and make links:
  1037. $comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
  1038. $comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
  1039. return $comment;
  1040. }
  1041. /**
  1042. * Converts autogenerated comments in edit summaries into section links.
  1043. *
  1044. * The pattern for autogen comments is / * foo * /, which makes for
  1045. * some nasty regex.
  1046. * We look for all comments, match any text before and after the comment,
  1047. * add a separator where needed and format the comment itself with CSS
  1048. * Called by Linker::formatComment.
  1049. *
  1050. * @param string $comment Comment text
  1051. * @param Title|null $title An optional title object used to links to sections
  1052. * @param bool $local Whether section links should refer to local page
  1053. * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
  1054. * as used by WikiMap.
  1055. *
  1056. * @return string Formatted comment (wikitext)
  1057. */
  1058. private static function formatAutocomments(
  1059. $comment, $title = null, $local = false, $wikiId = null
  1060. ) {
  1061. // @todo $append here is something of a hack to preserve the status
  1062. // quo. Someone who knows more about bidi and such should decide
  1063. // (1) what sane rendering even *is* for an LTR edit summary on an RTL
  1064. // wiki, both when autocomments exist and when they don't, and
  1065. // (2) what markup will make that actually happen.
  1066. $append = '';
  1067. $comment = preg_replace_callback(
  1068. // To detect the presence of content before or after the
  1069. // auto-comment, we use capturing groups inside optional zero-width
  1070. // assertions. But older versions of PCRE can't directly make
  1071. // zero-width assertions optional, so wrap them in a non-capturing
  1072. // group.
  1073. '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
  1074. function ( $match ) use ( $title, $local, $wikiId, &$append ) {
  1075. global $wgLang;
  1076. // Ensure all match positions are defined
  1077. $match += [ '', '', '', '' ];
  1078. $pre = $match[1] !== '';
  1079. $auto = $match[2];
  1080. $post = $match[3] !== '';
  1081. $comment = null;
  1082. Hooks::run(
  1083. 'FormatAutocomments',
  1084. [ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
  1085. );
  1086. if ( $comment === null ) {
  1087. $link = '';
  1088. if ( $title ) {
  1089. $section = $auto;
  1090. # Remove links that a user may have manually put in the autosummary
  1091. # This could be improved by copying as much of Parser::stripSectionName as desired.
  1092. $section = str_replace( '[[:', '', $section );
  1093. $section = str_replace( '[[', '', $section );
  1094. $section = str_replace( ']]', '', $section );
  1095. $section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
  1096. if ( $local ) {
  1097. $sectionTitle = Title::makeTitleSafe( NS_MAIN, '', $section );
  1098. } else {
  1099. $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
  1100. $title->getDBkey(), $section );
  1101. }
  1102. if ( $sectionTitle ) {
  1103. $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
  1104. } else {
  1105. $link = '';
  1106. }
  1107. }
  1108. if ( $pre ) {
  1109. # written summary $presep autocomment (summary /* section */)
  1110. $pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
  1111. }
  1112. if ( $post ) {
  1113. # autocomment $postsep written summary (/* section */ summary)
  1114. $auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
  1115. }
  1116. $auto = '<span class="autocomment">' . $auto . '</span>';
  1117. $comment = $pre . $link . $wgLang->getDirMark()
  1118. . '<span dir="auto">' . $auto;
  1119. $append .= '</span>';
  1120. }
  1121. return $comment;
  1122. },
  1123. $comment
  1124. );
  1125. return $comment . $append;
  1126. }
  1127. /**
  1128. * Formats wiki links and media links in text; all other wiki formatting
  1129. * is ignored
  1130. *
  1131. * @since 1.16.3. $wikiId added in 1.26
  1132. * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
  1133. *
  1134. * @param string $comment Text to format links in. WARNING! Since the output of this
  1135. * function is html, $comment must be sanitized for use as html. You probably want
  1136. * to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
  1137. * this function.
  1138. * @param Title|null $title An optional title object used to links to sections
  1139. * @param bool $local Whether section links should refer to local page
  1140. * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
  1141. * as used by WikiMap.
  1142. *
  1143. * @return string
  1144. */
  1145. public static function formatLinksInComment(
  1146. $comment, $title = null, $local = false, $wikiId = null
  1147. ) {
  1148. return preg_replace_callback(
  1149. '/
  1150. \[\[
  1151. :? # ignore optional leading colon
  1152. ([^\]|]+) # 1. link target; page names cannot include ] or |
  1153. (?:\|
  1154. # 2. link text
  1155. # Stop matching at ]] without relying on backtracking.
  1156. ((?:]?[^\]])*+)
  1157. )?
  1158. \]\]
  1159. ([^[]*) # 3. link trail (the text up until the next link)
  1160. /x',
  1161. function ( $match ) use ( $title, $local, $wikiId ) {
  1162. global $wgContLang;
  1163. $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
  1164. $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
  1165. $comment = $match[0];
  1166. # fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
  1167. if ( strpos( $match[1], '%' ) !== false ) {
  1168. $match[1] = strtr(
  1169. rawurldecode( $match[1] ),
  1170. [ '<' => '&lt;', '>' => '&gt;' ]
  1171. );
  1172. }
  1173. # Handle link renaming [[foo|text]] will show link as "text"
  1174. if ( $match[2] != "" ) {
  1175. $text = $match[2];
  1176. } else {
  1177. $text = $match[1];
  1178. }
  1179. $submatch = [];
  1180. $thelink = null;
  1181. if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
  1182. # Media link; trail not supported.
  1183. $linkRegexp = '/\[\[(.*?)\]\]/';
  1184. $title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
  1185. if ( $title ) {
  1186. $thelink = Linker::makeMediaLinkObj( $title, $text );
  1187. }
  1188. } else {
  1189. # Other kind of link
  1190. # Make sure its target is non-empty
  1191. if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
  1192. $match[1] = substr( $match[1], 1 );
  1193. }
  1194. if ( $match[1] !== false && $match[1] !== '' ) {
  1195. if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
  1196. $trail = $submatch[1];
  1197. } else {
  1198. $trail = "";
  1199. }
  1200. $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
  1201. list( $inside, $trail ) = Linker::splitTrail( $trail );
  1202. $linkText = $text;
  1203. $linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
  1204. $target = Title::newFromText( $linkTarget );
  1205. if ( $target ) {
  1206. if ( $target->getText() == '' && !$target->isExternal()
  1207. && !$local && $title
  1208. ) {
  1209. $target = $title->createFragmentTarget( $target->getFragment() );
  1210. }
  1211. $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
  1212. }
  1213. }
  1214. }
  1215. if ( $thelink ) {
  1216. // If the link is still valid, go ahead and replace it in!
  1217. $comment = preg_replace(
  1218. $linkRegexp,
  1219. StringUtils::escapeRegexReplacement( $thelink ),
  1220. $comment,
  1221. 1
  1222. );
  1223. }
  1224. return $comment;
  1225. },
  1226. $comment
  1227. );
  1228. }
  1229. /**
  1230. * Generates a link to the given Title
  1231. *
  1232. * @note This is only public for technical reasons. It's not intended for use outside Linker.
  1233. *
  1234. * @param LinkTarget $linkTarget
  1235. * @param string $text
  1236. * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
  1237. * as used by WikiMap.
  1238. * @param string|string[] $options See the $options parameter in Linker::link.
  1239. *
  1240. * @return string HTML link
  1241. */
  1242. public static function makeCommentLink(
  1243. LinkTarget $linkTarget, $text, $wikiId = null, $options = []
  1244. ) {
  1245. if ( $wikiId !== null && !$linkTarget->isExternal() ) {
  1246. $link = self::makeExternalLink(
  1247. WikiMap::getForeignURL(
  1248. $wikiId,
  1249. $linkTarget->getNamespace() === 0
  1250. ? $linkTarget->getDBkey()
  1251. : MWNamespace::getCanonicalName( $linkTarget->getNamespace() ) . ':'
  1252. . $linkTarget->getDBkey(),
  1253. $linkTarget->getFragment()
  1254. ),
  1255. $text,
  1256. /* escape = */ false // Already escaped
  1257. );
  1258. } else {
  1259. $link = self::link( $linkTarget, $text, [], [], $options );
  1260. }
  1261. return $link;
  1262. }
  1263. /**
  1264. * @param Title $contextTitle
  1265. * @param string $target
  1266. * @param string &$text
  1267. * @return string
  1268. */
  1269. public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
  1270. # Valid link forms:
  1271. # Foobar -- normal
  1272. # :Foobar -- override special treatment of prefix (images, language links)
  1273. # /Foobar -- convert to CurrentPage/Foobar
  1274. # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
  1275. # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
  1276. # ../Foobar -- convert to CurrentPage/Foobar,
  1277. # (from CurrentPage/CurrentSubPage)
  1278. # ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
  1279. # (from CurrentPage/CurrentSubPage)
  1280. $ret = $target; # default return value is no change
  1281. # Some namespaces don't allow subpages,
  1282. # so only perform processing if subpages are allowed
  1283. if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
  1284. $hash = strpos( $target, '#' );
  1285. if ( $hash !== false ) {
  1286. $suffix = substr( $target, $hash );
  1287. $target = substr( $target, 0, $hash );
  1288. } else {
  1289. $suffix = '';
  1290. }
  1291. # T9425
  1292. $target = trim( $target );
  1293. # Look at the first character
  1294. if ( $target != '' && $target[0] === '/' ) {
  1295. # / at end means we don't want the slash to be shown
  1296. $m = [];
  1297. $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
  1298. if ( $trailingSlashes ) {
  1299. $noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
  1300. } else {
  1301. $noslash = substr( $target, 1 );
  1302. }
  1303. $ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
  1304. if ( $text === '' ) {
  1305. $text = $target . $suffix;
  1306. } # this might be changed for ugliness reasons
  1307. } else {
  1308. # check for .. subpage backlinks
  1309. $dotdotcount = 0;
  1310. $nodotdot = $target;
  1311. while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
  1312. ++$dotdotcount;
  1313. $nodotdot = substr( $nodotdot, 3 );
  1314. }
  1315. if ( $dotdotcount > 0 ) {
  1316. $exploded = explode( '/', $contextTitle->getPrefixedText() );
  1317. if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
  1318. $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
  1319. # / at the end means don't show full path
  1320. if ( substr( $nodotdot, -1, 1 ) === '/' ) {
  1321. $nodotdot = rtrim( $nodotdot, '/' );
  1322. if ( $text === '' ) {
  1323. $text = $nodotdot . $suffix;
  1324. }
  1325. }
  1326. $nodotdot = trim( $nodotdot );
  1327. if ( $nodotdot != '' ) {
  1328. $ret .= '/' . $nodotdot;
  1329. }
  1330. $ret .= $suffix;
  1331. }
  1332. }
  1333. }
  1334. }
  1335. return $ret;
  1336. }
  1337. /**
  1338. * Wrap a comment in standard punctuation and formatting if
  1339. * it's non-empty, otherwise return empty string.
  1340. *
  1341. * @since 1.16.3. $wikiId added in 1.26
  1342. * @param string $comment
  1343. * @param Title|null $title Title object (to generate link to section in autocomment) or null
  1344. * @param bool $local Whether section links should refer to local page
  1345. * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
  1346. * For use with external changes.
  1347. *
  1348. * @return string
  1349. */
  1350. public static function commentBlock(
  1351. $comment, $title = null, $local = false, $wikiId = null
  1352. ) {
  1353. // '*' used to be the comment inserted by the software way back
  1354. // in antiquity in case none was provided, here for backwards
  1355. // compatibility, acc. to brion -ævar
  1356. if ( $comment == '' || $comment == '*' ) {
  1357. return '';
  1358. } else {
  1359. $formatted = self::formatComment( $comment, $title, $local, $wikiId );
  1360. $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
  1361. return " <span class=\"comment\">$formatted</span>";
  1362. }
  1363. }
  1364. /**
  1365. * Wrap and format the given revision's comment block, if the current
  1366. * user is allowed to view it.
  1367. *
  1368. * @since 1.16.3
  1369. * @param Revision $rev
  1370. * @param bool $local Whether section links should refer to local page
  1371. * @param bool $isPublic Show only if all users can see it
  1372. * @return string HTML fragment
  1373. */
  1374. public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
  1375. if ( $rev->getComment( Revision::RAW ) == "" ) {
  1376. return "";
  1377. }
  1378. if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
  1379. $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
  1380. } elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
  1381. $block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
  1382. $rev->getTitle(), $local );
  1383. } else {
  1384. $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
  1385. }
  1386. if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
  1387. return " <span class=\"history-deleted\">$block</span>";
  1388. }
  1389. return $block;
  1390. }
  1391. /**
  1392. * @since 1.16.3
  1393. * @param int $size
  1394. * @return string
  1395. */
  1396. public static function formatRevisionSize( $size ) {
  1397. if ( $size == 0 ) {
  1398. $stxt = wfMessage( 'historyempty' )->escaped();
  1399. } else {
  1400. $stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
  1401. $stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
  1402. }
  1403. return "<span class=\"history-size\">$stxt</span>";
  1404. }
  1405. /**
  1406. * Add another level to the Table of Contents
  1407. *
  1408. * @since 1.16.3
  1409. * @return string
  1410. */
  1411. public static function tocIndent() {
  1412. return "\n<ul>";
  1413. }
  1414. /**
  1415. * Finish one or more sublevels on the Table of Contents
  1416. *
  1417. * @since 1.16.3
  1418. * @param int $level
  1419. * @return string
  1420. */
  1421. public static function tocUnindent( $level ) {
  1422. return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
  1423. }
  1424. /**
  1425. * parameter level defines if we are on an indentation level
  1426. *
  1427. * @since 1.16.3
  1428. * @param string $anchor
  1429. * @param string $tocline
  1430. * @param string $tocnumber
  1431. * @param string $level
  1432. * @param string|bool $sectionIndex
  1433. * @return string
  1434. */
  1435. public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
  1436. $classes = "toclevel-$level";
  1437. if ( $sectionIndex !== false ) {
  1438. $classes .= " tocsection-$sectionIndex";
  1439. }
  1440. // \n<li class="$classes"><a href="#$anchor"><span class="tocnumber">
  1441. // $tocnumber</span> <span class="toctext">$tocline</span></a>
  1442. return "\n" . Html::openElement( 'li', [ 'class' => $classes ] )
  1443. . Html::rawElement( 'a',
  1444. [ 'href' => "#$anchor" ],
  1445. Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber )
  1446. . ' '
  1447. . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline )
  1448. );
  1449. }
  1450. /**
  1451. * End a Table Of Contents line.
  1452. * tocUnindent() will be used instead if we're ending a line below
  1453. * the new level.
  1454. * @since 1.16.3
  1455. * @return string
  1456. */
  1457. public static function tocLineEnd() {
  1458. return "</li>\n";
  1459. }
  1460. /**
  1461. * Wraps the TOC in a table and provides the hide/collapse javascript.
  1462. *
  1463. * @since 1.16.3
  1464. * @param string $toc Html of the Table Of Contents
  1465. * @param string|Language|bool $lang Language for the toc title, defaults to user language
  1466. * @return string Full html of the TOC
  1467. */
  1468. public static function tocList( $toc, $lang = false ) {
  1469. $lang = wfGetLangObj( $lang );
  1470. $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
  1471. return '<div id="toc" class="toc">'
  1472. . Html::openElement( 'div', [
  1473. 'class' => 'toctitle',
  1474. 'lang' => $lang->getHtmlCode(),
  1475. 'dir' => $lang->getDir(),
  1476. ] )
  1477. . '<h2>' . $title . "</h2></div>\n"
  1478. . $toc
  1479. . "</ul>\n</div>\n";
  1480. }
  1481. /**
  1482. * Generate a table of contents from a section tree.
  1483. *
  1484. * @since 1.16.3. $lang added in 1.17
  1485. * @param array $tree Return value of ParserOutput::getSections()
  1486. * @param string|Language|bool $lang Language for the toc title, defaults to user language
  1487. * @return string HTML fragment
  1488. */
  1489. public static function generateTOC( $tree, $lang = false ) {
  1490. $toc = '';
  1491. $lastLevel = 0;
  1492. foreach ( $tree as $section ) {
  1493. if ( $section['toclevel'] > $lastLevel ) {
  1494. $toc .= self::tocIndent();
  1495. } elseif ( $section['toclevel'] < $lastLevel ) {
  1496. $toc .= self::tocUnindent(
  1497. $lastLevel - $section['toclevel'] );
  1498. } else {
  1499. $toc .= self::tocLineEnd();
  1500. }
  1501. $toc .= self::tocLine( $section['anchor'],
  1502. $section['line'], $section['number'],
  1503. $section['toclevel'], $section['index'] );
  1504. $lastLevel = $section['toclevel'];
  1505. }
  1506. $toc .= self::tocLineEnd();
  1507. return self::tocList( $toc, $lang );
  1508. }
  1509. /**
  1510. * Create a headline for content
  1511. *
  1512. * @since 1.16.3
  1513. * @param int $level The level of the headline (1-6)
  1514. * @param string $attribs Any attributes for the headline, starting with
  1515. * a space and ending with '>'
  1516. * This *must* be at least '>' for no attribs
  1517. * @param string $anchor The anchor to give the headline (the bit after the #)
  1518. * @param string $html HTML for the text of the header
  1519. * @param string $link HTML to add for the section edit link
  1520. * @param string|bool $fallbackAnchor A second, optional anchor to give for
  1521. * backward compatibility (false to omit)
  1522. *
  1523. * @return string HTML headline
  1524. */
  1525. public static function makeHeadline( $level, $attribs, $anchor, $html,
  1526. $link, $fallbackAnchor = false
  1527. ) {
  1528. $anchorEscaped = htmlspecialchars( $anchor );
  1529. $fallback = '';
  1530. if ( $fallbackAnchor !== false && $fallbackAnchor !== $anchor ) {
  1531. $fallbackAnchor = htmlspecialchars( $fallbackAnchor );
  1532. $fallback = "<span id=\"$fallbackAnchor\"></span>";
  1533. }
  1534. $ret = "<h$level$attribs"
  1535. . "$fallback<span class=\"mw-headline\" id=\"$anchorEscaped\">$html</span>"
  1536. . $link
  1537. . "</h$level>";
  1538. return $ret;
  1539. }
  1540. /**
  1541. * Split a link trail, return the "inside" portion and the remainder of the trail
  1542. * as a two-element array
  1543. * @param string $trail
  1544. * @return array
  1545. */
  1546. static function splitTrail( $trail ) {
  1547. global $wgContLang;
  1548. $regex = $wgContLang->linkTrail();
  1549. $inside = '';
  1550. if ( $trail !== '' ) {
  1551. $m = [];
  1552. if ( preg_match( $regex, $trail, $m ) ) {
  1553. $inside = $m[1];
  1554. $trail = $m[2];
  1555. }
  1556. }
  1557. return [ $inside, $trail ];
  1558. }
  1559. /**
  1560. * Generate a rollback link for a given revision. Currently it's the
  1561. * caller's responsibility to ensure that the revision is the top one. If
  1562. * it's not, of course, the user will get an error message.
  1563. *
  1564. * If the calling page is called with the parameter &bot=1, all rollback
  1565. * links also get that parameter. It causes the edit itself and the rollback
  1566. * to be marked as "bot" edits. Bot edits are hidden by default from recent
  1567. * changes, so this allows sysops to combat a busy vandal without bothering
  1568. * other users.
  1569. *
  1570. * If the option verify is set this function will return the link only in case the
  1571. * revision can be reverted. Please note that due to performance limitations
  1572. * it might be assumed that a user isn't the only contributor of a page while
  1573. * (s)he is, which will lead to useless rollback links. Furthermore this wont
  1574. * work if $wgShowRollbackEditCount is disabled, so this can only function
  1575. * as an additional check.
  1576. *
  1577. * If the option noBrackets is set the rollback link wont be enclosed in "[]".
  1578. *
  1579. * @since 1.16.3. $context added in 1.20. $options added in 1.21
  1580. *
  1581. * @param Revision $rev
  1582. * @param IContextSource $context Context to use or null for the main context.
  1583. * @param array $options
  1584. * @return string
  1585. */
  1586. public static function generateRollback( $rev, IContextSource $context = null,
  1587. $options = [ 'verify' ]
  1588. ) {
  1589. if ( $context === null ) {
  1590. $context = RequestContext::getMain();
  1591. }
  1592. $editCount = false;
  1593. if ( in_array( 'verify', $options, true ) ) {
  1594. $editCount = self::getRollbackEditCount( $rev, true );
  1595. if ( $editCount === false ) {
  1596. return '';
  1597. }
  1598. }
  1599. $inner = self::buildRollbackLink( $rev, $context, $editCount );
  1600. if ( !in_array( 'noBrackets', $options, true ) ) {
  1601. $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
  1602. }
  1603. return '<span class="mw-rollback-link">' . $inner . '</span>';
  1604. }
  1605. /**
  1606. * This function will return the number of revisions which a rollback
  1607. * would revert and, if $verify is set it will verify that a revision
  1608. * can be reverted (that the user isn't the only contributor and the
  1609. * revision we might rollback to isn't deleted). These checks can only
  1610. * function as an additional check as this function only checks against
  1611. * the last $wgShowRollbackEditCount edits.
  1612. *
  1613. * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
  1614. * is set and the user is the only contributor of the page.
  1615. *
  1616. * @param Revision $rev
  1617. * @param bool $verify Try to verify that this revision can really be rolled back
  1618. * @return int|bool|null
  1619. */
  1620. public static function getRollbackEditCount( $rev, $verify ) {
  1621. global $wgShowRollbackEditCount;
  1622. if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
  1623. // Nothing has happened, indicate this by returning 'null'
  1624. return null;
  1625. }
  1626. $dbr = wfGetDB( DB_REPLICA );
  1627. // Up to the value of $wgShowRollbackEditCount revisions are counted
  1628. $revQuery = Revision::getQueryInfo();
  1629. $res = $dbr->select(
  1630. $revQuery['tables'],
  1631. [ 'rev_user_text' => $revQuery['fields']['rev_user_text'], 'rev_deleted' ],
  1632. // $rev->getPage() returns null sometimes
  1633. [ 'rev_page' => $rev->getTitle()->getArticleID() ],
  1634. __METHOD__,
  1635. [
  1636. 'USE INDEX' => [ 'revision' => 'page_timestamp' ],
  1637. 'ORDER BY' => 'rev_timestamp DESC',
  1638. 'LIMIT' => $wgShowRollbackEditCount + 1
  1639. ],
  1640. $revQuery['joins']
  1641. );
  1642. $editCount = 0;
  1643. $moreRevs = false;
  1644. foreach ( $res as $row ) {
  1645. if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) {
  1646. if ( $verify &&
  1647. ( $row->rev_deleted & Revision::DELETED_TEXT
  1648. || $row->rev_deleted & Revision::DELETED_USER
  1649. ) ) {
  1650. // If the user or the text of the revision we might rollback
  1651. // to is deleted in some way we can't rollback. Similar to
  1652. // the sanity checks in WikiPage::commitRollback.
  1653. return false;
  1654. }
  1655. $moreRevs = true;
  1656. break;
  1657. }
  1658. $editCount++;
  1659. }
  1660. if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
  1661. // We didn't find at least $wgShowRollbackEditCount revisions made by the current user
  1662. // and there weren't any other revisions. That means that the current user is the only
  1663. // editor, so we can't rollback
  1664. return false;
  1665. }
  1666. return $editCount;
  1667. }
  1668. /**
  1669. * Build a raw rollback link, useful for collections of "tool" links
  1670. *
  1671. * @since 1.16.3. $context added in 1.20. $editCount added in 1.21
  1672. * @param Revision $rev
  1673. * @param IContextSource|null $context Context to use or null for the main context.
  1674. * @param int $editCount Number of edits that would be reverted
  1675. * @return string HTML fragment
  1676. */
  1677. public static function buildRollbackLink( $rev, IContextSource $context = null,
  1678. $editCount = false
  1679. ) {
  1680. global $wgShowRollbackEditCount, $wgMiserMode;
  1681. // To config which pages are affected by miser mode
  1682. $disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
  1683. if ( $context === null ) {
  1684. $context = RequestContext::getMain();
  1685. }
  1686. $title = $rev->getTitle();
  1687. $query = [
  1688. 'action' => 'rollback',
  1689. 'from' => $rev->getUserText(),
  1690. 'token' => $context->getUser()->getEditToken( 'rollback' ),
  1691. ];
  1692. $attrs = [
  1693. 'data-mw' => 'interface',
  1694. 'title' => $context->msg( 'tooltip-rollback' )->text(),
  1695. ];
  1696. $options = [ 'known', 'noclasses' ];
  1697. if ( $context->getRequest()->getBool( 'bot' ) ) {
  1698. $query['bot'] = '1';
  1699. $query['hidediff'] = '1'; // T17999
  1700. }
  1701. $disableRollbackEditCount = false;
  1702. if ( $wgMiserMode ) {
  1703. foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
  1704. if ( $context->getTitle()->isSpecial( $specialPage ) ) {
  1705. $disableRollbackEditCount = true;
  1706. break;
  1707. }
  1708. }
  1709. }
  1710. if ( !$disableRollbackEditCount
  1711. && is_int( $wgShowRollbackEditCount )
  1712. && $wgShowRollbackEditCount > 0
  1713. ) {
  1714. if ( !is_numeric( $editCount ) ) {
  1715. $editCount = self::getRollbackEditCount( $rev, false );
  1716. }
  1717. if ( $editCount > $wgShowRollbackEditCount ) {
  1718. $html = $context->msg( 'rollbacklinkcount-morethan' )
  1719. ->numParams( $wgShowRollbackEditCount )->parse();
  1720. } else {
  1721. $html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
  1722. }
  1723. return self::link( $title, $html, $attrs, $query, $options );
  1724. } else {
  1725. $html = $context->msg( 'rollbacklink' )->escaped();
  1726. return self::link( $title, $html, $attrs, $query, $options );
  1727. }
  1728. }
  1729. /**
  1730. * @deprecated since 1.28, use TemplatesOnThisPageFormatter directly
  1731. *
  1732. * Returns HTML for the "templates used on this page" list.
  1733. *
  1734. * Make an HTML list of templates, and then add a "More..." link at
  1735. * the bottom. If $more is null, do not add a "More..." link. If $more
  1736. * is a Title, make a link to that title and use it. If $more is a string,
  1737. * directly paste it in as the link (escaping needs to be done manually).
  1738. * Finally, if $more is a Message, call toString().
  1739. *
  1740. * @since 1.16.3. $more added in 1.21
  1741. * @param Title[] $templates Array of templates
  1742. * @param bool $preview Whether this is for a preview
  1743. * @param bool $section Whether this is for a section edit
  1744. * @param Title|Message|string|null $more An escaped link for "More..." of the templates
  1745. * @return string HTML output
  1746. */
  1747. public static function formatTemplates( $templates, $preview = false,
  1748. $section = false, $more = null
  1749. ) {
  1750. wfDeprecated( __METHOD__, '1.28' );
  1751. $type = false;
  1752. if ( $preview ) {
  1753. $type = 'preview';
  1754. } elseif ( $section ) {
  1755. $type = 'section';
  1756. }
  1757. if ( $more instanceof Message ) {
  1758. $more = $more->toString();
  1759. }
  1760. $formatter = new TemplatesOnThisPageFormatter(
  1761. RequestContext::getMain(),
  1762. MediaWikiServices::getInstance()->getLinkRenderer()
  1763. );
  1764. return $formatter->format( $templates, $type, $more );
  1765. }
  1766. /**
  1767. * Returns HTML for the "hidden categories on this page" list.
  1768. *
  1769. * @since 1.16.3
  1770. * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
  1771. * or similar
  1772. * @return string HTML output
  1773. */
  1774. public static function formatHiddenCategories( $hiddencats ) {
  1775. $outText = '';
  1776. if ( count( $hiddencats ) > 0 ) {
  1777. # Construct the HTML
  1778. $outText = '<div class="mw-hiddenCategoriesExplanation">';
  1779. $outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
  1780. $outText .= "</div><ul>\n";
  1781. foreach ( $hiddencats as $titleObj ) {
  1782. # If it's hidden, it must exist - no need to check with a LinkBatch
  1783. $outText .= '<li>'
  1784. . self::link( $titleObj, null, [], [], 'known' )
  1785. . "</li>\n";
  1786. }
  1787. $outText .= '</ul>';
  1788. }
  1789. return $outText;
  1790. }
  1791. /**
  1792. * @deprecated since 1.28, use Language::formatSize() directly
  1793. *
  1794. * Format a size in bytes for output, using an appropriate
  1795. * unit (B, KB, MB or GB) according to the magnitude in question
  1796. *
  1797. * @since 1.16.3
  1798. * @param int $size Size to format
  1799. * @return string
  1800. */
  1801. public static function formatSize( $size ) {
  1802. wfDeprecated( __METHOD__, '1.28' );
  1803. global $wgLang;
  1804. return htmlspecialchars( $wgLang->formatSize( $size ) );
  1805. }
  1806. /**
  1807. * Given the id of an interface element, constructs the appropriate title
  1808. * attribute from the system messages. (Note, this is usually the id but
  1809. * isn't always, because sometimes the accesskey needs to go on a different
  1810. * element than the id, for reverse-compatibility, etc.)
  1811. *
  1812. * @since 1.16.3 $msgParams added in 1.27
  1813. * @param string $name Id of the element, minus prefixes.
  1814. * @param string|array|null $options Null, string or array with some of the following options:
  1815. * - 'withaccess' to add an access-key hint
  1816. * - 'nonexisting' to add an accessibility hint that page does not exist
  1817. * @param array $msgParams Parameters to pass to the message
  1818. *
  1819. * @return string Contents of the title attribute (which you must HTML-
  1820. * escape), or false for no title attribute
  1821. */
  1822. public static function titleAttrib( $name, $options = null, array $msgParams = [] ) {
  1823. $message = wfMessage( "tooltip-$name", $msgParams );
  1824. if ( !$message->exists() ) {
  1825. $tooltip = false;
  1826. } else {
  1827. $tooltip = $message->text();
  1828. # Compatibility: formerly some tooltips had [alt-.] hardcoded
  1829. $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
  1830. # Message equal to '-' means suppress it.
  1831. if ( $tooltip == '-' ) {
  1832. $tooltip = false;
  1833. }
  1834. }
  1835. $options = (array)$options;
  1836. if ( in_array( 'nonexisting', $options ) ) {
  1837. $tooltip = wfMessage( 'red-link-title', $tooltip ?: '' )->text();
  1838. }
  1839. if ( in_array( 'withaccess', $options ) ) {
  1840. $accesskey = self::accesskey( $name );
  1841. if ( $accesskey !== false ) {
  1842. // Should be build the same as in jquery.accessKeyLabel.js
  1843. if ( $tooltip === false || $tooltip === '' ) {
  1844. $tooltip = wfMessage( 'brackets', $accesskey )->text();
  1845. } else {
  1846. $tooltip .= wfMessage( 'word-separator' )->text();
  1847. $tooltip .= wfMessage( 'brackets', $accesskey )->text();
  1848. }
  1849. }
  1850. }
  1851. return $tooltip;
  1852. }
  1853. public static $accesskeycache;
  1854. /**
  1855. * Given the id of an interface element, constructs the appropriate
  1856. * accesskey attribute from the system messages. (Note, this is usually
  1857. * the id but isn't always, because sometimes the accesskey needs to go on
  1858. * a different element than the id, for reverse-compatibility, etc.)
  1859. *
  1860. * @since 1.16.3
  1861. * @param string $name Id of the element, minus prefixes.
  1862. * @return string Contents of the accesskey attribute (which you must HTML-
  1863. * escape), or false for no accesskey attribute
  1864. */
  1865. public static function accesskey( $name ) {
  1866. if ( isset( self::$accesskeycache[$name] ) ) {
  1867. return self::$accesskeycache[$name];
  1868. }
  1869. $message = wfMessage( "accesskey-$name" );
  1870. if ( !$message->exists() ) {
  1871. $accesskey = false;
  1872. } else {
  1873. $accesskey = $message->plain();
  1874. if ( $accesskey === '' || $accesskey === '-' ) {
  1875. # @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
  1876. # attribute, but this is broken for accesskey: that might be a useful
  1877. # value.
  1878. $accesskey = false;
  1879. }
  1880. }
  1881. self::$accesskeycache[$name] = $accesskey;
  1882. return self::$accesskeycache[$name];
  1883. }
  1884. /**
  1885. * Get a revision-deletion link, or disabled link, or nothing, depending
  1886. * on user permissions & the settings on the revision.
  1887. *
  1888. * Will use forward-compatible revision ID in the Special:RevDelete link
  1889. * if possible, otherwise the timestamp-based ID which may break after
  1890. * undeletion.
  1891. *
  1892. * @param User $user
  1893. * @param Revision $rev
  1894. * @param Title $title
  1895. * @return string HTML fragment
  1896. */
  1897. public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
  1898. $canHide = $user->isAllowed( 'deleterevision' );
  1899. if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
  1900. return '';
  1901. }
  1902. if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
  1903. return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
  1904. } else {
  1905. if ( $rev->getId() ) {
  1906. // RevDelete links using revision ID are stable across
  1907. // page deletion and undeletion; use when possible.
  1908. $query = [
  1909. 'type' => 'revision',
  1910. 'target' => $title->getPrefixedDBkey(),
  1911. 'ids' => $rev->getId()
  1912. ];
  1913. } else {
  1914. // Older deleted entries didn't save a revision ID.
  1915. // We have to refer to these by timestamp, ick!
  1916. $query = [
  1917. 'type' => 'archive',
  1918. 'target' => $title->getPrefixedDBkey(),
  1919. 'ids' => $rev->getTimestamp()
  1920. ];
  1921. }
  1922. return self::revDeleteLink( $query,
  1923. $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
  1924. }
  1925. }
  1926. /**
  1927. * Creates a (show/hide) link for deleting revisions/log entries
  1928. *
  1929. * @param array $query Query parameters to be passed to link()
  1930. * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
  1931. * @param bool $delete Set to true to use (show/hide) rather than (show)
  1932. *
  1933. * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
  1934. * span to allow for customization of appearance with CSS
  1935. */
  1936. public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
  1937. $sp = SpecialPage::getTitleFor( 'Revisiondelete' );
  1938. $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
  1939. $html = wfMessage( $msgKey )->escaped();
  1940. $tag = $restricted ? 'strong' : 'span';
  1941. $link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
  1942. return Xml::tags(
  1943. $tag,
  1944. [ 'class' => 'mw-revdelundel-link' ],
  1945. wfMessage( 'parentheses' )->rawParams( $link )->escaped()
  1946. );
  1947. }
  1948. /**
  1949. * Creates a dead (show/hide) link for deleting revisions/log entries
  1950. *
  1951. * @since 1.16.3
  1952. * @param bool $delete Set to true to use (show/hide) rather than (show)
  1953. *
  1954. * @return string HTML text wrapped in a span to allow for customization
  1955. * of appearance with CSS
  1956. */
  1957. public static function revDeleteLinkDisabled( $delete = true ) {
  1958. $msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
  1959. $html = wfMessage( $msgKey )->escaped();
  1960. $htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
  1961. return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
  1962. }
  1963. /**
  1964. * Returns the attributes for the tooltip and access key.
  1965. *
  1966. * @since 1.16.3. $msgParams introduced in 1.27
  1967. * @param string $name
  1968. * @param array $msgParams Params for constructing the message
  1969. * @param string|array|null $options Options to be passed to titleAttrib.
  1970. *
  1971. * @see Linker::titleAttrib for what options could be passed to $options.
  1972. *
  1973. * @return array
  1974. */
  1975. public static function tooltipAndAccesskeyAttribs(
  1976. $name,
  1977. array $msgParams = [],
  1978. $options = null
  1979. ) {
  1980. $options = (array)$options;
  1981. $options[] = 'withaccess';
  1982. $attribs = [
  1983. 'title' => self::titleAttrib( $name, $options, $msgParams ),
  1984. 'accesskey' => self::accesskey( $name )
  1985. ];
  1986. if ( $attribs['title'] === false ) {
  1987. unset( $attribs['title'] );
  1988. }
  1989. if ( $attribs['accesskey'] === false ) {
  1990. unset( $attribs['accesskey'] );
  1991. }
  1992. return $attribs;
  1993. }
  1994. /**
  1995. * Returns raw bits of HTML, use titleAttrib()
  1996. * @since 1.16.3
  1997. * @param string $name
  1998. * @param array|null $options
  1999. * @return null|string
  2000. */
  2001. public static function tooltip( $name, $options = null ) {
  2002. $tooltip = self::titleAttrib( $name, $options );
  2003. if ( $tooltip === false ) {
  2004. return '';
  2005. }
  2006. return Xml::expandAttributes( [
  2007. 'title' => $tooltip
  2008. ] );
  2009. }
  2010. }