[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/parser/ -> xhtml.php (source)

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