You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2065 lines
61 KiB

  1. <?php
  2. use dokuwiki\ChangeLog\MediaChangeLog;
  3. use dokuwiki\File\MediaResolver;
  4. use dokuwiki\File\PageResolver;
  5. use dokuwiki\Utf8\PhpString;
  6. use SimplePie\Author;
  7. /**
  8. * Renderer for XHTML output
  9. *
  10. * This is DokuWiki's main renderer used to display page content in the wiki
  11. *
  12. * @author Harry Fuecks <hfuecks@gmail.com>
  13. * @author Andreas Gohr <andi@splitbrain.org>
  14. *
  15. */
  16. class Doku_Renderer_xhtml extends Doku_Renderer
  17. {
  18. /** @var array store the table of contents */
  19. public $toc = [];
  20. /** @var array A stack of section edit data */
  21. protected $sectionedits = [];
  22. /** @var int last section edit id, used by startSectionEdit */
  23. protected $lastsecid = 0;
  24. /** @var array a list of footnotes, list starts at 1! */
  25. protected $footnotes = [];
  26. /** @var int current section level */
  27. protected $lastlevel = 0;
  28. /** @var array section node tracker */
  29. protected $node = [0, 0, 0, 0, 0];
  30. /** @var string temporary $doc store */
  31. protected $store = '';
  32. /** @var array global counter, for table classes etc. */
  33. protected $_counter = []; //
  34. /** @var int counts the code and file blocks, used to provide download links */
  35. protected $_codeblock = 0;
  36. /** @var array list of allowed URL schemes */
  37. protected $schemes;
  38. /**
  39. * Register a new edit section range
  40. *
  41. * @param int $start The byte position for the edit start
  42. * @param array $data Associative array with section data:
  43. * Key 'name': the section name/title
  44. * Key 'target': the target for the section edit,
  45. * e.g. 'section' or 'table'
  46. * Key 'hid': header id
  47. * Key 'codeblockOffset': actual code block index
  48. * Key 'start': set in startSectionEdit(),
  49. * do not set yourself
  50. * Key 'range': calculated from 'start' and
  51. * $key in finishSectionEdit(),
  52. * do not set yourself
  53. * @return string A marker class for the starting HTML element
  54. *
  55. * @author Adrian Lang <lang@cosmocode.de>
  56. */
  57. public function startSectionEdit($start, $data)
  58. {
  59. if (!is_array($data)) {
  60. msg(
  61. sprintf(
  62. 'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
  63. hsc((string)$data)
  64. ),
  65. -1
  66. );
  67. // @deprecated 2018-04-14, backward compatibility
  68. $args = func_get_args();
  69. $data = [];
  70. if (isset($args[1])) $data['target'] = $args[1];
  71. if (isset($args[2])) $data['name'] = $args[2];
  72. if (isset($args[3])) $data['hid'] = $args[3];
  73. }
  74. $data['secid'] = ++$this->lastsecid;
  75. $data['start'] = $start;
  76. $this->sectionedits[] = $data;
  77. return 'sectionedit' . $data['secid'];
  78. }
  79. /**
  80. * Finish an edit section range
  81. *
  82. * @param int $end The byte position for the edit end; null for the rest of the page
  83. *
  84. * @author Adrian Lang <lang@cosmocode.de>
  85. */
  86. public function finishSectionEdit($end = null, $hid = null)
  87. {
  88. if (count($this->sectionedits) == 0) {
  89. return;
  90. }
  91. $data = array_pop($this->sectionedits);
  92. if (!is_null($end) && $end <= $data['start']) {
  93. return;
  94. }
  95. if (!is_null($hid)) {
  96. $data['hid'] .= $hid;
  97. }
  98. $data['range'] = $data['start'] . '-' . (is_null($end) ? '' : $end);
  99. unset($data['start']);
  100. $this->doc .= '<!-- EDIT' . hsc(json_encode($data, JSON_THROW_ON_ERROR)) . ' -->';
  101. }
  102. /**
  103. * Returns the format produced by this renderer.
  104. *
  105. * @return string always 'xhtml'
  106. */
  107. public function getFormat()
  108. {
  109. return 'xhtml';
  110. }
  111. /**
  112. * Initialize the document
  113. */
  114. public function document_start()
  115. {
  116. //reset some internals
  117. $this->toc = [];
  118. }
  119. /**
  120. * Finalize the document
  121. */
  122. public function document_end()
  123. {
  124. // Finish open section edits.
  125. while ($this->sectionedits !== []) {
  126. if ($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
  127. // If there is only one section, do not write a section edit
  128. // marker.
  129. array_pop($this->sectionedits);
  130. } else {
  131. $this->finishSectionEdit();
  132. }
  133. }
  134. if ($this->footnotes !== []) {
  135. $this->doc .= '<div class="footnotes">' . DOKU_LF;
  136. foreach ($this->footnotes as $id => $footnote) {
  137. // check its not a placeholder that indicates actual footnote text is elsewhere
  138. if (!str_starts_with($footnote, "@@FNT")) {
  139. // open the footnote and set the anchor and backlink
  140. $this->doc .= '<div class="fn">';
  141. $this->doc .= '<sup><a href="#fnt__' . $id . '" id="fn__' . $id . '" class="fn_bot">';
  142. $this->doc .= $id . ')</a></sup> ' . DOKU_LF;
  143. // get any other footnotes that use the same markup
  144. $alt = array_keys($this->footnotes, "@@FNT$id");
  145. foreach ($alt as $ref) {
  146. // set anchor and backlink for the other footnotes
  147. $this->doc .= ', <sup><a href="#fnt__' . ($ref) . '" id="fn__' . ($ref) . '" class="fn_bot">';
  148. $this->doc .= ($ref) . ')</a></sup> ' . DOKU_LF;
  149. }
  150. // add footnote markup and close this footnote
  151. $this->doc .= '<div class="content">' . $footnote . '</div>';
  152. $this->doc .= '</div>' . DOKU_LF;
  153. }
  154. }
  155. $this->doc .= '</div>' . DOKU_LF;
  156. }
  157. // Prepare the TOC
  158. global $conf;
  159. if (
  160. $this->info['toc'] &&
  161. is_array($this->toc) &&
  162. $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
  163. ) {
  164. global $TOC;
  165. $TOC = $this->toc;
  166. }
  167. // make sure there are no empty paragraphs
  168. $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
  169. }
  170. /**
  171. * Add an item to the TOC
  172. *
  173. * @param string $id the hash link
  174. * @param string $text the text to display
  175. * @param int $level the nesting level
  176. */
  177. public function toc_additem($id, $text, $level)
  178. {
  179. global $conf;
  180. //handle TOC
  181. if ($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
  182. $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
  183. }
  184. }
  185. /**
  186. * Render a heading
  187. *
  188. * @param string $text the text to display
  189. * @param int $level header level
  190. * @param int $pos byte position in the original source
  191. * @param bool $returnonly whether to return html or write to doc attribute
  192. * @return void|string writes to doc attribute or returns html depends on $returnonly
  193. */
  194. public function header($text, $level, $pos, $returnonly = false)
  195. {
  196. global $conf;
  197. if (blank($text)) return; //skip empty headlines
  198. $hid = $this->_headerToLink($text, true);
  199. //only add items within configured levels
  200. $this->toc_additem($hid, $text, $level);
  201. // adjust $node to reflect hierarchy of levels
  202. $this->node[$level - 1]++;
  203. if ($level < $this->lastlevel) {
  204. for ($i = 0; $i < $this->lastlevel - $level; $i++) {
  205. $this->node[$this->lastlevel - $i - 1] = 0;
  206. }
  207. }
  208. $this->lastlevel = $level;
  209. if (
  210. $level <= $conf['maxseclevel'] &&
  211. $this->sectionedits !== [] &&
  212. $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
  213. ) {
  214. $this->finishSectionEdit($pos - 1);
  215. }
  216. // build the header
  217. $header = DOKU_LF . '<h' . $level;
  218. if ($level <= $conf['maxseclevel']) {
  219. $data = [];
  220. $data['target'] = 'section';
  221. $data['name'] = $text;
  222. $data['hid'] = $hid;
  223. $data['codeblockOffset'] = $this->_codeblock;
  224. $header .= ' class="' . $this->startSectionEdit($pos, $data) . '"';
  225. }
  226. $header .= ' id="' . $hid . '">';
  227. $header .= $this->_xmlEntities($text);
  228. $header .= "</h$level>" . DOKU_LF;
  229. if ($returnonly) {
  230. return $header;
  231. } else {
  232. $this->doc .= $header;
  233. }
  234. }
  235. /**
  236. * Open a new section
  237. *
  238. * @param int $level section level (as determined by the previous header)
  239. */
  240. public function section_open($level)
  241. {
  242. $this->doc .= '<div class="level' . $level . '">' . DOKU_LF;
  243. }
  244. /**
  245. * Close the current section
  246. */
  247. public function section_close()
  248. {
  249. $this->doc .= DOKU_LF . '</div>' . DOKU_LF;
  250. }
  251. /**
  252. * Render plain text data
  253. *
  254. * @param $text
  255. */
  256. public function cdata($text)
  257. {
  258. $this->doc .= $this->_xmlEntities($text);
  259. }
  260. /**
  261. * Open a paragraph
  262. */
  263. public function p_open()
  264. {
  265. $this->doc .= DOKU_LF . '<p>' . DOKU_LF;
  266. }
  267. /**
  268. * Close a paragraph
  269. */
  270. public function p_close()
  271. {
  272. $this->doc .= DOKU_LF . '</p>' . DOKU_LF;
  273. }
  274. /**
  275. * Create a line break
  276. */
  277. public function linebreak()
  278. {
  279. $this->doc .= '<br/>' . DOKU_LF;
  280. }
  281. /**
  282. * Create a horizontal line
  283. */
  284. public function hr()
  285. {
  286. $this->doc .= '<hr />' . DOKU_LF;
  287. }
  288. /**
  289. * Start strong (bold) formatting
  290. */
  291. public function strong_open()
  292. {
  293. $this->doc .= '<strong>';
  294. }
  295. /**
  296. * Stop strong (bold) formatting
  297. */
  298. public function strong_close()
  299. {
  300. $this->doc .= '</strong>';
  301. }
  302. /**
  303. * Start emphasis (italics) formatting
  304. */
  305. public function emphasis_open()
  306. {
  307. $this->doc .= '<em>';
  308. }
  309. /**
  310. * Stop emphasis (italics) formatting
  311. */
  312. public function emphasis_close()
  313. {
  314. $this->doc .= '</em>';
  315. }
  316. /**
  317. * Start underline formatting
  318. */
  319. public function underline_open()
  320. {
  321. $this->doc .= '<em class="u">';
  322. }
  323. /**
  324. * Stop underline formatting
  325. */
  326. public function underline_close()
  327. {
  328. $this->doc .= '</em>';
  329. }
  330. /**
  331. * Start monospace formatting
  332. */
  333. public function monospace_open()
  334. {
  335. $this->doc .= '<code>';
  336. }
  337. /**
  338. * Stop monospace formatting
  339. */
  340. public function monospace_close()
  341. {
  342. $this->doc .= '</code>';
  343. }
  344. /**
  345. * Start a subscript
  346. */
  347. public function subscript_open()
  348. {
  349. $this->doc .= '<sub>';
  350. }
  351. /**
  352. * Stop a subscript
  353. */
  354. public function subscript_close()
  355. {
  356. $this->doc .= '</sub>';
  357. }
  358. /**
  359. * Start a superscript
  360. */
  361. public function superscript_open()
  362. {
  363. $this->doc .= '<sup>';
  364. }
  365. /**
  366. * Stop a superscript
  367. */
  368. public function superscript_close()
  369. {
  370. $this->doc .= '</sup>';
  371. }
  372. /**
  373. * Start deleted (strike-through) formatting
  374. */
  375. public function deleted_open()
  376. {
  377. $this->doc .= '<del>';
  378. }
  379. /**
  380. * Stop deleted (strike-through) formatting
  381. */
  382. public function deleted_close()
  383. {
  384. $this->doc .= '</del>';
  385. }
  386. /**
  387. * Callback for footnote start syntax
  388. *
  389. * All following content will go to the footnote instead of
  390. * the document. To achieve this the previous rendered content
  391. * is moved to $store and $doc is cleared
  392. *
  393. * @author Andreas Gohr <andi@splitbrain.org>
  394. */
  395. public function footnote_open()
  396. {
  397. // move current content to store and record footnote
  398. $this->store = $this->doc;
  399. $this->doc = '';
  400. }
  401. /**
  402. * Callback for footnote end syntax
  403. *
  404. * All rendered content is moved to the $footnotes array and the old
  405. * content is restored from $store again
  406. *
  407. * @author Andreas Gohr
  408. */
  409. public function footnote_close()
  410. {
  411. /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
  412. static $fnid = 0;
  413. // assign new footnote id (we start at 1)
  414. $fnid++;
  415. // recover footnote into the stack and restore old content
  416. $footnote = $this->doc;
  417. $this->doc = $this->store;
  418. $this->store = '';
  419. // check to see if this footnote has been seen before
  420. $i = array_search($footnote, $this->footnotes);
  421. if ($i === false) {
  422. // its a new footnote, add it to the $footnotes array
  423. $this->footnotes[$fnid] = $footnote;
  424. } else {
  425. // seen this one before, save a placeholder
  426. $this->footnotes[$fnid] = "@@FNT" . ($i);
  427. }
  428. // output the footnote reference and link
  429. $this->doc .= sprintf(
  430. '<sup><a href="#fn__%d" id="fnt__%d" class="fn_top">%d)</a></sup>',
  431. $fnid,
  432. $fnid,
  433. $fnid
  434. );
  435. }
  436. /**
  437. * Open an unordered list
  438. *
  439. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  440. */
  441. public function listu_open($classes = null)
  442. {
  443. $class = '';
  444. if ($classes !== null) {
  445. if (is_array($classes)) $classes = implode(' ', $classes);
  446. $class = " class=\"$classes\"";
  447. }
  448. $this->doc .= "<ul$class>" . DOKU_LF;
  449. }
  450. /**
  451. * Close an unordered list
  452. */
  453. public function listu_close()
  454. {
  455. $this->doc .= '</ul>' . DOKU_LF;
  456. }
  457. /**
  458. * Open an ordered list
  459. *
  460. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  461. */
  462. public function listo_open($classes = null)
  463. {
  464. $class = '';
  465. if ($classes !== null) {
  466. if (is_array($classes)) $classes = implode(' ', $classes);
  467. $class = " class=\"$classes\"";
  468. }
  469. $this->doc .= "<ol$class>" . DOKU_LF;
  470. }
  471. /**
  472. * Close an ordered list
  473. */
  474. public function listo_close()
  475. {
  476. $this->doc .= '</ol>' . DOKU_LF;
  477. }
  478. /**
  479. * Open a list item
  480. *
  481. * @param int $level the nesting level
  482. * @param bool $node true when a node; false when a leaf
  483. */
  484. public function listitem_open($level, $node = false)
  485. {
  486. $branching = $node ? ' node' : '';
  487. $this->doc .= '<li class="level' . $level . $branching . '">';
  488. }
  489. /**
  490. * Close a list item
  491. */
  492. public function listitem_close()
  493. {
  494. $this->doc .= '</li>' . DOKU_LF;
  495. }
  496. /**
  497. * Start the content of a list item
  498. */
  499. public function listcontent_open()
  500. {
  501. $this->doc .= '<div class="li">';
  502. }
  503. /**
  504. * Stop the content of a list item
  505. */
  506. public function listcontent_close()
  507. {
  508. $this->doc .= '</div>' . DOKU_LF;
  509. }
  510. /**
  511. * Output unformatted $text
  512. *
  513. * Defaults to $this->cdata()
  514. *
  515. * @param string $text
  516. */
  517. public function unformatted($text)
  518. {
  519. $this->doc .= $this->_xmlEntities($text);
  520. }
  521. /**
  522. * Start a block quote
  523. */
  524. public function quote_open()
  525. {
  526. $this->doc .= '<blockquote><div class="no">' . DOKU_LF;
  527. }
  528. /**
  529. * Stop a block quote
  530. */
  531. public function quote_close()
  532. {
  533. $this->doc .= '</div></blockquote>' . DOKU_LF;
  534. }
  535. /**
  536. * Output preformatted text
  537. *
  538. * @param string $text
  539. */
  540. public function preformatted($text)
  541. {
  542. $this->doc .= '<pre class="code">' . trim($this->_xmlEntities($text), "\n\r") . '</pre>' . DOKU_LF;
  543. }
  544. /**
  545. * Display text as file content, optionally syntax highlighted
  546. *
  547. * @param string $text text to show
  548. * @param string $language programming language to use for syntax highlighting
  549. * @param string $filename file path label
  550. * @param array $options assoziative array with additional geshi options
  551. */
  552. public function file($text, $language = null, $filename = null, $options = null)
  553. {
  554. $this->_highlight('file', $text, $language, $filename, $options);
  555. }
  556. /**
  557. * Display text as code content, optionally syntax highlighted
  558. *
  559. * @param string $text text to show
  560. * @param string $language programming language to use for syntax highlighting
  561. * @param string $filename file path label
  562. * @param array $options assoziative array with additional geshi options
  563. */
  564. public function code($text, $language = null, $filename = null, $options = null)
  565. {
  566. $this->_highlight('code', $text, $language, $filename, $options);
  567. }
  568. /**
  569. * Use GeSHi to highlight language syntax in code and file blocks
  570. *
  571. * @param string $type code|file
  572. * @param string $text text to show
  573. * @param string $language programming language to use for syntax highlighting
  574. * @param string $filename file path label
  575. * @param array $options assoziative array with additional geshi options
  576. * @author Andreas Gohr <andi@splitbrain.org>
  577. */
  578. public function _highlight($type, $text, $language = null, $filename = null, $options = null)
  579. {
  580. global $ID;
  581. global $lang;
  582. global $INPUT;
  583. $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language ?? '');
  584. if ($filename) {
  585. // add icon
  586. [$ext] = mimetype($filename, false);
  587. $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
  588. $class = 'mediafile mf_' . $class;
  589. $offset = 0;
  590. if ($INPUT->has('codeblockOffset')) {
  591. $offset = $INPUT->str('codeblockOffset');
  592. }
  593. $this->doc .= '<dl class="' . $type . '">' . DOKU_LF;
  594. $this->doc .= '<dt><a href="' .
  595. exportlink(
  596. $ID,
  597. 'code',
  598. ['codeblock' => $offset + $this->_codeblock]
  599. ) . '" title="' . $lang['download'] . '" class="' . $class . '">';
  600. $this->doc .= hsc($filename);
  601. $this->doc .= '</a></dt>' . DOKU_LF . '<dd>';
  602. }
  603. if (str_starts_with($text, "\n")) {
  604. $text = substr($text, 1);
  605. }
  606. if (str_ends_with($text, "\n")) {
  607. $text = substr($text, 0, -1);
  608. }
  609. if (empty($language)) { // empty is faster than is_null and can prevent '' string
  610. $this->doc .= '<pre class="' . $type . '">' . $this->_xmlEntities($text) . '</pre>' . DOKU_LF;
  611. } else {
  612. $class = 'code'; //we always need the code class to make the syntax highlighting apply
  613. if ($type != 'code') $class .= ' ' . $type;
  614. $this->doc .= "<pre class=\"$class $language\">" .
  615. p_xhtml_cached_geshi($text, $language, '', $options) .
  616. '</pre>' . DOKU_LF;
  617. }
  618. if ($filename) {
  619. $this->doc .= '</dd></dl>' . DOKU_LF;
  620. }
  621. $this->_codeblock++;
  622. }
  623. /**
  624. * Format an acronym
  625. *
  626. * Uses $this->acronyms
  627. *
  628. * @param string $acronym
  629. */
  630. public function acronym($acronym)
  631. {
  632. if (array_key_exists($acronym, $this->acronyms)) {
  633. $title = $this->_xmlEntities($this->acronyms[$acronym]);
  634. $this->doc .= '<abbr title="' . $title
  635. . '">' . $this->_xmlEntities($acronym) . '</abbr>';
  636. } else {
  637. $this->doc .= $this->_xmlEntities($acronym);
  638. }
  639. }
  640. /**
  641. * Format a smiley
  642. *
  643. * Uses $this->smiley
  644. *
  645. * @param string $smiley
  646. */
  647. public function smiley($smiley)
  648. {
  649. if (isset($this->smileys[$smiley])) {
  650. $this->doc .= '<img src="' . DOKU_BASE . 'lib/images/smileys/' . $this->smileys[$smiley] .
  651. '" class="icon smiley" alt="' . $this->_xmlEntities($smiley) . '" />';
  652. } else {
  653. $this->doc .= $this->_xmlEntities($smiley);
  654. }
  655. }
  656. /**
  657. * Format an entity
  658. *
  659. * Entities are basically small text replacements
  660. *
  661. * Uses $this->entities
  662. *
  663. * @param string $entity
  664. */
  665. public function entity($entity)
  666. {
  667. if (array_key_exists($entity, $this->entities)) {
  668. $this->doc .= $this->entities[$entity];
  669. } else {
  670. $this->doc .= $this->_xmlEntities($entity);
  671. }
  672. }
  673. /**
  674. * Typographically format a multiply sign
  675. *
  676. * Example: ($x=640, $y=480) should result in "640×480"
  677. *
  678. * @param string|int $x first value
  679. * @param string|int $y second value
  680. */
  681. public function multiplyentity($x, $y)
  682. {
  683. $this->doc .= "$x&times;$y";
  684. }
  685. /**
  686. * Render an opening single quote char (language specific)
  687. */
  688. public function singlequoteopening()
  689. {
  690. global $lang;
  691. $this->doc .= $lang['singlequoteopening'];
  692. }
  693. /**
  694. * Render a closing single quote char (language specific)
  695. */
  696. public function singlequoteclosing()
  697. {
  698. global $lang;
  699. $this->doc .= $lang['singlequoteclosing'];
  700. }
  701. /**
  702. * Render an apostrophe char (language specific)
  703. */
  704. public function apostrophe()
  705. {
  706. global $lang;
  707. $this->doc .= $lang['apostrophe'];
  708. }
  709. /**
  710. * Render an opening double quote char (language specific)
  711. */
  712. public function doublequoteopening()
  713. {
  714. global $lang;
  715. $this->doc .= $lang['doublequoteopening'];
  716. }
  717. /**
  718. * Render an closinging double quote char (language specific)
  719. */
  720. public function doublequoteclosing()
  721. {
  722. global $lang;
  723. $this->doc .= $lang['doublequoteclosing'];
  724. }
  725. /**
  726. * Render a CamelCase link
  727. *
  728. * @param string $link The link name
  729. * @param bool $returnonly whether to return html or write to doc attribute
  730. * @return void|string writes to doc attribute or returns html depends on $returnonly
  731. *
  732. * @see http://en.wikipedia.org/wiki/CamelCase
  733. */
  734. public function camelcaselink($link, $returnonly = false)
  735. {
  736. if ($returnonly) {
  737. return $this->internallink($link, $link, null, true);
  738. } else {
  739. $this->internallink($link, $link);
  740. }
  741. }
  742. /**
  743. * Render a page local link
  744. *
  745. * @param string $hash hash link identifier
  746. * @param string $name name for the link
  747. * @param bool $returnonly whether to return html or write to doc attribute
  748. * @return void|string writes to doc attribute or returns html depends on $returnonly
  749. */
  750. public function locallink($hash, $name = null, $returnonly = false)
  751. {
  752. global $ID;
  753. $name = $this->_getLinkTitle($name, $hash, $isImage);
  754. $hash = $this->_headerToLink($hash);
  755. $title = $ID . ' ↵';
  756. $doc = '<a href="#' . $hash . '" title="' . $title . '" class="wikilink1">';
  757. $doc .= $name;
  758. $doc .= '</a>';
  759. if ($returnonly) {
  760. return $doc;
  761. } else {
  762. $this->doc .= $doc;
  763. }
  764. }
  765. /**
  766. * Render an internal Wiki Link
  767. *
  768. * $search,$returnonly & $linktype are not for the renderer but are used
  769. * elsewhere - no need to implement them in other renderers
  770. *
  771. * @param string $id pageid
  772. * @param string|null $name link name
  773. * @param string|null $search adds search url param
  774. * @param bool $returnonly whether to return html or write to doc attribute
  775. * @param string $linktype type to set use of headings
  776. * @return void|string writes to doc attribute or returns html depends on $returnonly
  777. * @author Andreas Gohr <andi@splitbrain.org>
  778. */
  779. public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
  780. {
  781. global $conf;
  782. global $ID;
  783. global $INFO;
  784. $params = '';
  785. $parts = explode('?', $id, 2);
  786. if (count($parts) === 2) {
  787. $id = $parts[0];
  788. $params = $parts[1];
  789. }
  790. // For empty $id we need to know the current $ID
  791. // We need this check because _simpleTitle needs
  792. // correct $id and resolve_pageid() use cleanID($id)
  793. // (some things could be lost)
  794. if ($id === '') {
  795. $id = $ID;
  796. }
  797. // default name is based on $id as given
  798. $default = $this->_simpleTitle($id);
  799. // now first resolve and clean up the $id
  800. $id = (new PageResolver($ID))->resolveId($id, $this->date_at, true);
  801. $exists = page_exists($id, $this->date_at, false, true);
  802. $link = [];
  803. $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
  804. if (!$isImage) {
  805. if ($exists) {
  806. $class = 'wikilink1';
  807. } else {
  808. $class = 'wikilink2';
  809. $link['rel'] = 'nofollow';
  810. }
  811. } else {
  812. $class = 'media';
  813. }
  814. //keep hash anchor
  815. [$id, $hash] = sexplode('#', $id, 2);
  816. if (!empty($hash)) $hash = $this->_headerToLink($hash);
  817. //prepare for formating
  818. $link['target'] = $conf['target']['wiki'];
  819. $link['style'] = '';
  820. $link['pre'] = '';
  821. $link['suf'] = '';
  822. $link['more'] = 'data-wiki-id="' . $id . '"'; // id is already cleaned
  823. $link['class'] = $class;
  824. if ($this->date_at) {
  825. $params = $params . '&at=' . rawurlencode($this->date_at);
  826. }
  827. $link['url'] = wl($id, $params);
  828. $link['name'] = $name;
  829. $link['title'] = $id;
  830. //add search string
  831. if ($search) {
  832. ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
  833. if (is_array($search)) {
  834. $search = array_map('rawurlencode', $search);
  835. $link['url'] .= 's[]=' . implode('&amp;s[]=', $search);
  836. } else {
  837. $link['url'] .= 's=' . rawurlencode($search);
  838. }
  839. }
  840. //keep hash
  841. if ($hash) $link['url'] .= '#' . $hash;
  842. //output formatted
  843. if ($returnonly) {
  844. return $this->_formatLink($link);
  845. } else {
  846. $this->doc .= $this->_formatLink($link);
  847. }
  848. }
  849. /**
  850. * Render an external link
  851. *
  852. * @param string $url full URL with scheme
  853. * @param string|array $name name for the link, array for media file
  854. * @param bool $returnonly whether to return html or write to doc attribute
  855. * @return void|string writes to doc attribute or returns html depends on $returnonly
  856. */
  857. public function externallink($url, $name = null, $returnonly = false)
  858. {
  859. global $conf;
  860. $name = $this->_getLinkTitle($name, $url, $isImage);
  861. // url might be an attack vector, only allow registered protocols
  862. if (is_null($this->schemes)) $this->schemes = getSchemes();
  863. [$scheme] = explode('://', $url);
  864. $scheme = strtolower($scheme);
  865. if (!in_array($scheme, $this->schemes)) $url = '';
  866. // is there still an URL?
  867. if (!$url) {
  868. if ($returnonly) {
  869. return $name;
  870. } else {
  871. $this->doc .= $name;
  872. }
  873. return;
  874. }
  875. // set class
  876. if (!$isImage) {
  877. $class = 'urlextern';
  878. } else {
  879. $class = 'media';
  880. }
  881. //prepare for formating
  882. $link = [];
  883. $link['target'] = $conf['target']['extern'];
  884. $link['style'] = '';
  885. $link['pre'] = '';
  886. $link['suf'] = '';
  887. $link['more'] = '';
  888. $link['class'] = $class;
  889. $link['url'] = $url;
  890. $link['rel'] = '';
  891. $link['name'] = $name;
  892. $link['title'] = $this->_xmlEntities($url);
  893. if ($conf['relnofollow']) $link['rel'] .= ' ugc nofollow';
  894. if ($conf['target']['extern']) $link['rel'] .= ' noopener';
  895. //output formatted
  896. if ($returnonly) {
  897. return $this->_formatLink($link);
  898. } else {
  899. $this->doc .= $this->_formatLink($link);
  900. }
  901. }
  902. /**
  903. * Render an interwiki link
  904. *
  905. * You may want to use $this->_resolveInterWiki() here
  906. *
  907. * @param string $match original link - probably not much use
  908. * @param string|array $name name for the link, array for media file
  909. * @param string $wikiName indentifier (shortcut) for the remote wiki
  910. * @param string $wikiUri the fragment parsed from the original link
  911. * @param bool $returnonly whether to return html or write to doc attribute
  912. * @return void|string writes to doc attribute or returns html depends on $returnonly
  913. */
  914. public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false)
  915. {
  916. global $conf;
  917. $link = [];
  918. $link['target'] = $conf['target']['interwiki'];
  919. $link['pre'] = '';
  920. $link['suf'] = '';
  921. $link['more'] = '';
  922. $link['name'] = $this->_getLinkTitle($name, $wikiUri, $isImage);
  923. $link['rel'] = '';
  924. //get interwiki URL
  925. $exists = null;
  926. $url = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
  927. if (!$isImage) {
  928. $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
  929. $link['class'] = "interwiki iw_$class";
  930. } else {
  931. $link['class'] = 'media';
  932. }
  933. //do we stay at the same server? Use local target
  934. if (strpos($url, DOKU_URL) === 0 || strpos($url, DOKU_BASE) === 0) {
  935. $link['target'] = $conf['target']['wiki'];
  936. }
  937. if ($exists !== null && !$isImage) {
  938. if ($exists) {
  939. $link['class'] .= ' wikilink1';
  940. } else {
  941. $link['class'] .= ' wikilink2';
  942. $link['rel'] .= ' nofollow';
  943. }
  944. }
  945. if ($conf['target']['interwiki']) $link['rel'] .= ' noopener';
  946. $link['url'] = $url;
  947. $link['title'] = $this->_xmlEntities($link['url']);
  948. // output formatted
  949. if ($returnonly) {
  950. if ($url == '') return $link['name'];
  951. return $this->_formatLink($link);
  952. } elseif ($url == '') {
  953. $this->doc .= $link['name'];
  954. } else $this->doc .= $this->_formatLink($link);
  955. }
  956. /**
  957. * Link to windows share
  958. *
  959. * @param string $url the link
  960. * @param string|array $name name for the link, array for media file
  961. * @param bool $returnonly whether to return html or write to doc attribute
  962. * @return void|string writes to doc attribute or returns html depends on $returnonly
  963. */
  964. public function windowssharelink($url, $name = null, $returnonly = false)
  965. {
  966. global $conf;
  967. //simple setup
  968. $link = [];
  969. $link['target'] = $conf['target']['windows'];
  970. $link['pre'] = '';
  971. $link['suf'] = '';
  972. $link['style'] = '';
  973. $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
  974. if (!$isImage) {
  975. $link['class'] = 'windows';
  976. } else {
  977. $link['class'] = 'media';
  978. }
  979. $link['title'] = $this->_xmlEntities($url);
  980. $url = str_replace('\\', '/', $url);
  981. $url = 'file:///' . $url;
  982. $link['url'] = $url;
  983. //output formatted
  984. if ($returnonly) {
  985. return $this->_formatLink($link);
  986. } else {
  987. $this->doc .= $this->_formatLink($link);
  988. }
  989. }
  990. /**
  991. * Render a linked E-Mail Address
  992. *
  993. * Honors $conf['mailguard'] setting
  994. *
  995. * @param string $address Email-Address
  996. * @param string|array $name name for the link, array for media file
  997. * @param bool $returnonly whether to return html or write to doc attribute
  998. * @return void|string writes to doc attribute or returns html depends on $returnonly
  999. */
  1000. public function emaillink($address, $name = null, $returnonly = false)
  1001. {
  1002. global $conf;
  1003. //simple setup
  1004. $link = [];
  1005. $link['target'] = '';
  1006. $link['pre'] = '';
  1007. $link['suf'] = '';
  1008. $link['style'] = '';
  1009. $link['more'] = '';
  1010. $name = $this->_getLinkTitle($name, '', $isImage);
  1011. if (!$isImage) {
  1012. $link['class'] = 'mail';
  1013. } else {
  1014. $link['class'] = 'media';
  1015. }
  1016. $address = $this->_xmlEntities($address);
  1017. $address = obfuscate($address);
  1018. $title = $address;
  1019. if (empty($name)) {
  1020. $name = $address;
  1021. }
  1022. if ($conf['mailguard'] == 'visible') $address = rawurlencode($address);
  1023. $link['url'] = 'mailto:' . $address;
  1024. $link['name'] = $name;
  1025. $link['title'] = $title;
  1026. //output formatted
  1027. if ($returnonly) {
  1028. return $this->_formatLink($link);
  1029. } else {
  1030. $this->doc .= $this->_formatLink($link);
  1031. }
  1032. }
  1033. /**
  1034. * Render an internal media file
  1035. *
  1036. * @param string $src media ID
  1037. * @param string $title descriptive text
  1038. * @param string $align left|center|right
  1039. * @param int $width width of media in pixel
  1040. * @param int $height height of media in pixel
  1041. * @param string $cache cache|recache|nocache
  1042. * @param string $linking linkonly|detail|nolink
  1043. * @param bool $return return HTML instead of adding to $doc
  1044. * @return void|string writes to doc attribute or returns html depends on $return
  1045. */
  1046. public function internalmedia(
  1047. $src,
  1048. $title = null,
  1049. $align = null,
  1050. $width = null,
  1051. $height = null,
  1052. $cache = null,
  1053. $linking = null,
  1054. $return = false
  1055. ) {
  1056. global $ID;
  1057. if (strpos($src, '#') !== false) {
  1058. [$src, $hash] = sexplode('#', $src, 2);
  1059. }
  1060. $src = (new MediaResolver($ID))->resolveId($src, $this->date_at, true);
  1061. $exists = media_exists($src);
  1062. $noLink = false;
  1063. $render = $linking != 'linkonly';
  1064. $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
  1065. [$ext, $mime] = mimetype($src, false);
  1066. if (str_starts_with($mime, 'image') && $render) {
  1067. $link['url'] = ml(
  1068. $src,
  1069. [
  1070. 'id' => $ID,
  1071. 'cache' => $cache,
  1072. 'rev' => $this->_getLastMediaRevisionAt($src)
  1073. ],
  1074. ($linking == 'direct')
  1075. );
  1076. } elseif (($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
  1077. // don't link movies
  1078. $noLink = true;
  1079. } else {
  1080. // add file icons
  1081. $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
  1082. $link['class'] .= ' mediafile mf_' . $class;
  1083. $link['url'] = ml(
  1084. $src,
  1085. [
  1086. 'id' => $ID,
  1087. 'cache' => $cache,
  1088. 'rev' => $this->_getLastMediaRevisionAt($src)
  1089. ],
  1090. true
  1091. );
  1092. if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))) . ')';
  1093. }
  1094. if (!empty($hash)) $link['url'] .= '#' . $hash;
  1095. //markup non existing files
  1096. if (!$exists) {
  1097. $link['class'] .= ' wikilink2';
  1098. }
  1099. //output formatted
  1100. if ($return) {
  1101. if ($linking == 'nolink' || $noLink) {
  1102. return $link['name'];
  1103. } else {
  1104. return $this->_formatLink($link);
  1105. }
  1106. } elseif ($linking == 'nolink' || $noLink) {
  1107. $this->doc .= $link['name'];
  1108. } else {
  1109. $this->doc .= $this->_formatLink($link);
  1110. }
  1111. }
  1112. /**
  1113. * Render an external media file
  1114. *
  1115. * @param string $src full media URL
  1116. * @param string $title descriptive text
  1117. * @param string $align left|center|right
  1118. * @param int $width width of media in pixel
  1119. * @param int $height height of media in pixel
  1120. * @param string $cache cache|recache|nocache
  1121. * @param string $linking linkonly|detail|nolink
  1122. * @param bool $return return HTML instead of adding to $doc
  1123. * @return void|string writes to doc attribute or returns html depends on $return
  1124. */
  1125. public function externalmedia(
  1126. $src,
  1127. $title = null,
  1128. $align = null,
  1129. $width = null,
  1130. $height = null,
  1131. $cache = null,
  1132. $linking = null,
  1133. $return = false
  1134. ) {
  1135. if (link_isinterwiki($src)) {
  1136. [$shortcut, $reference] = sexplode('>', $src, 2, '');
  1137. $exists = null;
  1138. $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
  1139. if ($src == '' && empty($title)) {
  1140. // make sure at least something will be shown in this case
  1141. $title = $reference;
  1142. }
  1143. }
  1144. [$src, $hash] = sexplode('#', $src, 2);
  1145. $noLink = false;
  1146. if ($src == '') {
  1147. // only output plaintext without link if there is no src
  1148. $noLink = true;
  1149. }
  1150. $render = $linking != 'linkonly';
  1151. $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
  1152. $link['url'] = ml($src, ['cache' => $cache]);
  1153. [$ext, $mime] = mimetype($src, false);
  1154. if (str_starts_with($mime, 'image') && $render) {
  1155. // link only jpeg images
  1156. // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
  1157. } elseif (($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
  1158. // don't link movies
  1159. $noLink = true;
  1160. } else {
  1161. // add file icons
  1162. $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
  1163. $link['class'] .= ' mediafile mf_' . $class;
  1164. }
  1165. if ($hash) $link['url'] .= '#' . $hash;
  1166. //output formatted
  1167. if ($return) {
  1168. if ($linking == 'nolink' || $noLink) return $link['name'];
  1169. else return $this->_formatLink($link);
  1170. } elseif ($linking == 'nolink' || $noLink) {
  1171. $this->doc .= $link['name'];
  1172. } else $this->doc .= $this->_formatLink($link);
  1173. }
  1174. /**
  1175. * Renders an RSS feed
  1176. *
  1177. * @param string $url URL of the feed
  1178. * @param array $params Finetuning of the output
  1179. *
  1180. * @author Andreas Gohr <andi@splitbrain.org>
  1181. */
  1182. public function rss($url, $params)
  1183. {
  1184. global $lang;
  1185. global $conf;
  1186. require_once(DOKU_INC . 'inc/FeedParser.php');
  1187. $feed = new FeedParser();
  1188. $feed->set_feed_url($url);
  1189. //disable warning while fetching
  1190. if (!defined('DOKU_E_LEVEL')) {
  1191. $elvl = error_reporting(E_ERROR);
  1192. }
  1193. $rc = $feed->init();
  1194. if (isset($elvl)) {
  1195. error_reporting($elvl);
  1196. }
  1197. if ($params['nosort']) $feed->enable_order_by_date(false);
  1198. //decide on start and end
  1199. if ($params['reverse']) {
  1200. $mod = -1;
  1201. $start = $feed->get_item_quantity() - 1;
  1202. $end = $start - ($params['max']);
  1203. $end = ($end < -1) ? -1 : $end;
  1204. } else {
  1205. $mod = 1;
  1206. $start = 0;
  1207. $end = $feed->get_item_quantity();
  1208. $end = ($end > $params['max']) ? $params['max'] : $end;
  1209. }
  1210. $this->doc .= '<ul class="rss">';
  1211. if ($rc) {
  1212. for ($x = $start; $x != $end; $x += $mod) {
  1213. $item = $feed->get_item($x);
  1214. $this->doc .= '<li><div class="li">';
  1215. $lnkurl = $item->get_permalink();
  1216. $title = html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8');
  1217. // support feeds without links
  1218. if ($lnkurl) {
  1219. $this->externallink($item->get_permalink(), $title);
  1220. } else {
  1221. $this->doc .= ' ' . hsc($item->get_title());
  1222. }
  1223. if ($params['author']) {
  1224. $author = $item->get_author(0);
  1225. if ($author instanceof Author) {
  1226. $name = $author->get_name();
  1227. if (!$name) $name = $author->get_email();
  1228. if ($name) $this->doc .= ' ' . $lang['by'] . ' ' . hsc($name);
  1229. }
  1230. }
  1231. if ($params['date']) {
  1232. $this->doc .= ' (' . $item->get_local_date($conf['dformat']) . ')';
  1233. }
  1234. if ($params['details']) {
  1235. $desc = $item->get_description();
  1236. $desc = strip_tags($desc);
  1237. $desc = html_entity_decode($desc, ENT_QUOTES, 'UTF-8');
  1238. $this->doc .= '<div class="detail">';
  1239. $this->doc .= hsc($desc);
  1240. $this->doc .= '</div>';
  1241. }
  1242. $this->doc .= '</div></li>';
  1243. }
  1244. } else {
  1245. $this->doc .= '<li><div class="li">';
  1246. $this->doc .= '<em>' . $lang['rssfailed'] . '</em>';
  1247. $this->externallink($url);
  1248. if ($conf['allowdebug']) {
  1249. $this->doc .= '<!--' . hsc($feed->error) . '-->';
  1250. }
  1251. $this->doc .= '</div></li>';
  1252. }
  1253. $this->doc .= '</ul>';
  1254. }
  1255. /**
  1256. * Start a table
  1257. *
  1258. * @param int $maxcols maximum number of columns
  1259. * @param int $numrows NOT IMPLEMENTED
  1260. * @param int $pos byte position in the original source
  1261. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  1262. */
  1263. public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null)
  1264. {
  1265. // initialize the row counter used for classes
  1266. $this->_counter['row_counter'] = 0;
  1267. $class = 'table';
  1268. if ($classes !== null) {
  1269. if (is_array($classes)) $classes = implode(' ', $classes);
  1270. $class .= ' ' . $classes;
  1271. }
  1272. if ($pos !== null) {
  1273. $hid = $this->_headerToLink($class, true);
  1274. $data = [];
  1275. $data['target'] = 'table';
  1276. $data['name'] = '';
  1277. $data['hid'] = $hid;
  1278. $class .= ' ' . $this->startSectionEdit($pos, $data);
  1279. }
  1280. $this->doc .= '<div class="' . $class . '"><table class="inline">' .
  1281. DOKU_LF;
  1282. }
  1283. /**
  1284. * Close a table
  1285. *
  1286. * @param int $pos byte position in the original source
  1287. */
  1288. public function table_close($pos = null)
  1289. {
  1290. $this->doc .= '</table></div>' . DOKU_LF;
  1291. if ($pos !== null) {
  1292. $this->finishSectionEdit($pos);
  1293. }
  1294. }
  1295. /**
  1296. * Open a table header
  1297. */
  1298. public function tablethead_open()
  1299. {
  1300. $this->doc .= DOKU_TAB . '<thead>' . DOKU_LF;
  1301. }
  1302. /**
  1303. * Close a table header
  1304. */
  1305. public function tablethead_close()
  1306. {
  1307. $this->doc .= DOKU_TAB . '</thead>' . DOKU_LF;
  1308. }
  1309. /**
  1310. * Open a table body
  1311. */
  1312. public function tabletbody_open()
  1313. {
  1314. $this->doc .= DOKU_TAB . '<tbody>' . DOKU_LF;
  1315. }
  1316. /**
  1317. * Close a table body
  1318. */
  1319. public function tabletbody_close()
  1320. {
  1321. $this->doc .= DOKU_TAB . '</tbody>' . DOKU_LF;
  1322. }
  1323. /**
  1324. * Open a table footer
  1325. */
  1326. public function tabletfoot_open()
  1327. {
  1328. $this->doc .= DOKU_TAB . '<tfoot>' . DOKU_LF;
  1329. }
  1330. /**
  1331. * Close a table footer
  1332. */
  1333. public function tabletfoot_close()
  1334. {
  1335. $this->doc .= DOKU_TAB . '</tfoot>' . DOKU_LF;
  1336. }
  1337. /**
  1338. * Open a table row
  1339. *
  1340. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  1341. */
  1342. public function tablerow_open($classes = null)
  1343. {
  1344. // initialize the cell counter used for classes
  1345. $this->_counter['cell_counter'] = 0;
  1346. $class = 'row' . $this->_counter['row_counter']++;
  1347. if ($classes !== null) {
  1348. if (is_array($classes)) $classes = implode(' ', $classes);
  1349. $class .= ' ' . $classes;
  1350. }
  1351. $this->doc .= DOKU_TAB . '<tr class="' . $class . '">' . DOKU_LF . DOKU_TAB . DOKU_TAB;
  1352. }
  1353. /**
  1354. * Close a table row
  1355. */
  1356. public function tablerow_close()
  1357. {
  1358. $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF;
  1359. }
  1360. /**
  1361. * Open a table header cell
  1362. *
  1363. * @param int $colspan
  1364. * @param string $align left|center|right
  1365. * @param int $rowspan
  1366. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  1367. */
  1368. public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null)
  1369. {
  1370. $class = 'class="col' . $this->_counter['cell_counter']++;
  1371. if (!is_null($align)) {
  1372. $class .= ' ' . $align . 'align';
  1373. }
  1374. if ($classes !== null) {
  1375. if (is_array($classes)) $classes = implode(' ', $classes);
  1376. $class .= ' ' . $classes;
  1377. }
  1378. $class .= '"';
  1379. $this->doc .= '<th ' . $class;
  1380. if ($colspan > 1) {
  1381. $this->_counter['cell_counter'] += $colspan - 1;
  1382. $this->doc .= ' colspan="' . $colspan . '"';
  1383. }
  1384. if ($rowspan > 1) {
  1385. $this->doc .= ' rowspan="' . $rowspan . '"';
  1386. }
  1387. $this->doc .= '>';
  1388. }
  1389. /**
  1390. * Close a table header cell
  1391. */
  1392. public function tableheader_close()
  1393. {
  1394. $this->doc .= '</th>';
  1395. }
  1396. /**
  1397. * Open a table cell
  1398. *
  1399. * @param int $colspan
  1400. * @param string $align left|center|right
  1401. * @param int $rowspan
  1402. * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
  1403. */
  1404. public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null)
  1405. {
  1406. $class = 'class="col' . $this->_counter['cell_counter']++;
  1407. if (!is_null($align)) {
  1408. $class .= ' ' . $align . 'align';
  1409. }
  1410. if ($classes !== null) {
  1411. if (is_array($classes)) $classes = implode(' ', $classes);
  1412. $class .= ' ' . $classes;
  1413. }
  1414. $class .= '"';
  1415. $this->doc .= '<td ' . $class;
  1416. if ($colspan > 1) {
  1417. $this->_counter['cell_counter'] += $colspan - 1;
  1418. $this->doc .= ' colspan="' . $colspan . '"';
  1419. }
  1420. if ($rowspan > 1) {
  1421. $this->doc .= ' rowspan="' . $rowspan . '"';
  1422. }
  1423. $this->doc .= '>';
  1424. }
  1425. /**
  1426. * Close a table cell
  1427. */
  1428. public function tablecell_close()
  1429. {
  1430. $this->doc .= '</td>';
  1431. }
  1432. /**
  1433. * Returns the current header level.
  1434. * (required e.g. by the filelist plugin)
  1435. *
  1436. * @return int The current header level
  1437. */
  1438. public function getLastlevel()
  1439. {
  1440. return $this->lastlevel;
  1441. }
  1442. #region Utility functions
  1443. /**
  1444. * Build a link
  1445. *
  1446. * Assembles all parts defined in $link returns HTML for the link
  1447. *
  1448. * @param array $link attributes of a link
  1449. * @return string
  1450. *
  1451. * @author Andreas Gohr <andi@splitbrain.org>
  1452. */
  1453. public function _formatLink($link)
  1454. {
  1455. //make sure the url is XHTML compliant (skip mailto)
  1456. if (!str_starts_with($link['url'], 'mailto:')) {
  1457. $link['url'] = str_replace('&', '&amp;', $link['url']);
  1458. $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
  1459. }
  1460. //remove double encodings in titles
  1461. $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
  1462. // be sure there are no bad chars in url or title
  1463. // (we can't do this for name because it can contain an img tag)
  1464. $link['url'] = strtr($link['url'], ['>' => '%3E', '<' => '%3C', '"' => '%22']);
  1465. $link['title'] = strtr($link['title'], ['>' => '&gt;', '<' => '&lt;', '"' => '&quot;']);
  1466. $ret = '';
  1467. $ret .= $link['pre'];
  1468. $ret .= '<a href="' . $link['url'] . '"';
  1469. if (!empty($link['class'])) $ret .= ' class="' . $link['class'] . '"';
  1470. if (!empty($link['target'])) $ret .= ' target="' . $link['target'] . '"';
  1471. if (!empty($link['title'])) $ret .= ' title="' . $link['title'] . '"';
  1472. if (!empty($link['style'])) $ret .= ' style="' . $link['style'] . '"';
  1473. if (!empty($link['rel'])) $ret .= ' rel="' . trim($link['rel']) . '"';
  1474. if (!empty($link['more'])) $ret .= ' ' . $link['more'];
  1475. $ret .= '>';
  1476. $ret .= $link['name'];
  1477. $ret .= '</a>';
  1478. $ret .= $link['suf'];
  1479. return $ret;
  1480. }
  1481. /**
  1482. * Renders internal and external media
  1483. *
  1484. * @param string $src media ID
  1485. * @param string $title descriptive text
  1486. * @param string $align left|center|right
  1487. * @param int $width width of media in pixel
  1488. * @param int $height height of media in pixel
  1489. * @param string $cache cache|recache|nocache
  1490. * @param bool $render should the media be embedded inline or just linked
  1491. * @return string
  1492. * @author Andreas Gohr <andi@splitbrain.org>
  1493. */
  1494. public function _media(
  1495. $src,
  1496. $title = null,
  1497. $align = null,
  1498. $width = null,
  1499. $height = null,
  1500. $cache = null,
  1501. $render = true
  1502. ) {
  1503. $ret = '';
  1504. [$ext, $mime] = mimetype($src);
  1505. if (str_starts_with($mime, 'image')) {
  1506. // first get the $title
  1507. if (!is_null($title)) {
  1508. $title = $this->_xmlEntities($title);
  1509. } elseif ($ext == 'jpg' || $ext == 'jpeg') {
  1510. //try to use the caption from IPTC/EXIF
  1511. require_once(DOKU_INC . 'inc/JpegMeta.php');
  1512. $jpeg = new JpegMeta(mediaFN($src));
  1513. $cap = $jpeg->getTitle();
  1514. if (!empty($cap)) {
  1515. $title = $this->_xmlEntities($cap);
  1516. }
  1517. }
  1518. if (!$render) {
  1519. // if the picture is not supposed to be rendered
  1520. // return the title of the picture
  1521. if ($title === null || $title === "") {
  1522. // just show the sourcename
  1523. $title = $this->_xmlEntities(PhpString::basename(noNS($src)));
  1524. }
  1525. return $title;
  1526. }
  1527. //add image tag
  1528. $ret .= '<img src="' . ml(
  1529. $src,
  1530. [
  1531. 'w' => $width,
  1532. 'h' => $height,
  1533. 'cache' => $cache,
  1534. 'rev' => $this->_getLastMediaRevisionAt($src)
  1535. ]
  1536. ) . '"';
  1537. $ret .= ' class="media' . $align . '"';
  1538. $ret .= ' loading="lazy"';
  1539. if ($title) {
  1540. $ret .= ' title="' . $title . '"';
  1541. $ret .= ' alt="' . $title . '"';
  1542. } else {
  1543. $ret .= ' alt=""';
  1544. }
  1545. if (!is_null($width)) {
  1546. $ret .= ' width="' . $this->_xmlEntities($width) . '"';
  1547. }
  1548. if (!is_null($height)) {
  1549. $ret .= ' height="' . $this->_xmlEntities($height) . '"';
  1550. }
  1551. $ret .= ' />';
  1552. } elseif (media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
  1553. // first get the $title
  1554. $title ??= false;
  1555. if (!$render) {
  1556. // if the file is not supposed to be rendered
  1557. // return the title of the file (just the sourcename if there is no title)
  1558. return $this->_xmlEntities($title ?: PhpString::basename(noNS($src)));
  1559. }
  1560. $att = [];
  1561. $att['class'] = "media$align";
  1562. if ($title) {
  1563. $att['title'] = $title;
  1564. }
  1565. if (media_supportedav($mime, 'video')) {
  1566. //add video
  1567. $ret .= $this->_video($src, $width, $height, $att);
  1568. }
  1569. if (media_supportedav($mime, 'audio')) {
  1570. //add audio
  1571. $ret .= $this->_audio($src, $att);
  1572. }
  1573. } elseif ($mime == 'application/x-shockwave-flash') {
  1574. if (!$render) {
  1575. // if the flash is not supposed to be rendered
  1576. // return the title of the flash
  1577. if (!$title) {
  1578. // just show the sourcename
  1579. $title = PhpString::basename(noNS($src));
  1580. }
  1581. return $this->_xmlEntities($title);
  1582. }
  1583. $att = [];
  1584. $att['class'] = "media$align";
  1585. if ($align == 'right') $att['align'] = 'right';
  1586. if ($align == 'left') $att['align'] = 'left';
  1587. $ret .= html_flashobject(
  1588. ml($src, ['cache' => $cache], true, '&'),
  1589. $width,
  1590. $height,
  1591. ['quality' => 'high'],
  1592. null,
  1593. $att,
  1594. $this->_xmlEntities($title)
  1595. );
  1596. } elseif ($title) {
  1597. // well at least we have a title to display
  1598. $ret .= $this->_xmlEntities($title);
  1599. } else {
  1600. // just show the sourcename
  1601. $ret .= $this->_xmlEntities(PhpString::basename(noNS($src)));
  1602. }
  1603. return $ret;
  1604. }
  1605. /**
  1606. * Escape string for output
  1607. *
  1608. * @param $string
  1609. * @return string
  1610. */
  1611. public function _xmlEntities($string)
  1612. {
  1613. return hsc($string);
  1614. }
  1615. /**
  1616. * Construct a title and handle images in titles
  1617. *
  1618. * @param string|array $title either string title or media array
  1619. * @param string $default default title if nothing else is found
  1620. * @param bool $isImage will be set to true if it's a media file
  1621. * @param null|string $id linked page id (used to extract title from first heading)
  1622. * @param string $linktype content|navigation
  1623. * @return string HTML of the title, might be full image tag or just escaped text
  1624. * @author Harry Fuecks <hfuecks@gmail.com>
  1625. */
  1626. public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content')
  1627. {
  1628. $isImage = false;
  1629. if (is_array($title)) {
  1630. $isImage = true;
  1631. return $this->_imageTitle($title);
  1632. } elseif (is_null($title) || trim($title) == '') {
  1633. if (useHeading($linktype) && $id) {
  1634. $heading = p_get_first_heading($id);
  1635. if (!blank($heading)) {
  1636. return $this->_xmlEntities($heading);
  1637. }
  1638. }
  1639. return $this->_xmlEntities($default);
  1640. } else {
  1641. return $this->_xmlEntities($title);
  1642. }
  1643. }
  1644. /**
  1645. * Returns HTML code for images used in link titles
  1646. *
  1647. * @param array $img
  1648. * @return string HTML img tag or similar
  1649. * @author Andreas Gohr <andi@splitbrain.org>
  1650. */
  1651. public function _imageTitle($img)
  1652. {
  1653. global $ID;
  1654. // some fixes on $img['src']
  1655. // see internalmedia() and externalmedia()
  1656. [$img['src']] = explode('#', $img['src'], 2);
  1657. if ($img['type'] == 'internalmedia') {
  1658. $img['src'] = (new MediaResolver($ID))->resolveId($img['src'], $this->date_at, true);
  1659. }
  1660. return $this->_media(
  1661. $img['src'],
  1662. $img['title'],
  1663. $img['align'],
  1664. $img['width'],
  1665. $img['height'],
  1666. $img['cache']
  1667. );
  1668. }
  1669. /**
  1670. * helperfunction to return a basic link to a media
  1671. *
  1672. * used in internalmedia() and externalmedia()
  1673. *
  1674. * @param string $src media ID
  1675. * @param string $title descriptive text
  1676. * @param string $align left|center|right
  1677. * @param int $width width of media in pixel
  1678. * @param int $height height of media in pixel
  1679. * @param string $cache cache|recache|nocache
  1680. * @param bool $render should the media be embedded inline or just linked
  1681. * @return array associative array with link config
  1682. * @author Pierre Spring <pierre.spring@liip.ch>
  1683. */
  1684. public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render)
  1685. {
  1686. global $conf;
  1687. $link = [];
  1688. $link['class'] = 'media';
  1689. $link['style'] = '';
  1690. $link['pre'] = '';
  1691. $link['suf'] = '';
  1692. $link['more'] = '';
  1693. $link['target'] = $conf['target']['media'];
  1694. if ($conf['target']['media']) $link['rel'] = 'noopener';
  1695. $link['title'] = $this->_xmlEntities($src);
  1696. $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render);
  1697. return $link;
  1698. }
  1699. /**
  1700. * Embed video(s) in HTML
  1701. *
  1702. * @param string $src - ID of video to embed
  1703. * @param int $width - width of the video in pixels
  1704. * @param int $height - height of the video in pixels
  1705. * @param array $atts - additional attributes for the <video> tag
  1706. * @return string
  1707. * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
  1708. *
  1709. * @author Anika Henke <anika@selfthinker.org>
  1710. */
  1711. public function _video($src, $width, $height, $atts = null)
  1712. {
  1713. // prepare width and height
  1714. if (is_null($atts)) $atts = [];
  1715. $atts['width'] = (int)$width;
  1716. $atts['height'] = (int)$height;
  1717. if (!$atts['width']) $atts['width'] = 320;
  1718. if (!$atts['height']) $atts['height'] = 240;
  1719. $posterUrl = '';
  1720. $files = [];
  1721. $tracks = [];
  1722. $isExternal = media_isexternal($src);
  1723. if ($isExternal) {
  1724. // take direct source for external files
  1725. [/* ext */, $srcMime] = mimetype($src);
  1726. $files[$srcMime] = $src;
  1727. } else {
  1728. // prepare alternative formats
  1729. $extensions = ['webm', 'ogv', 'mp4'];
  1730. $files = media_alternativefiles($src, $extensions);
  1731. $poster = media_alternativefiles($src, ['jpg', 'png']);
  1732. $tracks = media_trackfiles($src);
  1733. if (!empty($poster)) {
  1734. $posterUrl = ml(reset($poster), '', true, '&');
  1735. }
  1736. }
  1737. $out = '';
  1738. // open video tag
  1739. $out .= '<video ' . buildAttributes($atts) . ' controls="controls"';
  1740. if ($posterUrl) $out .= ' poster="' . hsc($posterUrl) . '"';
  1741. $out .= '>' . NL;
  1742. $fallback = '';
  1743. // output source for each alternative video format
  1744. foreach ($files as $mime => $file) {
  1745. if ($isExternal) {
  1746. $url = $file;
  1747. $linkType = 'externalmedia';
  1748. } else {
  1749. $url = ml($file, '', true, '&');
  1750. $linkType = 'internalmedia';
  1751. }
  1752. $title = empty($atts['title'])
  1753. ? $this->_xmlEntities(PhpString::basename(noNS($file)))
  1754. : $atts['title'];
  1755. $out .= '<source src="' . hsc($url) . '" type="' . $mime . '" />' . NL;
  1756. // alternative content (just a link to the file)
  1757. $fallback .= $this->$linkType(
  1758. $file,
  1759. $title,
  1760. null,
  1761. null,
  1762. null,
  1763. $cache = null,
  1764. $linking = 'linkonly',
  1765. $return = true
  1766. );
  1767. }
  1768. // output each track if any
  1769. foreach ($tracks as $trackid => $info) {
  1770. [$kind, $srclang] = array_map('hsc', $info);
  1771. $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
  1772. $out .= "label=\"$srclang\" ";
  1773. $out .= 'src="' . ml($trackid, '', true) . '">' . NL;
  1774. }
  1775. // finish
  1776. $out .= $fallback;
  1777. $out .= '</video>' . NL;
  1778. return $out;
  1779. }
  1780. /**
  1781. * Embed audio in HTML
  1782. *
  1783. * @param string $src - ID of audio to embed
  1784. * @param array $atts - additional attributes for the <audio> tag
  1785. * @return string
  1786. * @author Anika Henke <anika@selfthinker.org>
  1787. *
  1788. */
  1789. public function _audio($src, $atts = [])
  1790. {
  1791. $files = [];
  1792. $isExternal = media_isexternal($src);
  1793. if ($isExternal) {
  1794. // take direct source for external files
  1795. [/* ext */, $srcMime] = mimetype($src);
  1796. $files[$srcMime] = $src;
  1797. } else {
  1798. // prepare alternative formats
  1799. $extensions = ['ogg', 'mp3', 'wav'];
  1800. $files = media_alternativefiles($src, $extensions);
  1801. }
  1802. $out = '';
  1803. // open audio tag
  1804. $out .= '<audio ' . buildAttributes($atts) . ' controls="controls">' . NL;
  1805. $fallback = '';
  1806. // output source for each alternative audio format
  1807. foreach ($files as $mime => $file) {
  1808. if ($isExternal) {
  1809. $url = $file;
  1810. $linkType = 'externalmedia';
  1811. } else {
  1812. $url = ml($file, '', true, '&');
  1813. $linkType = 'internalmedia';
  1814. }
  1815. $title = $atts['title'] ?: $this->_xmlEntities(PhpString::basename(noNS($file)));
  1816. $out .= '<source src="' . hsc($url) . '" type="' . $mime . '" />' . NL;
  1817. // alternative content (just a link to the file)
  1818. $fallback .= $this->$linkType(
  1819. $file,
  1820. $title,
  1821. null,
  1822. null,
  1823. null,
  1824. $cache = null,
  1825. $linking = 'linkonly',
  1826. $return = true
  1827. );
  1828. }
  1829. // finish
  1830. $out .= $fallback;
  1831. $out .= '</audio>' . NL;
  1832. return $out;
  1833. }
  1834. /**
  1835. * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
  1836. * which returns an existing media revision less or equal to rev or date_at
  1837. *
  1838. * @param string $media_id
  1839. * @access protected
  1840. * @return string revision ('' for current)
  1841. * @author lisps
  1842. */
  1843. protected function _getLastMediaRevisionAt($media_id)
  1844. {
  1845. if (!$this->date_at || media_isexternal($media_id)) return '';
  1846. $changelog = new MediaChangeLog($media_id);
  1847. return $changelog->getLastRevisionAt($this->date_at);
  1848. }
  1849. #endregion
  1850. }
  1851. //Setup VIM: ex: et ts=4 :