1<?php 2/** 3 * DokuWiki xslfo plugin: Export single pages to PDF via XSL-FO. 4 * 5 * @license GPL 3 (http://www.gnu.org/licenses/gpl.html) 6 * @author Sam Wilson <sam@samwilson.id.au> 7 */ 8 9/** 10 * Ensure that we're running within Dokuwiki. 11 */ 12if (!defined('DOKU_INC')) { 13 die(); 14} 15if (!defined('DOKU_PLUGIN')) { 16 define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 17} 18 19/** 20 * The main xslfo class. 21 */ 22class action_plugin_xslfo extends DokuWiki_Action_Plugin { 23 24 /** @var string Current XSL template */ 25 private $template; 26 27 /** @var string Full path to the current XSL file */ 28 private $template_path; 29 30 /** 31 * Register the events 32 */ 33 public function register(&$controller) { 34 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'preprocess', array()); 35 } 36 37 /** 38 * Do the HTML to PDF conversion work 39 * 40 * @param Doku_Event $event 41 * @param array $param 42 * @return bool 43 */ 44 public function preprocess(&$event, $param) { 45 global $ID, $REV, $ACT; 46 47 // Check that this is our action 48 if ($ACT != 'export_xslfo') { 49 return false; 50 } 51 $event->preventDefault(); 52 53 // Check for the XML plugin 54 if (!class_exists('renderer_plugin_xml')) { 55 msg('The XML plugin is required by the XSLFO plugin.', -1); 56 return false; 57 } 58 59 // Check user authorisation 60 if (auth_quickaclcheck($ID) < AUTH_READ) { 61 return false; 62 } 63 64 // Set up template and page title 65 $this->setupTemplate(); 66 $title = p_get_first_heading($ID); 67 68 // Prepare and check the cache 69 // (The same cache key is also used below for the XML file) 70 $cache_key = $ID.$REV.$this->template; 71 $pdf_cache = new cache($cache_key, '.pdf'); 72 $cache_dependencies['files'] = array( 73 __FILE__, 74 wikiFN($ID, $REV), 75 $this->template_path, 76 getConfigFiles('main'), 77 ); 78 if (!$this->getConf('usecache') || !$pdf_cache->useCache($cache_dependencies)) { 79 if (!$this->generatePDF($cache_key, $pdf_cache->cache)) { 80 return false; 81 } 82 } 83 84 $this->sendFile($pdf_cache->cache, $title); 85 } 86 87 /** 88 * Generate the PDF file. 89 * If $_GET['debug'] is set, the raw XML will be output. 90 * 91 * @global string $ID 92 * @global string $REV 93 * @param string $cache_key The key of the cache, for the XML file 94 * @param string $pdf_filename The full path to write the PDF to 95 * @return boolean True if the PDF was generated successfully 96 */ 97 protected function generatePDF($cache_key, $pdf_filename) { 98 global $ID, $REV; 99 100 // Replace placeholders in the command string 101 $filenames = array( 102 'xml' => $this->setupXML(), 103 'xsl' => $this->template_path, 104 'pdf' => $pdf_filename, 105 ); 106 107 // Display final XML for debugging purposes (for admins only) 108 if (isset($_GET['debug'])) { 109 $xml = htmlspecialchars(file_get_contents($filenames['xml'])); 110 msg("Final XML: <pre>$xml</pre>", 0, '', '', MSG_ADMINS_ONLY); 111 return false; 112 } 113 114 $command_template = $this->getConf('command').' 2>&1'; 115 $command = preg_replace_callback('/{(\w+)}/', function ($m) use ($filenames) { 116 return $filenames[$m[1]]; 117 }, $command_template); 118 119 // Execute the FO processor command, and give up if it fails 120 if (file_exists($pdf_filename)) { 121 unlink($pdf_filename); 122 } 123 exec($command, $out); 124 if (!file_exists($pdf_filename)) { 125 msg("Unable to produce PDF.", -1); 126 msg("Command: <code>$command</code><br />Output:<pre>".join("\n", $out).'</pre>', 0, '', '', MSG_ADMINS_ONLY); 127 return false; 128 } else { 129 return true; 130 } 131 } 132 133 /** 134 * Get the page XML, add some useful paths to it (in the 135 * <dokuwiki> element) and return the filename of the cached XML file. 136 * Doesn't check for an existing XML cache because at this point we always 137 * want to re-render. The image paths are added here, rather than in the XML 138 * plugin, to avoid data exposure (the end user won't ever see this XML). 139 * 140 * @global string $ID 141 * @global string $REV 142 * @global array $conf 143 * @return string Full filesystem path to the cached XML file 144 */ 145 protected function setupXML() { 146 global $ID, $REV, $INFO, $conf; 147 148 // Construct the new dokuwiki element 149 $dw_element = new SimpleXMLElement('<dokuwiki></dokuwiki>'); 150 $dw_element->addChild('tplincdir', strtr(tpl_incdir(), '\\', '/')); 151 $dw_element->addChild('mediadir', strtr($conf['mediadir'], '\\', '/')); 152 $dw_element->addChild('lastmod', dformat($INFO['lastmod'])); 153 $params = ($REV) ? array('rev'=> $REV) : null; 154 $dw_element->addChild('url', wl($ID, $params, true)); 155 156 // Get the basic page XML 157 $file = wikiFN($ID, $REV); 158 $instructions = p_get_instructions(io_readWikiPage($file, $ID, $REV)); 159 $original_xml = p_render('xml', $instructions, $info); 160 161 // Some plugins might break the XML rendering. 162 try { 163 // Produces an E_WARNING error message for each error found in the 164 // XML data and additionally throws an Exception if the XML data 165 // could not be parsed. 166 libxml_use_internal_errors(true); 167 $page = new SimpleXMLElement($original_xml); 168 } catch (Exception $e) { 169 msg($e->getMessage(), -1); 170 $admin_msg = ''; 171 foreach (libxml_get_errors() as $error) { 172 $admin_msg .= "$error->message (line $error->line column $error->column)<br />"; 173 } 174 $admin_msg .= "Unable to parse XML: <pre>".htmlspecialchars($original_xml)."</pre>"; 175 msg($admin_msg, 0, '', '', MSG_ADMINS_ONLY); 176 return false; 177 } 178 179 // Add image paths (for resized images) for use in the XSL 180 foreach ($page->xpath('//media') as $media) { 181 $src = mediaFN($media['src']); 182 $ext = current(mimetype($src, false)); 183 if($media['width'] && $media['height'] > 0) { 184 $filename = media_crop_image($src, $ext, (int)$media['width'], (int)$media['height']); 185 } else { 186 $filename = media_resize_image($src, $ext, (int)$media['width'], (int)$media['height']); 187 } 188 $media_filename = $dw_element->addChild('media_filename', $filename); 189 $media_filename->addAttribute('src', $media['src']); 190 $media_filename->addAttribute('width', $media['width']); 191 $media_filename->addAttribute('height', $media['height']); 192 } 193 194 // Insert the new XML into the page's XML 195 $new_xml = str_replace('<?xml version="1.0"?>', '', $dw_element->asXML()); 196 $xml = str_replace('</document>', $new_xml.'</document>', $original_xml); 197 198 // Cache the XML (for use by the XSLFO processor, not subsequent calls 199 // to this method) and return its full filesystem path. 200 $xml_cache = new cache($ID.$REV.'_xslfo', '.xml'); 201 $xml_cache->storeCache($xml); 202 return $xml_cache->cache; 203 } 204 205 /** 206 * Get the full filesystem path to the current XSL in the current site 207 * template's xslfo directory. 208 * 209 * @uses $_REQUEST['tpl'] 210 * @return string The full path to the XSL file 211 */ 212 protected function setupTemplate() { 213 if (!empty($_REQUEST['tpl'])) { 214 $this->template = $_REQUEST['tpl']; 215 } else { 216 $this->template = $this->getConf('template'); 217 } 218 $this->template_path = realpath(tpl_incdir().$this->template); 219 // Might resolve to a directory, so check it's a file. 220 if (!is_file($this->template_path)) { 221 $this->template = 'default.xsl'; 222 $this->template_path = __DIR__.DIRECTORY_SEPARATOR.$this->template; 223 } 224 } 225 226 /** 227 * Send the PDF file to the user. 228 * 229 * @param string $file Full filesystem path to the cached PDF 230 * @param string $title The title of the document, to be turned into a filename 231 */ 232 public function sendFile($file, $title) { 233 234 // Start sending HTTP headers 235 header('Content-Type: application/pdf'); 236 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 237 header('Pragma: public'); 238 http_conditionalRequest(filemtime($file)); 239 240 // Construct a nice filename from the title 241 $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); 242 if ($this->getConf('output') == 'file') { 243 header('Content-Disposition: attachment; filename="'.$filename.'.pdf";'); 244 } else { 245 header('Content-Disposition: inline; filename="'.$filename.'.pdf";'); 246 } 247 248 // Use sendfile if possible 249 if (http_sendfile($file)) { 250 exit(0); 251 } 252 253 // Send file or fail with error 254 $fp = @fopen($file, "rb"); 255 if ($fp) { 256 http_rangeRequest($fp, filesize($file), 'application/pdf'); 257 exit(0); 258 } else { 259 header("HTTP/1.0 500 Internal Server Error"); 260 print "Could not read file - bad permissions?"; 261 exit(1); 262 } 263 } 264 265} 266