1<?php
2/**
3 * Source Plugin: includes a source file using the geshi highlighter
4 *
5 * Syntax:     <source filename lang|title>
6 *   filename  (required) can be a local path/file name or a remote file uri
7 *             to use remote file uri, allow_url_fopen=On must be set in the server's php.ini
8 *             filenames with spaces must be surrounded by matched pairs of quotes (" or ')
9 *   lang      (optional) programming language name, is passed to geshi for code highlighting
10 *             if not provided, the plugin will attempt to derive a value from the file name
11 *             (refer $extensions in render() method)
12 *   title     (optional) all text after '|' will be rendered above the main code text with a
13 *             different style. If no title is present, it will be set to "file: filename"
14 *
15 *  *** WARNING ***
16 *
17 *  Unless configured correctly this plugin can be a huge security risk.
18 *  Please review/consider
19 *    - users who have access to the wiki
20 *    - php.ini setting, allow_url_fopen
21 *    - php.ini setting, open_basedir
22 *    - this plugin's location, allow & deny settings.
23 *
24 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
25 * @author     Christopher Smith <chris@jalakai.co.uk>
26 */
27if(!defined('DOKU_INC')) die();  // no Dokuwiki, no go
28
29if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
30require_once(DOKU_PLUGIN.'syntax.php');
31
32/**
33 * All DokuWiki plugins to extend the parser/rendering mechanism
34 * need to inherit from this class
35 */
36class syntax_plugin_source extends DokuWiki_Syntax_Plugin {
37
38  //------------------- [ Security settings ] ---------------------------------------------
39  var $location = '';     // prepended to all file names, restricting the filespace exposed to the plugin
40  var $allow = array();   // if not empty, ONLY files with the extensions listed will be allowed
41  var $deny = array();    // if $allow array is empty, any file with an extension listed in $deny array will be denied
42  var $rules = array();   // more complex allow/deny rules, refer documentation
43
44  //------------------------[ Other settings ] ---------------------------------------------
45  var $extensions = array(
46      'htm' => 'html4strict',
47      'html' => 'html4strict',
48      'js' => 'javascript'
49    );
50
51    /**
52     * return some info
53     */
54    function getInfo(){
55      return array(
56        'author' => 'Christopher Smith',
57        'email'  => 'chris@jalakai.co.uk',
58        'date'   => '2008-08-13',
59        'name'   => 'Source Plugin',
60        'desc'   => 'Include a remote source file
61                     Syntax: <source filename #startline-endline language|title>',
62        'url'    => 'http://www.dokuwiki.org/plugin:source',
63      );
64    }
65
66    function getType() { return 'substition'; }
67    function getPType() { return 'block'; }
68    function getSort() { return 330; }
69
70    /**
71     * Connect pattern to lexer
72     */
73    function connectTo($mode) {
74      $this->Lexer->addSpecialPattern('<source.*?>',$mode,substr(get_class($this), 7));
75    }
76
77    /**
78     * Handle the match
79     */
80    function handle($match, $state, $pos, &$handler){
81      $match = trim(substr($match,7,-1));                    //strip <source from start and > from end
82
83      // ['|"]?<filename>[\1] [#<line#>-<line#>] <lang> | <title>
84      list($attr, $title) = preg_split('/\|/u', $match, 2);  //split out title
85
86      $attr = trim($attr);
87      $pattern = ($attr{0} == '"' || $attr{0} == "'") ? $attr{0} : '\s';
88      list($file, $prop) = preg_split("/$pattern/u", $attr, 3, PREG_SPLIT_NO_EMPTY);
89
90      if (isset($prop) && trim($prop)) {
91        $matches = array();
92        if (preg_match('/\s*(?:(?:#(\d+)-(\d+))\s*)?(\w+)?/',$prop,$matches)) {
93          list(,$start,$end,$lang) = $matches;
94          if (!isset($lang)) $lang = '';
95        }
96      } else {
97        $start = $end = $lang = '';
98      }
99
100      return array(trim($file), $lang, (isset($title)?trim($title):''), $start, $end);
101    }
102
103    /**
104     * Create output
105     */
106    function render($format, &$renderer, $data) {
107
108      $this->_loadSettings();
109
110      list($file, $lang, $title, $start, $end) = $data;
111      $ext = substr(strrchr($file, '.'),1);
112
113      $ok = false;
114      if (count($this->allow)) {
115        if (in_array($ext, $this->allow)) $ok = true;
116      } else {
117        if (!in_array($ext, $this->deny)) $ok = true;
118      }
119
120      // prevent filenames which attempt to move up directory tree by using ".."
121      if ($ok && $this->location && preg_match('/(?:^|\/)\.\.(?:\/|$)/', $file)) $ok = false;
122      if ($ok && $this->rules) $ok = $this->_checkRules($file);
123
124      if (!$lang) { $lang = isset($this->extensions[$ext]) ? $this->extensions[$ext] : $ext; }
125
126      switch ($format) {
127        case 'xhtml' :
128
129          if ($ok && ($source = $this->_getSource($file,$start,$end))) {
130
131            $title = ($title) ? "<span>".hsc($title)."</span>"
132                               : $this->_makeTitle($file, $start, $end);
133
134            $renderer->doc .= "<div class='source'><p>$title</p>";
135            $renderer->code($source, $lang);
136            $renderer->doc .= "</div>";
137          } else {
138            $error = sprintf($this->getLang('error_file'),hsc($file));
139            $renderer->doc .= '<div class="source"><p><span>'.$error.'</span></p></div>';
140          }
141          break;
142
143        case 'metadata' :
144          if ($ok) {
145            $renderer->meta['relation']['haspart'][$file] = array('owner'=>$this->getPluginName());
146          }
147          break;
148
149        default :
150          if ($ok) {
151            $renderer->code($this->_getSource($file,$start,$end), $lang);
152          }
153      }
154
155    }
156
157    function _makeTitle($file,$start,$end) {
158      $lines = $start ? sprintf($this->getLang('lines'),$start,$end) : '';
159      $title = sprintf($this->getLang('title'),hsc($file),$lines);
160
161      return $title;
162    }
163
164    function _getSource($file,$start,$end) {
165
166      $source = @file($this->location.$file);
167      if (empty($source)) return '';
168
169      // $start is a 1 based index, need to correct to 0 based when slicing arrray
170      if (!empty($start)) {
171        $lines = count($source);
172        if ($start > $lines) {
173          $source = $this->getLang('error_start');
174        } else if ($end < $start) {
175          $source = $this->getLang('error_end');
176        } else if ($end > $lines) {
177          $source = join('',array_slice($source,$start-1));
178        } else {
179          $source = join('',array_slice($source,$start-1,$end-$start));
180        }
181      } else {
182        $source = join('',$source);
183      }
184
185      return $source;
186    }
187
188    function _checkRules($file) {
189      $permit = true;
190      foreach ($this->rules as $rule) {
191        list($allow_deny, $pattern) = $rule;
192        if ($allow_deny == 'allow') {
193          if (preg_match($pattern,$file)) $permit = true;
194        } else {
195          if (preg_match($pattern,$file)) $permit = false;
196        }
197      }
198
199      return $permit;
200    }
201
202    function _loadSettings() {
203      static $loaded = false;
204
205      if ($loaded) return;
206
207      $this->location = $this->getConf('location');
208
209      $allow = $this->getConf('allow');
210      $this->allow = !empty($allow) ? explode('|',$allow) : array();
211
212      $deny = $this->getConf('deny');
213      $this->deny = !empty($deny) ? explode('|',$deny) : array();
214
215      $rules = $this->getConf('rules');
216      if (!empty($rules)) $this->_parseRules($rules);
217
218      $loaded = true;
219    }
220
221    function _parseRules($rules) {
222      $rules = explode("\n",$rules);
223      foreach ($rules as $rule) {
224        $rule = trim($rule);
225        if (!$rule || $rule{0} == ';') continue;
226
227        $match = array();
228        if (!preg_match('/^(allow|deny)\s+(.+)$/i',$rule,$match)) continue;
229
230        $this->rules[] = array(strtolower($match[1]),$match[2]);
231      }
232    }
233}
234
235//Setup VIM: ex: et ts=4 enc=utf-8 :