This plugin allows you to include the contents of another file, with syntax highlighting, into the current page.
Syntax:
<source filename language|title>
allow_url_fopen=On must be set in the webserver's php.ini file (also refer to the location setting and the security warning below).|) is treated as the title. If not present the file name will be used, e.g. "file: filename"You can see the plugin in action here.
Indiscriminate use of this plug in can be a serious security risk to your web server. It has the potential to expose any file on the web server computer that is accessible to the web server software. In particular:
allow_url_fopen=On is a potential security risk. If you use this setting, I strongly recommend ensuring 'php' is included in the denied extension list (refer $deny setting below).open_basedir is correctly set. If not, this plugin has the potential to expose any file accessible to the webserver to the page creator/editor.
For further information, see PHP File System Security
<source> command. $location='http://' you can force all file retrieval through webservers and thereby make use of the webservers' own restrictions (e.g. in Apache systems, DocumentRoot, .htaccess files) which are likely to be stronger than that of the file system itself. $location all file names which include the path traversal ".." will be rejected. $allow is empty, no file with an extension included in $deny may be "sourced". If $allow is non-empty $deny is ignored."file-extension" => "GeSHI-language".Plugin sources: zip format (3k), tar.gz format (2k)
Download the source to your plugin folder, lib/plugins and extract its contents. That will create a new plugin folder, lib/plugins/source and install the plugin there.
The folder will contain:
action.php action plugin to control caching conf/default.php conf/metadata.php conf/settings.class.php lang/en/lang.php lang/en/settings.php style.css styles for the new boxes and titles syntax.php syntax plugin script
The plugin is now installed.
<?php
/**
* Source Plugin: includes a source file using the geshi highlighter
*
* Syntax: <source filename lang|title>
* filename (required) can be a local path/file name or a remote file uri
* to use remote file uri, allow_url_fopen=On must be set in the server's php.ini
* filenames with spaces must be surrounded by matched pairs of quotes (" or ')
* lang (optional) programming language name, is passed to geshi for code highlighting
* if not provided, the plugin will attempt to derive a value from the file name
* (refer $extensions in render() method)
* title (optional) all text after '|' will be rendered above the main code text with a
* different style. If no title is present, it will be set to "file: filename"
*
* *** WARNING ***
*
* Unless configured correctly this plugin can be a huge security risk.
* Please review/consider
* - users who have access to the wiki
* - php.ini setting, allow_url_fopen
* - php.ini setting, open_basedir
* - this plugin's location, allow & deny settings.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Christopher Smith <chris@jalakai.co.uk>
*/
if(!defined('DOKU_INC')) die(); // no Dokuwiki, no go
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
/**
* All DokuWiki plugins to extend the parser/rendering mechanism
* need to inherit from this class
*/
class syntax_plugin_source extends DokuWiki_Syntax_Plugin {
//------------------- [ Security settings ] ---------------------------------------------
var $location = ''; // prepended to all file names, restricting the filespace exposed to the plugin
var $allow = array(); // if not empty, ONLY files with the extensions listed will be allowed
var $deny = array(); // if $allow array is empty, any file with an extension listed in $deny array will be denied
var $rules = array(); // more complex allow/deny rules, refer documentation
//------------------------[ Other settings ] ---------------------------------------------
var $extensions = array(
'htm' => 'html4strict',
'html' => 'html4strict',
'js' => 'javascript'
);
/**
* return some info
*/
function getInfo(){
return array(
'author' => 'Christopher Smith',
'email' => 'chris@jalakai.co.uk',
'date' => '2008-08-13',
'name' => 'Source Plugin',
'desc' => 'Include a remote source file
Syntax: <source filename #startline-endline language|title>',
'url' => 'http://www.dokuwiki.org/plugin:source',
);
}
function getType() { return 'substition'; }
function getPType() { return 'block'; }
function getSort() { return 330; }
/**
* Connect pattern to lexer
*/
function connectTo($mode) {
$this->Lexer->addSpecialPattern('<source.*?>',$mode,substr(get_class($this), 7));
}
/**
* Handle the match
*/
function handle($match, $state, $pos, &$handler){
$match = trim(substr($match,7,-1)); //strip <source from start and > from end
// ['|"]?<filename>[\1] [#<line#>-<line#>] <lang> | <title>
list($attr, $title) = preg_split('/\|/u', $match, 2); //split out title
$attr = trim($attr);
$pattern = ($attr{0} == '"' || $attr{0} == "'") ? $attr{0} : '\s';
list($file, $prop) = preg_split("/$pattern/u", $attr, 3, PREG_SPLIT_NO_EMPTY);
if (isset($prop) && trim($prop)) {
$matches = array();
if (preg_match('/\s*(?:(?:#(\d+)-(\d+))\s*)?(\w+)?/',$prop,$matches)) {
list(,$start,$end,$lang) = $matches;
if (!isset($lang)) $lang = '';
}
} else {
$start = $end = $lang = '';
}
return array(trim($file), $lang, (isset($title)?trim($title):''), $start, $end);
}
/**
* Create output
*/
function render($format, &$renderer, $data) {
$this->_loadSettings();
list($file, $lang, $title, $start, $end) = $data;
$ext = substr(strrchr($file, '.'),1);
$ok = false;
if (count($this->allow)) {
if (in_array($ext, $this->allow)) $ok = true;
} else {
if (!in_array($ext, $this->deny)) $ok = true;
}
// prevent filenames which attempt to move up directory tree by using ".."
if ($ok && $this->location && preg_match('/(?:^|\/)\.\.(?:\/|$)/', $file)) $ok = false;
if ($ok && $this->rules) $ok = $this->_checkRules($file);
if (!$lang) { $lang = isset($this->extensions[$ext]) ? $this->extensions[$ext] : $ext; }
switch ($format) {
case 'xhtml' :
if ($ok && (false !== ($source = $this->_getSource($file,$start,$end)))) {
$title = ($title) ? "<span>".hsc($title)."</span>"
: $this->_makeTitle($file, $start, $end);
$renderer->doc .= "<div class='source'><p>$title</p>";
$renderer->code($source, $lang);
$renderer->doc .= "</div>";
} else {
$error = sprintf($this->getLang('error_file'),hsc($file));
$renderer->doc .= '<div class="source"><p><span>'.$error.'</span></p></div>';
}
break;
case 'metadata' :
if ($ok) {
$renderer->meta['relation']['haspart'][$file] = array('owner'=>$this->getPluginName());
}
break;
default :
if ($ok) {
$renderer->code($this->_getSource($file,$start,$end), $lang);
}
}
}
function _makeTitle($file,$start,$end) {
$lines = $start ? sprintf($this->getLang('lines'),$start,$end) : '';
$title = sprintf($this->getLang('title'),hsc($file),$lines);
return $title;
}
function _getSource($file,$start,$end) {
if (!file_exists($this->location.$file)) return false;
$source = @file($this->location.$file);
if (empty($source)) return '';
// $start is a 1 based index, need to correct to 0 based when slicing arrray
if (!empty($start)) {
$lines = count($source);
if ($start > $lines) {
$source = $this->getLang('error_start');
} else if ($end < $start) {
$source = $this->getLang('error_end');
} else if ($end > $lines) {
$source = join('',array_slice($source,$start-1));
} else {
$source = join('',array_slice($source,$start-1,$end-$start));
}
} else {
$source = join('',$source);
}
return $source;
}
function _checkRules($file) {
$permit = true;
foreach ($this->rules as $rule) {
list($allow_deny, $pattern) = $rule;
if ($allow_deny == 'allow') {
if (preg_match($pattern,$file)) $permit = true;
} else {
if (preg_match($pattern,$file)) $permit = false;
}
}
return $permit;
}
function _loadSettings() {
static $loaded = false;
if ($loaded) return;
$this->location = $this->getConf('location');
$allow = $this->getConf('allow');
$this->allow = !empty($allow) ? explode('|',$allow) : array();
$deny = $this->getConf('deny');
$this->deny = !empty($deny) ? explode('|',$deny) : array();
$rules = $this->getConf('rules');
if (!empty($rules)) $this->_parseRules($rules);
$loaded = true;
}
function _parseRules($rules) {
$rules = explode("\n",$rules);
foreach ($rules as $rule) {
$rule = trim($rule);
if (!$rule || $rule{0} == ';') continue;
$match = array();
if (!preg_match('/^(allow|deny)\s+(.+)$/i',$rule,$match)) continue;
$this->rules[] = array(strtolower($match[1]),$match[2]);
}
}
}
//Setup VIM: ex: et ts=4 enc=utf-8 :<?php
/**
* Source Plugin: includes a source file using the geshi highlighter
*
* Action plugin component, for cache validity determination
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Christopher Smith <chris@jalakai.co.uk>
*/
if(!defined('DOKU_INC')) die(); // no Dokuwiki, no go
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'action.php');
/**
* All DokuWiki plugins to extend the parser/rendering mechanism
* need to inherit from this class
*/
class action_plugin_source extends DokuWiki_Action_Plugin {
/**
* return some info
*/
function getInfo(){
return array(
'author' => 'Christopher Smith',
'email' => 'chris@jalakai.co.uk',
'date' => '2008-08-13',
'name' => 'Source Plugin',
'desc' => 'Include a remote source file',
'url' => 'http://www.dokuwiki.org/plugin:source',
);
}
/**
* plugin should use this method to register its handlers with the dokuwiki's event controller
*/
function register(&$controller) {
$controller->register_hook('PARSER_CACHE_USE','BEFORE', $this, '_cache_prepare');
}
/**
* prepare the cache object for default _useCache action
*/
function _cache_prepare(&$event, $param) {
$cache =& $event->data;
// we're only interested in wiki pages and supported render modes
if (!isset($cache->page)) return;
if (!isset($cache->mode) || in_array($cache->mode,array('i','metadata'))) return;
$max_age = $this->_cache_maxage($cache->page);
if (is_null($max_age)) return;
if ($max_age <= 0) {
// expire the cache
$event->preventDefault();
$event->stopPropagation();
$event->result = false;
return;
}
$cache->depends['age'] = !empty($cache->depends['age']) ? min($cache->depends['age'],$max_age): $max_age;
}
/**
* determine the max allowable age of the cache
*
* @param string $id wiki page name
*
* @return int max allowable age of the cache
* null means not applicable
*/
function _cache_maxage($id) {
$hasPart = p_get_metadata($id, 'relation haspart');
if (empty($hasPart) || !is_array($hasPart)) return null;
$location = $this->getConf('location');
$age = 0;
foreach ($hasPart as $file => $data) {
// ensure the metadata entry was created by or for this plugin
if (empty($data['owner']) || $data['owner'] != $this->getPluginName()) continue;
$file = $this->getConf['location'].$file;
// determine max allowable age for the cache
// if filemtime can't be determined, either for a non-existent $file or for a $file using
// an unsupported scheme, expire the cache immediately/always
$mtime = @filemtime($file);
if (!$mtime) { return 0; }
$age = max($age,$mtime);
}
return $age ? time()-$age : null;
}
}
//Setup VIM: ex: et ts=4 enc=utf-8 :
The plugin comes with some style additions in the style.css file.
<source> blocks from the <code> (blue) and <file> (red) blocks rendered by the code plugin. These can of course can be modified to suit your own requirements. More details on modifying the styles are given after the file contents listing.
/*
* source plugin extension - style additions
*
* @author Christopher Smith chris@jalakai.co.uk
* @link http://wiki.jalakai.co.uk/dokuwiki/doku.php/tutorials/codeplugin
*/
/* source plugin extensions */
/* layout */
.source {
width: 92%;
margin: 1em auto;
border: 1px solid;
padding: 4px;
}
.source p {
font-size: 90%;
margin: 0;
padding: 2px;
}
.source pre.code {
margin: 4px 0 0 0;
}
/* colours */
div.source {
border-color: #bdb;
background: #e4f8f2;
}
div.source p {
background: #c4e4d4;
}
div.source pre.code {
border: 1px dashed #9c9;
background: #ecfaf6;
}
The source file contents are wrapped within a <div> tag of class source and the file name is given above the contents. The html fragment will look like
<div class='source'> <p>{title}</p> <pre class='code {language}'>{file contents}</pre> </div>
Therefore, standard Dokuwiki styling can be overridden with the following style selectors:
/* style the source file name */ /* set to display:none to prevent it being displayed at all */ .source p {} /* style the file contents, e.g. font, border & background */ .source .code {} /* override the geshi hilight styles in /lib/styles/style.css */ .source .code .<geshi-hilite-code> {}
Hi Chris - I was using this plugin when I discovered that it does not handle filenames with spaces in them gracefully. Would you like me to replace the source above with a patched source that handles filenames with spaces (and handles ../ file security), or perhaps I could send it to you by e-mail first? oracle [dot] shinoda [at] gmail [dot] com (Nigel McNie)I am interested in how to handle spaces in file names, email me or post a snippet here. The code above has been updated. The ".." problem should be fixed and a title parameter has been added. The best way to handle spaces maybe to require the file name to be surround in matched quotes. What do you think? — Christopher Smith 2005/08/19 23:49