1<?php
2/**
3 * texit multifunction Class
4 * Copyright (C) 2013   Elie Roux <elie.roux@telecom-bretagne.eu>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19 * --------------------------------------------------------------------
20 *
21 */
22if(!defined('DOKU_INC')) die();
23if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
24if(!defined('PLUGIN_TEXIT')) define('PLUGIN_TEXIT',DOKU_PLUGIN.'texit/');
25if(!defined('PLUGIN_TEXIT_CONF')) define('PLUGIN_TEXIT_CONF',PLUGIN_TEXIT.'conf/');
26require_once(PLUGIN_TEXIT.'texitrender.php');
27
28class config_plugin_texit {
29  var $id;
30  var $ns;
31  var $namespace_mode;
32  var $nsbpc;
33  var $conf;
34  var $mediadir;
35  var $texitdir;
36  var $prfix;
37  var $all_files;
38  var $texit_render_obj; // not initialized by constructor, done only if needed
39  var $bibfn;
40 /*
41  * I didn't use a helper plugin because I needed a constructor.
42  * This basically sets up the environment by computing base the filenames, etc.
43  *
44  */
45  function __construct($id, $namespace_mode, $conf, $nsbpc_obj) {
46    $this->id = cleanID($id);
47    $this->ns = getNS(cleanID($id));
48    $this->namespace_mode = $namespace_mode;
49    $this->nsbpc = $nsbpc_obj;
50    $this->conf = $conf;
51    $this->set_prefix();
52    $this->_set_texit_dir();
53    $this->_set_media_dir();
54    $this->bibfn = $this->generate_bib();
55    $this->get_all_files();
56    $this->conf['latexentities'] = false; // we generate it at compile time
57    $this->texit_render_obj = false;
58  }
59 /*
60  * This function sets $this->latexentities to an array where keys are
61  * the initial characters and values are the characters escaped in
62  * LaTeX (ex: _ => \_). It gets in by calling conftohash on conf/entities.cfg
63  * in the plugin's directory.
64  */
65  function get_entities() {
66    $basefn = PLUGIN_TEXIT_CONF.'entities.cfg';
67    return $this->confToHash($basefn);
68  }
69
70  /**
71  * Builds a hash from a configfile
72  *
73  * If $lower is set to true all hash keys are converted to
74  * lower case.
75  *
76  * This is a modified version of Dokuwiki's function
77  * that doesn't consider # as a comment character (we
78  * need it for LaTeX entities).
79  */
80  function confToHash($file) {
81    $conf = array();
82    $lines = @file( $file );
83    if ( !$lines ) return false;
84    foreach ( $lines as $line ) {
85      $line = trim($line);
86      if(empty($line)) continue;
87      $line = preg_split('/[\s\t]+/',$line,2);
88      // Build the associative array
89      $conf[$line[0]] = $line[1];
90    }
91    return $conf;
92  }
93
94 /*
95  * This function (eventually) generates the file texit.bib, in the directory
96  * of the namespace pointed by the "reference-db-enable" configuration option
97  * of the refnotes plugin.
98  *
99  * To do so, it merges:
100  *   - the BibTeX parts of all pages in the refnotes's database namespace
101  *       ("refnotes" by default)
102  *   - the conf/bibliography.bib in the texit plugin directory
103  */
104  function generate_bib() {
105    global $conf;
106    $bibtext = ''; // we merge all the files in this string
107    $basefn = PLUGIN_TEXIT_CONF.'bibliography.bib';
108    if (!is_callable("refnotes_configuration::getSetting")) {
109      // case where refnotes isn't available. In this case the
110      // file to include is just $basefn.
111      return $basefn;
112    }
113    // code coming from refnotes' syntax.php
114    $refnotes_nsdir = refnotes_configuration::getSetting('reference-db-namespace');
115    $refnotes_nsdir = str_replace(':', '/', $refnotes_nsdir);
116    $refnotes_nsdir = trim($refnotes_nsdir, '/ ');
117    $destfn = $conf['datadir'].'/'.$refnotes_nsdir.'/texit.bib';
118    $all_refnotes_pages = Array();
119    $opts = array('listdirs'  => false,
120      'listfiles' => true,
121      'pagesonly' => true,
122      'skipacl'   => false, // to check for read right
123      'sneakyacl' => true,
124      'showhidden'=> false,
125      );
126    // we cannot use $opts in search_list or in search_namespaces, see
127    // https://bugs.dokuwiki.org/index.php?do=details&task_id=2858
128    search($all_refnotes_pages,$conf['datadir'],'search_universal',$opts,$refnotes_nsdir);
129    // now all_refnotes_pages contains all the configuration pages of refnotes, that
130    // we'll have to merge...
131    // First step here is to see if we need to recompile anything:
132    if (is_readable($destfn)) {
133      // if the file is readable, then it might be up-to-date?
134      $needsupdate = false;
135      if (is_readable($basefn)) {
136        $needsupdate = $this->_needs_update($basefn, $destfn);
137      }
138      foreach ($all_refnotes_pages as $page) {
139        // A problem here: if the refnote page doesn't contain
140        // any bibtex code, the update will take place anyway,
141        // but it doesn't sound critical.
142        if ($this->_needs_update(wikiFN($page['id']), $destfn)) {
143          $needsupdate = true;
144        }
145      }
146      // if the file doesn't need update, we just return.
147      if (!$needsupdate) {
148        return $destfn;
149      }
150    }
151    if (is_readable($basefn)) {
152      $bibtext = file_get_contents($basefn);
153    }
154    foreach($all_refnotes_pages as $page) {
155      $fn = wikiFN($page['id']);
156      $bibtext .= $this->parse_refnotes_page($fn);
157    }
158    if (empty($bibtext)) {
159      return false;
160    }
161    file_put_contents($destfn, $bibtext);
162    // we return the filename where the bibliography is saved
163    return $destfn;
164  }
165
166  function parse_refnotes_page ($fn) {
167    $filestr = file_get_contents($fn);
168    preg_match_all('#(?<=<code bibtex>)(((?!</code>).)*)(?=</code>)#ms', $filestr, $matches);
169    $return = '';
170    foreach($matches[0] as $match) {
171      $return .= $match."\n";
172    }
173    return $return;
174  }
175
176  function set_prefix() {
177    if (!$this->conf['use_prefix']) {
178      $this->prefix = '';
179      return;
180    } else {
181      if (!empty($this->conf['pre_prefix'])) {
182        $this->prefix = $this->conf['pre_prefix'].":";
183      }
184      $this->prefix .= $this->ns;
185      if ($this->conf['prefix_separator']) {
186        $this->prefix = str_replace(':', $this->conf['prefix_separator'], $this->prefix);
187        $this->prefix .= $this->conf['prefix_separator'];
188      } // else we keep it this way
189    }
190  }
191
192  function _create_dir($path) {
193    global $conf;
194    $res = init_path($path);
195    if(empty($res)) {
196      // let's create it, recursively
197      $res = io_mkdir_p($path);
198      //$res = mkdir($path, $conf['dmode'], true);
199      if(!$res){
200        die("Unable to create directory $path, please create it.");
201      }
202    }
203  }
204
205// This function escapes a filename so that it doesn't contain _ character:
206  function _escape_fn($fn) {
207    $bn = basename($fn);
208    $bn = str_replace('_', '-', $bn);
209    $dn = dirname($fn);
210    if ($dn == ".") {
211      return $bn;
212    }
213    return dirname($fn).'/'.$bn;
214  }
215
216  function _set_media_dir() {
217    global $conf;
218    $path = $conf['mediadir'];
219    $path .= '/'.str_replace(':','/',$this->ns);
220    // taken from init_paths in inc/init.php
221    $this->_create_dir($path);
222    $this->mediadir = $path;
223  }
224
225  function _set_texit_dir() {
226    global $conf;
227    $path = $this->conf['texitdir'];
228    // taken from init_paths in inc/init.php
229    $path = empty($path) ? $conf['datadir'].'/../texit' : $path;
230    $path .= '/'.str_replace(':','/',$this->ns);
231    $this->_create_dir($path);
232    $path = realpath($path);
233    $this->texitdir = $path;
234  }
235
236  function get_zip_fn() {
237    return $this->mediadir.'/'.$this->get_common_basename().".zip";
238  }
239
240  function get_base_bib_fn() {
241    return $this->bibfn;
242  }
243
244  function get_dest_bib_fn() {
245    // we always call it texit.bib for practical reasons, this may
246    // change in the future
247    return $this->texitdir.'/'.'texit.bib';
248  }
249
250  function get_pdf_media_fn() {
251    return $this->mediadir.'/'.$this->prefix.$this->get_common_basename().".pdf";
252  }
253
254  function get_pdf_media_id() {
255    return $this->ns.':'.$this->prefix.$this->get_common_basename().".pdf";
256  }
257
258  function get_pdf_texit_fn() {
259    return $this->texitdir.'/'.$this->get_common_basename().".pdf";
260  }
261
262
263
264 /* This returns 'all' if in namespace-mode, or the escaped ID, without extension.
265  *
266  */
267  function get_common_basename() {
268    if ($this->namespace_mode) {
269      return "all";
270    } else {
271      return $this->_escape_fn(noNS($this->id));
272    }
273  }
274
275  /* This returns the full path of the base header file we take as reference
276   * for this compilation. In case nothing is found, false is returned.
277   */
278  function get_base_header_fn() {
279    // first we look for nsbpc headers
280    // the names are 'texit-namespace' or 'texit-page'
281    $header_name = "texit-page";
282    if ($this->namespace_mode) {
283      $header_name = "texit-namespace";
284    }
285    $found = $this->nsbpc->getConfFN($header_name, $this->ns);
286    if ($found) {
287      return $found;
288    }
289    // No nsbpc configuration was found, now looking in the conf/ directory of
290    // the plugin. Names are different here...
291    $header_name = "header-page.tex";
292    if ($this->namespace_mode) {
293      $header_name = "header-namespace.tex";
294    }
295    if (is_readable(PLUGIN_TEXIT_CONF.$header_name)) {
296      return PLUGIN_TEXIT_CONF.$header_name;
297    }
298    return false;
299  }
300
301  /* This returns the full path of the header file we want in the destination
302   * texit namespace.
303   */
304  function get_dest_header_fn() {
305    if ($this->namespace_mode) {
306      return $this->texitdir."/all.tex";
307    } else {
308      return $this->texitdir.'/'.$this->get_common_basename().".tex";
309    }
310  }
311  /* This returns the full path of the base footer file we take as reference
312   * for this compilation, or false if there is no such file.
313   */
314  function get_base_footer_fn() {
315    // first we look through nsbpc
316    $found = $this->nsbpc->getConfFN("texit-footer", $this->ns);
317    if ($found) {
318      return $found;
319    }
320    // No nsbpc configuration was found, now looking in the conf/ directory of
321    // the plugin.
322    if (is_readable(PLUGIN_TEXIT_CONF."footer.tex")) {
323      return PLUGIN_TEXIT_CONF."footer.tex";
324    }
325    return false;
326  }
327  /* This returns the full path of the commands file we want in the destination
328   * texit namespace.
329   */
330  function get_dest_footer_fn() {
331    return $this->texitdir."/footer.tex";
332  }
333  /* This returns the full path of the base coommands file we take as reference
334   * for this compilation.
335   */
336  function get_base_commands_fn() {
337    // first we look through nsbpc
338    $found = $this->nsbpc->getConfFN("texit-commands", $this->ns);
339    if ($found) {
340      return $found;
341    }
342    // No nsbpc configuration was found, now looking in the conf/ directory of
343    // the plugin.
344    if (is_readable(PLUGIN_TEXIT_CONF."commands.tex")) {
345      return PLUGIN_TEXIT_CONF."commands.tex";
346    }
347    return false;
348  }
349  /* This returns the full path of the commands file we want in the destination
350   * texit namespace.
351   */
352  function get_dest_commands_fn() {
353    return $this->texitdir."/commands.tex";
354  }
355
356 /* This function returns an array of all IDs of pages to be rendered by TeXit.
357  *
358  */
359  function get_all_IDs() {
360    global $conf;
361    if ($this->namespace_mode) {
362      $list = array();
363      $nsdir = str_replace(':', '/', $this->ns);
364      $opts = array('listdirs'  => false,
365                    'listfiles' => true,
366                    'pagesonly' => true,
367                    'depth'     => 1,
368                    'skipacl'   => false, // to check for read right
369                    'sneakyacl' => true,
370                    'showhidden'=> false,
371                    );
372	  if ($this->conf['includestart'] == false) {
373	    $opts['idmatch'] = "^((?!start$).)+$";
374      }
375      search($list,$conf['datadir'],'search_universal',$opts,$nsdir);
376      return $list;
377    } else {
378      return array(array('id' => $this->id));
379    }
380  }
381
382 /* Returns an array with base and destination filenames. Works with full paths.
383  *
384  * The returned array has the following structure:
385  *    [base] => (type, fn)
386  * where:
387  *  * base is the base filename (like /path/to/dkwiki/pages/ns/id.txt)
388  *  * type is either "header", "commands", "tex" or "bib".
389  *  * fn is the absolute destination filename (prefix included)
390  */
391  function get_all_files() {
392   // this gives us all the page ids that need txt->tex conversion:
393   $id_array = $this->get_all_IDs();
394   $result = array();
395   // now we put them all in the $result array
396   foreach($id_array as $value) {
397     if (!is_array($value) || !$value['id']) { // I did'nt find any more elegant way to do so
398       continue;
399     }
400     $fn = wikiFN($value['id']);
401     $dest = $this->texitdir.'/'.noNS($value['id'])."-content.tex";
402     $dest = $this->_escape_fn($dest);
403     $result[$fn] = array('type' => 'tex', 'fn' => $dest);
404   }
405   // and we add the header and command
406   $base = $this->get_base_header_fn();
407   if (!$base) {
408     nice_die("TeXit: Unable to find a header file!");
409   }
410   $result[$base] = array('type' => 'header', 'fn' => $this->get_dest_header_fn());
411   $base = $this->get_base_commands_fn();
412   if (!$base) {
413     nice_die("TeXit: Unable to find a commands file!");
414   }
415   $result[$base] = array('type' => 'commands', 'fn' => $this->get_dest_commands_fn());
416   $bib = $this->get_base_bib_fn();
417   if ($bib) { // not mandatory
418     $result[$bib] = array('type' => 'bib', 'fn' => $this->get_dest_bib_fn());
419   }
420   $footer = $this->get_base_footer_fn();
421   if ($footer) { // not mandatory
422     $result[$footer] = array('type' => 'footer', 'fn' => $this->get_dest_footer_fn());
423   }
424   $this->all_files = $result;
425  }
426
427 /* This function takes three arguments:
428  *    * base is the full path of the base header file
429  *           (for instance /path/to/dkwiki/lib/plugin/texit/conf/header-page.tex)
430  *    * dest is the full path of the destination header file
431  *    * all_files is the table returned by get_all_files()
432  *
433  * It reads $base, adds \input lines for $all_files and writes the result in
434  * $dest.
435  */
436  function compile_header($base, $dest, $all_files) {
437    // first we simply copy the file
438    $this->simple_copy($base, $dest);
439    // we prepare a string to append at the end:
440    $toappend = "\n";
441    // we spot the last value:
442    $beginning = 1;
443    $footer = false;
444    foreach($this->all_files as $value) {
445      switch($value['type']) {
446        case 'tex':
447          // between two different files, we call the \dokuinternspagedo
448          // macro, doing nothing by default.
449          if (!$beginning) {
450            $toappend .= "\\dokuinternspagedo\n\n";
451          }
452          $toappend .= '\dokuinclude{'.basename($value['fn'], '.tex')."}\n\n";
453          break;
454        case 'footer':
455          $footer = basename($value['fn'], '.tex');
456        default:
457          break;
458      }
459      $beginning = 0;
460    }
461    if ($footer) {
462      $toappend .= "\dokuinclude{".$footer."}\n";
463    }
464    $toappend .= "\n\\end{document}";
465    // the we open it in append mode to write things at the end:
466    file_put_contents($dest, $toappend, FILE_APPEND);
467  }
468
469 /* This function takes two arguments:
470  *    * base is the full path of the base page file
471  *           (for instance /path/to/dkwiki/data/pages/ns/id.txt)
472  *    * dest is the full path of the destination tex file
473  *
474  * It reads $base, renders it into TeX and writes $dest.
475  */
476  function compile_tex($base, $dest) {
477    if (!$this->conf['latexentities'])
478      {
479        $this->conf['latexentities'] = $this->get_entities();
480      }
481    if (!$this->texit_render_obj)
482      {
483        $this->texit_render_obj = new texitrender_plugin_texit($this);
484      }
485    $this->texit_render_obj->process($base, $dest);
486  }
487
488 /* This function takes two arguments:
489  *    * base is the full path of the base file
490  *    * dest is the full path of the destination tex file
491  *
492  * It copies $base into $dest.
493  */
494  function simple_copy($base, $dest) {
495    if (!copy($base, $dest)) {
496      nice_die("TeXit: unable to copy $base into $dest.");
497    }
498  }
499
500 /*
501  * This functions returns true if $base is more recent that $dest, and
502  * false otherwise. If $dest doesn't exist, then we consider it needs
503  * update and thus return true.
504  */
505  function _needs_update($base, $dest) {
506    if (!file_exists($dest) || !file_exists($dest)) {
507        return true;
508      }
509    return filemtime($base) > filemtime($dest);
510  }
511
512 /* This function sets the TeX compilation environment up by copying the files
513  * in the good folders and renames them. It uses file modification timestamps
514  * to evaluate if files need to be recompiled or recopied.
515  *
516  * The returned value is a boolean: true if something has been updated, and
517  * false otherwise.
518  */
519  function setup_files() {
520    if (!is_array($this->all_files)) {
521        $this->get_all_files();
522      }
523    if (!is_array($this->all_files)) {
524        die("TeXit: cannot analyze files");
525      }
526    $needsupdate = false;
527    foreach($this->all_files as $base => $dest) {
528      $destfn = $dest['fn'];
529      if ($this->_needs_update($base, $destfn)) {
530        $needsupdate = true;
531        switch($dest['type']) {
532          case "header":
533            $this->compile_header($base, $destfn, $this->all_files);
534            break;
535          case "commands":
536            $this->simple_copy($base, $destfn);
537            break;
538          case "bib":
539            $this->simple_copy($base, $destfn);
540            break;
541          case "footer":
542            $this->simple_copy($base, $destfn);
543            break;
544          case "tex":
545            $this->compile_tex($base, $destfn);
546            break;
547          default:
548            break;
549        }
550      }
551    }
552    return $needsupdate;
553  }
554
555 /* This function calls latexmk with the good options on the good files.
556  */
557  function _do_latexmk() {
558    if (!is_dir($this->texitdir)) {
559      die("TeXit: directory $this->texitdir doesn't exit");
560    }
561    chdir($this->texitdir);
562    $basecmdline = '';
563    if (isset($this->conf['latexmk_path'])
564      && trim($this->conf['latexmk_path']) != "") {
565      $basecmdline = $this->conf['latexmk_path'] . DIRECTORY_SEPARATOR;
566    } else {
567      $basecmdline = '';
568    }
569    $cmdline = $basecmdline."latexmk -f ";
570    if ($this->bibfn) {
571      $cmdline .= "-bibtex ";
572    }
573    switch ($this->conf['latex_mode'])
574    {
575      case "latex":
576        // TODO: test, comes from http://users.phys.psu.edu/~collins/software/latexmk-jcc/
577        $cmdline .= "-e '\$dvipdf = \"dvipdfm %O -o %D %S\";' -pdfdvi ";
578        break;
579      case "pdflatex":
580        $cmdline .= "-pdf ";
581        break;
582      case "lualatex":
583        $cmdline .= "-pdf -pdflatex=lualatex ";
584        break;
585      case "xelatex":
586        $cmdline .= "-latex=xelatex -e '\$dvipdf = \"dvipdfmx %O -o %D %S\";' -pdfdvi ";
587        break;
588      default:
589        // error
590        break;
591    }
592    $file = basename($this->get_dest_header_fn());
593    $cmdline .= $file . ' 2>&1 ';
594    $ret = 0;
595    @exec($cmdline, $output, $ret);
596    if ($ret) {
597      print("<br/>TeXit error: latexmk returned error code ".$ret."<br/>\n<br/>Log:<br/>\n");
598      print_r(implode("<br/>\n", $output));
599    }
600    // at the end, we clean temporary files. There is currently no way to tell
601    // latexmk to clean at the end of the compilation... quite a shame...
602    // An email has been written to the author in this sense.
603    $cmdline = $basecmdline."latexmk -c 2>&1";
604    $log = @exec($cmdline, $output, $ret);
605    if ($ret) {
606      print("<br/>TeXit error: latexmk -c returned error code ".$ret."<br/>\n<br/>Log:<br/>\n");
607      print_r(implode("<br/>\n", $output));
608    }
609  }
610
611 /* This function zips the good files in the texit namespace in a .zip archive
612  * in the media namespace.
613  */
614  function compile_zip() {
615    $zipfn = $this->get_zip_fn();
616    // if the file already exists and needs update, remove it.
617    if (@file_exists($zipfn)) {
618      unlink($zipfn);
619    }
620    $zip = new ZipArchive();
621    if ($zip->open($zipfn, ZipArchive::CREATE) !== true) {
622      exit("Unable to create $zipfn\n");
623    }
624    // First argument of addFile is the absolute, second is the name we want
625    // in the archive (in our case, the basename).
626    $zip->addFile($this->get_pdf_texit_fn(), basename($this->get_pdf_texit_fn()));
627    foreach($this->all_files as $base => $dest) {
628      $zip->addFile($dest['fn'], basename($dest['fn']));
629    }
630    $zip->close();
631  }
632
633 /* My mind is too used to C programming and thus this is a bit too
634  * iterative and not object-oriented enough...
635  *
636  * This function processes everything when the user asks for a PDF.
637  */
638  function process() {
639    $needsupdate = $this->setup_files();
640    $pdftexitfn  = $this->get_pdf_texit_fn();
641    $pdfmediafn  = $this->get_pdf_media_fn();
642    $pdfmediaid  = $this->get_pdf_media_id();
643    $zipfn       = $this->get_zip_fn();
644    if ($needsupdate || !@file_exists($pdftexitfn)) {
645      $this->_do_latexmk();
646    }
647    // then copy the pdf to media
648    if ($needsupdate || !@file_exists($pdfmediafn)) {
649      $this->simple_copy($pdftexitfn, $pdfmediafn);
650    }
651    if ($this->conf['use_zip'] && ($needsupdate || !@file_exists($zipfn))) {
652        $this->compile_zip();
653    }
654    return $this->id_to_url($pdfmediaid);
655  }
656
657 /* This returns an absolute URL from a media ID.
658  */
659  function id_to_url($pdfmediaid) {
660    // internal dokuwiki function, defined in inc/common.php
661    return ml($pdfmediaid, '', true, '&amp;', true);
662  }
663}
664
665?>
666