1<?php 2/** 3 * Display File Dokuwiki Plugin (Syntax Component) 4 * 5 * Description: The Display File Plugin displays the content of a specified file on the local system using a displayfile element 6 * 7 * Syntax: <displayfile lang target /> 8 * lang (Required): The language of the content file. This is used by Dokuwiki's built-in syntax highlighting GeSHi 9 * library. To disable syntax highlighting, specify a dask (-) character for the lang value. The 10 * supported lang values are the same as those provided by Dokuwiki's <code> and <file> markup and 11 * can be found on the Dokuwiki syntax page: https://www.dokuwiki.org/wiki:syntax#syntax_highlighting 12 * target (Required): The last part of a file path to the desired file on the local file system. This will be appended to 13 * the value of the plugin's root_path configuration option. The target value can be enclosed in single 14 * or double quotes (' or "). The target path part must be enclosed in quotes if it contains spaces. 15 * 16 * @license The MIT License (https://opensource.org/licenses/MIT) 17 * @author Jay Jeckel <jeckelmail@gmail.com> 18 * 19 * Copyright (c) 2016 Jay Jeckel 20 * Licensed under the MIT license: https://opensource.org/licenses/MIT 21 * Permission is granted to use, copy, modify, and distribute the work. 22 * Full license information available in the project LICENSE file. 23 */ 24 25if(!defined('DOKU_INC')) { die(); } 26 27class syntax_plugin_displayfile extends DokuWiki_Syntax_Plugin 28{ 29 const PATTERN = '<displayfile\s+[a-z0-9_\-]+?\s*.*?\s*\/>'; 30 31 function getInfo() { return confToHash(dirname(__FILE__) . '/plugin.info.txt'); } 32 33 function getType() { return 'substition'; } 34 35 function getPType() { return 'block'; } 36 37 function getSort() { return 5; }// Must come before the <code>/<file> patterns are handled at 200/210. 38 39 function connectTo($mode) 40 { 41 $this->Lexer->addSpecialPattern(self::PATTERN, $mode, 'plugin_displayfile'); 42 $pattern = '<<' . 'display' . '\s' . 'file' . '\s' . '[a-z0-9_\-]+?' . '\s*' . '.*?' . '>>'; 43 $this->Lexer->addSpecialPattern($pattern, $mode, 'plugin_displayfile'); 44 } 45 46 function handle($match, $state, $pos, Doku_Handler $handler) 47 { 48 $match = $match[1] == '<' ? substr($match, 15, -2) : substr($match, 13, -3); 49 list($language, $target) = explode(' ', $match, 2); 50 51 $target = trim($target, " \t\n\r\0\x0B\"'"); 52 $target = str_replace("\\", '/', $target); 53 $target = ltrim($target, '/'); 54 55 $target_title = false; 56 $title = ($target_title ? $target : basename($target)); 57 58 $text = 'CONTENT NOT SET'; 59 $error = $this->_validate($target, $title, $text); 60 return $error === false ? array($text, $language, $title) : array("ERROR: {$error}", null, null); 61 //$output = $error === false ? "$language $title>$text" : "->ERROR: $error"; 62 //$handler->file($output, DOKU_LEXER_UNMATCHED, $pos); 63 //return null; 64 } 65 66 function render($format, Doku_Renderer $renderer, $data) 67 { 68 if ($format == 'xhtml') 69 { 70 list($text, $language, $title) = $data; 71 if (!$this->_isPhpFixRequired($text, $language)) 72 { $renderer->file($text, $language, $title); } 73 else 74 { 75 $this->_displayHeader($renderer, $title); 76 $this->_displayBody($renderer, $text, $language); 77 $renderer->doc .= '</dd></dl>'.DOKU_LF; 78 } 79 return true; 80 } 81 return false; 82 } 83 84 // Returns error message or false; if false, then $content holds the file contents. 85 function _validate($target, $title, &$content) 86 { 87 $root = $this->getConf('root_path'); 88 $root = str_replace("\\", '/', $root); 89 $root = rtrim($root, '/'); 90 91 // First, validate that the root path is set to something. 92 if (!isset($root) || $root === '') 93 { return $this->getLang('error_root_empty'); } 94 95 $real_root = realpath($root); 96 97 // Second, check that the root path exists and is a directory. 98 if ($real_root === false || !is_dir($real_root)) 99 { return $this->getLang('error_root_invalid'); } 100 101 // We now know that root exists. 102 103 // Third, defend against simple traversal attacks in the target. 104 else if (strpos($target, '../') !== false) 105 { return sprintf($this->getLang('error_traversal_token'), $title); } 106 107 $ext = pathinfo($target , PATHINFO_EXTENSION); 108 109 // Fourth, validate deny of extension. 110 $deny_extensions = explode(' ', $this->getConf('deny_extensions'));//array('sh'); 111 if (count($deny_extensions) > 0 && in_array($ext, $deny_extensions)) 112 { return sprintf($this->getLang('error_extension_deny'), $ext); } 113 114 // Fifth, validate deny of extension. 115 $allow_extensions = explode(' ', $this->getConf('allow_extensions'));//array('txt', 'php', 'js', 'css'); 116 if (count($allow_extensions) > 0 && !in_array($ext, $allow_extensions)) 117 { return sprintf($this->getLang('error_extension_allow'), $ext); } 118 119 $real_path = realpath($real_root . "/" . $target); 120 121 // Sixth, catch the file trying to read itself. 122 // Behavior undefined if this file doesn't exist. 123 if (realpath(__FILE__) == $real_path) 124 { return sprintf($this->getLang('error_self'), $title); } 125 126 // !!ATTENTION!! Security must be considered in the following sections. 127 // 128 // The root path is assumed safe as it is provided by the admin, 129 // but the target path stub is user-provided input and must be 130 // treated as dangerous by default. Specifically, path traversal 131 // attacks need to be mitigated. 132 // 133 // The main threat is use of path traversal shortcuts, such as the 134 // '.', '..', and '~' tokens, to cause the resolved path to point at 135 // files and directories outside the expected root path. Thankfully, 136 // it is fairly easy to detect these attacks by normalizing and expanding 137 // both the root and the path using realpath(). If the real path doesn't 138 // literally begin with the real root, then you know you have a traversal. 139 // Good, then that's done, right? Mostly, but not quite. 140 // 141 // The quirk is that realpath() only works for files that exist. If the 142 // file doesn't exist, then you don't have a real path to compare to the 143 // real root. Where the issue appears is when the developer follows their 144 // natural instinct and tries to help the user by providing more useful 145 // error messages; first check if the file doesn't exist, say as much; then 146 // when we know the file exists, another message to say it is invalid if it 147 // traverses outside the root. 148 // 149 // And there it is, you've opened a hole into the underlying system. What 150 // we've done is give an attacker the ability to check if any arbitrary File 151 // does or doesn't exist on whatever system is running the plugin. I know 152 // that doesn't seem like much, but by checking the existence of files an 153 // attacker can determine what OS the system is running, down to the specific 154 // version of the OS, and that could inform more tailored attacks. 155 // 156 // Fortunately, there is a simple fix for this: deny the urge to be helpful. 157 // Don't provide separate messages for the files that don't exist and files 158 // that exist but have traversed outside the root path. This way the only 159 // thing an attacker knows is that they can't access a file, nothing more, and 160 // that is all they need to know. For good measure, all invalid paths should 161 // return this same message to avoid any kind of proping of file information 162 // through error messages. 163 164 // Seventh, ensure the path exists. 165 else if ($real_path === false) { return sprintf($this->getLang('error_access'), $title); } 166 167 // We know that root and path exist. 168 169 // Eighth, the final wall against path traversal outside the root. 170 // Simple begins-with comparison, it's simple but effective. 171 else if (strpos($real_path, $real_root) !== 0) { return sprintf($this->getLang('error_access'), $title); } 172 173 // Ninth, ensure the file is a file and readable, and do any other good-measure checks. 174 else if (is_dir($real_path)) { return sprintf($this->getLang('error_access'), $title); } 175 else if (!is_readable($real_path)) { return sprintf($this->getLang('error_access'), $title); } 176 177 // Tenth, attempt to read the contents and return error if failed. 178 $result = file_get_contents($real_path); 179 if ($result === false) 180 { 181 // This represents an actual error outside our control. Since the user should 182 // have access to this content, it's ok to have a distinct error message. 183 //return "The file '$title' could not be read."; 184 return sprintf($this->getLang('error_read'), $title); 185 } 186 187 // Finally, set the out variable to the result of the read and return false. 188 $content = $result; 189 return false; 190 } 191 192 // There is a bug in GeShi, the syntax highlighting engine underlying Dokuwiki. 193 // If it is passed content that starts with a < character, is told to highlight as 194 // php, and contains a multiline comment of more than 610 characters, then the page 195 // will crash. If any one of those isn't met, then the bug isn't raised. 196 function _isPhpFixCandidate($text, $language) 197 { return $text[0] == '<' && ($language == 'php' || $language == 'php-brief'); } 198 199 function _isPhpFixRequired($text, $language) 200 { 201 if ($this->_isPhpFixCandidate($text, $language)) 202 { 203 $result = preg_match_all('/\/\*.{610,}?\*\//s', $text); 204 return $result === false || $result > 0; 205 } 206 return false; 207 } 208 209 function _getCodeBlockCount(Doku_Renderer $renderer) 210 { 211 $reflection = new ReflectionClass($renderer); 212 $property = $reflection->getProperty('_codeblock'); 213 $property->setAccessible(true); 214 return $property->getValue($renderer); 215 } 216 217 function _incCodeBlockCount(Doku_Renderer $renderer) 218 { 219 $reflection = new ReflectionClass($renderer); 220 $property = $reflection->getProperty('_codeblock'); 221 $property->setAccessible(true); 222 $codeblock = $property->getValue($renderer); 223 $property->setValue($renderer, $codeblock + 1); 224 return $property->getValue($renderer); 225 } 226 227 function _displayHeader(Doku_Renderer $renderer, $title) 228 { 229 global $ID; 230 global $lang; 231 global $INPUT; 232 233 list($ext) = mimetype($title, false); 234 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 235 $class = 'mediafile mf_' . $class; 236 237 $offset = 0; 238 if ($INPUT->has('codeblockOffset')) 239 { $offset = $INPUT->str('codeblockOffset'); } 240 241 $renderer->doc .= '<dl class="file">'.DOKU_LF; 242 $renderer->doc .= '<dt><a href="' 243 . exportlink($ID, 'code', array('codeblock' => $offset + $this->_getCodeBlockCount($renderer))) 244 . '" title="' . $lang['download'] . '" class="' . $class . '">'; 245 $renderer->doc .= hsc($title); 246 $renderer->doc .= '</a></dt>'.DOKU_LF.'<dd>'; 247 } 248 249 function _displayBody(Doku_Renderer $renderer, $text, $language = null, $options = null) 250 { 251 $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language); 252 $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language); 253 254 //if($text[0] == "\n") { $text = substr($text, 1); } 255 //if(substr($text, -1) == "\n") { $text = substr($text, 0, -1); } 256 257 if(empty($language)) 258 { $renderer->doc .= '<pre class="file">' .$renderer->_xmlEntities($text) . '</pre>' . DOKU_LF; } 259 else 260 { 261 $renderer->doc .= "<pre class=\"code file {$language}\">" . DOKU_LF; 262 // This method is only called when this is true. 263 //if ($this->_isPhpFixRequired($text, $language)) 264 { 265 // To avoid the GeShi error, we strip the first line 266 // and process it separately from the rest. 267 $index = strpos($text, "\n"); 268 $line = substr($text, 0, $index); 269 $renderer->doc .= p_xhtml_cached_geshi($line, $language, '', $options) . DOKU_LF; 270 $text = substr($text, $index + 1); 271 } 272 $renderer->doc .= p_xhtml_cached_geshi($text, $language, '', $options) . DOKU_LF; 273 $renderer->doc .= '</pre>' . DOKU_LF; 274 } 275 276 //$renderer->_codeblock++; 277 $this->_incCodeBlockCount($renderer); 278 } 279} 280 281//Setup VIM: ex: et ts=4 enc=utf-8 : 282?>