Linker.php 73 KB

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