<?php
/*
* load_config() loads configuration options from a ini file located at the
* path given .This file is parsed with sections enabled and the configuration
* options for the main cms are located in the `[cms]' section. White and black
* lists are to be provided as a single string seperated by comma (`,'). Theses
* will be transformed into arrays and joined with the default list items. The
* function returns a multi-dimensional array with sections (only 'cms' for now)
* as keys on the first level and option names as keys in the second level.
*
* $config_path
* string path to the configuration file to be loaded
*/
function load_config($config_path){
$config = parse_ini_file($config_path, true);
if(!isset($config['cms'])){
$config['cms'] = array();
}
// just set the timezone immediately
if(isset($config['cms']['timezone'])){
date_default_timezone_set($config['cms']['timezone']);
} else {
date_default_timezone_set("Europe/Berlin");
}
if(!isset($config['cms']['global_root_dir'])){
$config['cms']['global_root_dir'] =
'/home/ps0ke/Projects/Greiner/global_root_dir/';
}
if(isset($config['cms']['directory_black_list'])){
$config['cms']['directory_black_list'] =
explode(',', $config['cms']['directory_black_list']);
} else {
$config['cms']['directory_black_list'] = array();
}
$config['cms']['directory_black_list'] =
array_merge(
$config['cms']['directory_black_list'],
array('.', '..', '.git', 'mth-cms')
);
if(isset($config['cms']['file_name_black_list'])){
$config['cms']['file_name_black_list'] =
explode(',', $config['cms']['file_name_black_list']);
} else {
$config['cms']['file_name_black_list'] = array();
}
$config['cms']['file_name_black_list'] =
array_merge(
$config['cms']['file_name_black_list'],
array('index')
);
if(isset($config['cms']['file_extension_white_list'])){
$config['cms']['file_extension_white_list'] =
explode(',', $config['cms']['file_extension_white_list']);
} else {
$config['cms']['file_extension_white_list'] = array();
}
$config['cms']['file_extension_white_list'] =
array_merge(
$config['cms']['file_extension_white_list'],
array('html')
);
/*
* Automatically enable `php' as a valid extension if its execution is
* enabled. It would be better to use `!==' and `===' for checking for
* true boolean types. This would prevent logic errors due to PHP's
* automatic type conversion (e.g. a non empy string like a wrongly
* splelled `false' in the config would evaluate to true). Unfortune-
* nately this doesen't work and I suspect that parse_ini_file() does
* not set true boolean types.
*/
if(!isset($config['cms']['execute_php']) ||
$config['cms']['execute_php'] != true
){
$config['cms']['execute_php'] = false;
}
if($config['cms']['execute_php'] == true){
array_push(
$config['cms']['file_extension_white_list'],
'php'
);
}
return $config;
}
/*
* link_from_path() generates a link (no full URL) from a path to a local file.
* It defaults to the language set globally, but you can set a language manu-
* ally.
*
* $path
* the path of the file to be linked to. Starting `.' and `/' will be re-
* moved.
* $language
* the language of the page the the link poits to. This will default to the
* language set globally. Should be a two-letter country code. For now
* either `en' or `de'.
*/
function link_from_path($path, $language=null){
/*
* I am not really sure which variable to use: SCRIPT_URI and SCRIPT_URL
* are not guaranteed to be available on all systems. SCRIPT_NAME seems to
* be a safe choice, but I don't know how flexible and robust this is.
* CONTEXT_PREFIX would probably be the best choice but is also not
* available on all systems.
*
* relative links
* - SCRIPT_NAME
* - SCRIPT_URL
* - CONTEXT_PREFIX
*
* absolute links
* - SCIPT_URI
*/
$base = pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME);
$base = ltrim($base, '/');
$base = rtrim($base, '/');
if(is_null($language)){
if('en' == $GLOBALS['lang']){
$lang = 'en/';
} else {
$lang = '';
}
} else {
if('de' == $language){
$lang = '';
} else {
$lang = $language.'/';
}
}
$path = ltrim($path, '.');
$path = ltrim($path, '/');
return "/$base/{$lang}page/$path";
}
/*
* language_toggle_html_link() renders an html string containing a link to
* the other language version of the page. It uses the language set globally.
* Note: It does *not* check, wether the language is actually available!
*
* $path
* defaults to the GET path, the path for the page.
* $lang
* defaults to the opposite of the global language. The language of the page
8 the link points to.
*/
function language_toggle_html_link($path=null, $lang=null){
if(is_null($path)){
$path = $GLOBALS['path'];
}
if(is_null($lang)){
$lang = $GLOBALS['lang'];
}
if('en' == $lang){
$other_lang_link = link_from_path($path, 'de');
$other_lang_display = 'Deutsch';
} else {
$other_lang_link = link_from_path($path, 'en');
$other_lang_display = 'English';
}
echo "<a href=\"$other_lang_link\">$other_lang_display</a>";
}
/*
* Returns a boolean value if the *other* language is available. This is
* intended to be used in templating to only show the language toggle when
* available. This function is language aware.
*
* $path
* the path of the page to check availability for. Defaults to the global
* GET path
* $lang
* the *current* (!) language. This function will check availability of the
* *counter part* language. E.g. if $lang is 'de',
* language_toggle_available() will return `true' if the page at $path is
* available in 'en'!
*/
function language_toggle_available($path=null, $lang=null){
if(is_null($path)){
$path = $GLOBALS['path'];
}
if(is_null($lang)){
$lang = $GLOBALS['lang'];
}
if('en' == $lang){
$file = file_name_from_path($path, 'de');
} else {
$file = file_name_from_path($path, 'en');
}
return file_exists($file);
}
/*
* Generates the actual file name to render from the input path. This respects
* the global language value if not given otherwise and will return index.html
* files for directories.
*
* $path
* the path to create the file name for. May be a directory
* $lang
* if given, the filename will have this language suffix
*/
function file_name_from_path($path, $lang=null){
if(is_null($lang)){
$lang = $GLOBALS['lang'];
}
if(!file_exists($path)){
return null;
}
// If a directory is requested, try to serve its index.html
if(is_dir($path)){
$file = rtrim($path, '/').'/index.html';
} else {
$file = $path;
}
// Construct the english version file name.
if('de' != $lang){
$info = pathinfo($file);
$file = $info['dirname'].'/'.$info['filename'].".$lang.".$info['extension'];
}
return $file;
}
/*
* The Node class contains a linked list of files or directories, which is
* build recursivly when the object is created.
*
* $path
* contains the path to the node, relative to the path the top-most Node
* was initialized with, as a string.
* $display_name
* contains the name that shall be rendered. Will be set by the user. A
* string.
* $display_name_en
* contains the render-name for the english version of the file
* $display_position
* is an integer index relevant for the ordering of the menu items during
* rendering. Will be set by the user
* $is_directory
* is a boolean value wether the Node is a file or a directory; this
* possibly containing sub nodes
* $sub_nodes
* is null if there are no sub nodes; Otherwise it is an array containing
* additional Node objects.
* $visible
* is a boolean value wether the Node should be added to to the sub-nodes
* and therefore be rendered in the navigation. It can be set by the user
* via the ini configuration but defaults to true.
*
* $navigation_first
* is a boolean value wether the Node should get the `first' class in
* rendering of the navigation
*/
class Node {
public $path;
public $display_name;
public $display_name_en = null;
public $display_position;
public $is_directory;
public $sub_nodes = null;
public $visible = true;
private $navigation_first = false;
function __construct($path){
if(!file_exists($path)){
throw new Exception("$path does not exist.");
}
$this->path = $path;
$this->is_directory = is_dir($path);
$this->sub_nodes = $this->sub_nodes();
/*
* Try to parse the display information from the ini-section. Fall back
* to file name if the information was not set. parse_ini_file() is
* called silent to surpress warnings if file does not exist. If the
* node is a file, the file is read and the first comment block is
* parsed as an ini-section.
*/
if($this->is_directory){
$ini = @parse_ini_file($this->path.'/dir.ini');
} else {
$lines = file($this->path);
$ini_array = array();
foreach($lines as $line){
if("<!--\n" == $line){
continue;
} else if("-->\n" == $line){
break;
} else {
array_push($ini_array, $line);
}
}
$ini = @parse_ini_string(implode($ini_array));
}
if(isset($ini['name'])){
$this->display_name = $ini['name'];
} else {
$this->display_name = pathinfo($path)['filename'];
}
if(isset($ini['position'])){
$this->display_position = $ini['position'];
} else {
$this->display_position = 1000;
}
if(isset($ini['visible'])){
$this->visible = $ini['visible'];
}
if(isset($ini['name_en'])){
$this->display_name_en = $ini['name_en'];
}
}
/*
* The build_navigation() function recursivly builds a directory tree from
* the node with nested html unordered lists. It renderes content to the
* page. Only top nodes for each level in the current path are shown. This
* is achieved by testing the current Node path against each part of the
* requested path and its parent directory incrementally.
* Links will point to english version if viewed from an english version
* page.
*
* $path
* is the GET path. It is used to narrow the drill-down. Only the top
* nodes in each level are shown.
* $first_run
* holds a boolean value wether the function is run for the first time
* i. e. has been called by the programmer or if it was called as an
* act of recursion by itself.
*/
public function build_navigation($path, $first_run=true){
$path_array = explode('/', $path);
$incremental_path = ".";
$visible = false;
foreach($path_array as $part){
$incremental_path .= "/".$part;
$visible |= dirname($this->path) == dirname($incremental_path);
$visible |= dirname($this->path) == $incremental_path;
}
$expanded = true;
if(!$first_run && $visible){
if('en' == $GLOBALS['lang'] && !is_null($this->display_name_en)){
$display_name = $this->display_name_en;
} else {
$display_name = $this->display_name;
}
$link = link_from_path($this->path);
$classes = '';
if($this->navigation_first){
$classes .= 'first ';
} else {
$classes .= 'leaf ';
}
if($this->is_directory){
/*
* check if the current node is a parent of the requested node
* by checking if its path is a sub string of the requested
* path.
*/
if(strpos('./'.$path, $this->path) === false){
$expanded = false;
$classes .= 'collapsed ';
} else {
$classes .= 'expanded ';
}
}
$classes = rtrim($classes);
if('./'.$path == $this->path){
$active = ' class="active"';
} else {
$active = '';
}
echo "<li class=\"$classes\">";
echo "<a href=\"${link}\" title=\"$display_name\" onfocus=\"blurLink(this);\"$active>";
echo $display_name;
echo "</a>\n";
}
if($this->is_directory && $this->sub_nodes && $expanded){
/*
* The nesting level of a navigation point is calculated simply by
* counting the elements in the path.
*
* Wether the Node is the first in a navigation list is figured out
* by using a private variable `$navigation_first' that is set for
* each Node. Before recursing each sub node during rendering, the
* `$first' variable is set to true, which will set every sub
* node's $navigation_first. After setting it (for the first time)
* $first is set to false and will set all following $navigation_
* first to false.
*/
$level = count(explode('/', $this->path));
echo "<ul class=\"menu level-$level\">\n";
$first = true;
foreach($this->sub_nodes as $sub_node){
if(!is_null($sub_node)){
$sub_node->navigation_first = $first;
$first = false;
$sub_node->build_navigation($path, false);
}
}
echo "</ul>\n";
}
if(!$first_run && $visible){
echo "</li>\n";
}
}
/*
* find_node_by_path returns the Node object correspronding to the $path.
* It recursivly walks the Node-tree and checks if the path is coreect. If
* it matches, the Node object is returned, null otherwise.
*/
public function find_node_by_path($path){
if(rtrim($this->path, '/') == rtrim('./'.$path, '/')){
return $this;
} else {
if($this->sub_nodes){
foreach($this->sub_nodes as $sub_node){
$ret = $sub_node->find_node_by_path($path);
if(!is_null($ret)){
return $ret;
}
}
return null;
} else {
return null;
}
}
}
/*
* the sub_nodes() function is called by the constructor and fills the
* sub_nodes property with an array of Node objects. Only valid sub_nodes
* will be valid. File names and extensions are matched against various
* black- and white-lists.
*/
private function sub_nodes(){
global $config;
if($this->is_directory){
$sub_nodes = array();
foreach(glob("{$this->path}/*") as $sub_path){
$add_this_node = true;
$pathinfo = pathinfo($sub_path);
if(is_dir($sub_path)){
// directory name black-list
if(in_array(
$pathinfo['filename'],
$config['cms']['directory_black_list']
)
){
$add_this_node = false;
}
} else {
// file extension white-list
if(! in_array(
$pathinfo['extension'],
$config['cms']['file_extension_white_list']
)
||
// file name black-list
in_array(
$pathinfo['filename'],
$config['cms']['file_name_black_list']
)
){
$add_this_node = false;
}
// exclude english versions of files
if( preg_match('/\.en$/', $pathinfo['filename']) ){
$add_this_node = false;
}
}
$new_node = new Node($sub_path);
if($add_this_node && $new_node->visible){
$display_position = $new_node->display_position;
while(isset($sub_nodes[$display_position])){
$display_position += 1;
}
$sub_nodes[$display_position] = $new_node;
}
}
ksort($sub_nodes);
return $sub_nodes;
} else {
return null;
}
}
}
/*
* load_templated loads template files. It looks for them first in a local
* directory that every user can change. If it does not find the desired
* template there, it looks in a the `global_root_dir' from the config.
* This should be a central directory on the webserver that only the admin
* has access to. This makes updating the templates easy for thos users that
* don't need customization.
*
* $template
* the name of the template to load as a string. The file extension should
* not be included here.
*/
function load_template($template){
global $config;
/*
* All global variables that hold teh cms' state must be included in this
* scope to be available from within the include()ed template file.
*/
global $path;
global $base_node;
global $current_node;
global $lang;
$global_path = rtrim($config['cms']['global_root_dir'], '/').'/'.$template.'.html';
$local_path ="mth-cms/templates/$template.html";
if(file_exists($local_path)){
include($local_path);
} else {
include($global_path);
}
}
/*
* render_content() renders the content in respect to the GET path and the
* globally set language. If the file does not exist or only another language is
* available, it will transparently render an error page.
*/
function render_content(){
global $config;
global $path;
global $lang;
$file = file_name_from_path($path);
$file_de = file_name_from_path($path, 'de');
if('en' == $lang && !file_exists($file) && file_exists($file_de)){
// English version was not found, but a german version exists.
load_template('error_404_other_language_available');
} else if(!file_exists($file)){
// No german version exists
load_template('error_404');
} else {
/*
* If PHP execution is activated in the config and the file is a PHP
* file, run it. Otherwise check if the file is encoded as UTF-8. If
* not, try to convert it to UTF-8. Do not even try to use
* mb_detect_encoding() as it just does not work at all. Therefore
* try the one encoding that is expected to be used the most apart from
* UTF-8: ISO-8859-15 aka Latin-9. This is the best I can do.
*/
if($config['cms']['execute_php']
&& 'php' == pathinfo($file, PATHINFO_EXTENSION)
){
include($file);
} else {
$content = file_get_contents($file);
if(!mb_check_encoding($content, 'UTF-8')){
$content = mb_convert_encoding($content, 'UTF-8', 'ISO-8859-15');
}
echo $content;
}
}
}
/*
* Renders the page title to the output. Is ment to be used in the template's
* <title> tag. title() is language aware.
*/
function title(){
global $lang;
global $path;
global $current_node;
if(file_exists(file_name_from_path($path))){
if('en' == $lang && !is_null($current_node->display_name_en)){
echo $current_node->display_name_en;
} else {
echo $current_node->display_name;
}
} else {
if('en' == $lang){
echo "404 Page not found";
} else {
echo "404 Seite nicht gefunden";
}
}
}
/*
* Renders the date of last modification of the current document to the output.
* It uses the filesystem `mtime'. last_modified() is language aware.
*/
function last_modified(){
global $path;
global $lang;
$file = file_name_from_path($path);
if(file_exists($file)){
$mtime = date("d.m.Y", filemtime($file));
if('en' == $lang){
echo("Last modified: $mtime");
} else {
echo("Letzte Ändergung: $mtime");
}
}
}
/*
* this renders the quicklings section of the page. Mainly a convenience alias
* to `load_template()'.
*/
function quicklinks(){
load_template('quicklinks');
}
/*
* renders the infloline/footer section of the page. Mainly a convenience alias
* for `load_template()'
*/
function footer(){
load_template('footer');
}
?>
<?php
/*
* main function that sets up all relevant global variables and calls the data
* model creation and content rendering functions
*/
$path = $_GET['path'];
if(isset($_GET['lang']) && 'en' == $_GET['lang']){
$lang = 'en';
} else {
$lang = 'de';
}
/*
* Check if the requested file is a text file. If so, try to serve it in
* the cms. Otherwise (e.g. an image) just serve it as is.
*/
if(file_exists(file_name_from_path($path))){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $path);
finfo_close($finfo);
if(!is_dir($path) && !preg_match("/^text\//", $mime_type)){
header("Content-Type: $mime_type");
echo(file_get_contents($path));
exit(0);
}
} else {
header("HTTP/1.1 404 Not Found");
}
$config = load_config('mth-cms/config.ini');
$base_node = new Node('.');
/*
* This tries to set the $current_node to the Node object corresponding to the
* current $path. This might fail if the actual file is on a black list and
* thus not added to the $base_node tree. If this is the case, we just use the
* Node of the parent directory. This seems to be the best solution because
* file name black listing is mostly used to exclude `index' files.
*/
$current_node = $base_node->find_node_by_path($path);
if(is_null($current_node)
&& in_array(
pathinfo($path)['filename'],
$config['cms']['file_name_black_list']
)
){
$current_node = $base_node->find_node_by_path(pathinfo($path)['dirname']);
}
// debugging
//echo '<pre>';
//print_r($_GET);
//print_r($base_node);
//print_r($_SERVER);
//print_r($current_node);
//var_dump($config);
//echo '</pre>';
load_template('main');
?>
Here you find the average performance (time & memory) of each version. A grayed out version indicates it didn't complete successfully (based on exit-code).