action = $settings->defaultaction; $env->page = ""; $env->user = "Anonymous"; $env->is_logged_in = false; $env->is_admin = false; session_start(); ///////// Login System ///////// // Clear expired sessions if(isset($_SESSION["$settings->sessionprefix-expiretime"]) and $_SESSION["$settings->sessionprefix-expiretime"] < time()) { // Clear the session variables $_SESSION = []; session_destroy(); $env->is_logged_in = false; $env->user = "Anonymous"; } if(!isset($_SESSION[$settings->sessionprefix . "-user"]) and !isset($_SESSION[$settings->sessionprefix . "-pass"])) { // The user is not logged in $env->is_logged_in = false; } else { $env->user = $_SESSION[$settings->sessionprefix . "-user"]; $env->pass = $_SESSION[$settings->sessionprefix . "-pass"]; if($settings->users[$env->user] == $env->pass) { // The user is logged in $env->is_logged_in = true; } else { // The user's login details are invalid (what is going on here?) // Unset the session variables, treat them as an anonymous user, // and get out of here $env->is_logged_in = false; $env->user = "Anonymous"; $env->pass = ""; // Clear the session data $_SESSION = []; //delete all the variables session_destroy(); //destroy the session } } //check to see if the currently logged in user is an admin $env->is_admin = false; if($env->is_logged_in) { foreach($settings->admins as $admin_username) { if($admin_username == $env->user) { $env->is_admin = true; break; } } } /////// Login System End /////// /////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////// Functions //////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// /* * @summary Converts a filesize into a human-readable string. * @source http://php.net/manual/en/function.filesize.php#106569 * @editor Starbeamrainbowlabs * * @param $bytes - The number of bytes to convert. * @param $decimals - The number of decimal places to preserve. */ function human_filesize($bytes, $decimals = 2) { $sz = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "YB", "ZB"]; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; } /* * @summary Calculates the time sincce a particular timestamp and returns a * human-readable result. * @source http://snippets.pro/snippet/137-php-convert-the-timestamp-to-human-readable-format/ * * @param $time - The timestamp to convert. * * @returns {string} - The time since the given timestamp pas a human-readable string. */ function human_time_since($time) { $timediff = time() - $time; $tokens = array ( 31536000 => 'year', 2592000 => 'month', 604800 => 'week', 86400 => 'day', 3600 => 'hour', 60 => 'minute', 1 => 'second' ); foreach ($tokens as $unit => $text) { if ($timediff < $unit) continue; $numberOfUnits = floor($timediff / $unit); return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'').' ago'; } } /* * @summary A recursive glob() function. * * @param $pattern - The glob pattern to use to find filenames. * @param $flags - The glob flags to use when finding filenames. * * @returns {array} - An array of the filepaths that match the given glob. */ // From http://in.php.net/manual/en/function.glob.php#106595 function glob_recursive($pattern, $flags = 0) { $files = glob($pattern, $flags); foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { $prefix = "$dir/"; // Remove the "./" from the beginning if it exists if(substr($prefix, 0, 2) == "./") $prefix = substr($prefix, 2); $files = array_merge($files, glob_recursive($prefix . basename($pattern), $flags)); } return $files; } /* * @summary Gets a list of all the sub pages of the current page. * * @param $pageindex - The pageindex to use to search. * @param $pagename - The name of the page to list the sub pages of. * * @returns An objectt containing all the subpages, and their respective distances from the given page name in the pageindex tree. */ function get_subpages($pageindex, $pagename) { $pagenames = get_object_vars($pageindex); $result = new stdClass(); $stem = "$pagename/"; $stem_length = strlen($stem); foreach($pagenames as $entry => $value) { if(substr($entry, 0, $stem_length) == $stem) { // We found a subpage // Extract the subpage's key relative to the page that we are searching for $subpage_relative_key = substr($entry, $stem_length, -3); // Calculate how many times removed the current subpage is from the current page. 0 = direct descendant. $times_removed = substr_count($subpage_relative_key, "/"); // Store the name of the subpage we found $result->$entry = $times_removed; } } unset($pagenames); return $result; } /* * @summary Makes sure that a subpage's parents exist. Note this doesn't check the pagename itself. * * @param The pagename to check. * */ function check_subpage_parents($pagename) { global $pageindex; // Save the new pageindex and return if there aren't any more parent pages to check if(strpos($pagename, "/") === false) { file_put_contents("./pageindex.json", json_encode($pageindex, JSON_PRETTY_PRINT)); return; } $parent_pagename = substr($pagename, 0, strrpos($pagename, "/")); $parent_page_filename = "$parent_pagename.md"; if(!file_exists($parent_page_filename)) { // This parent page doesn't exist! Create it and add it to the page index. touch($parent_page_filename, 0); $newentry = new stdClass(); $newentry->filename = $parent_page_filename; $newentry->size = 0; $newentry->lastmodified = 0; $newentry->lasteditor = "none"; $pageindex->$parent_pagename = $newentry; } check_subpage_parents($parent_pagename); } /* * @summary makes a path safe * * @details paths may only contain alphanumeric characters, spaces, underscores, and dashes */ function makepathsafe($string) { return preg_replace("/[^0-9a-zA-Z\_\-\ \/]/i", "", $string); } /* * @summary Hides an email address from bots by adding random html entities. * * @returns The mangled email address. */ function hide_email($str) { $hidden_email = ""; for($i = 0; $i < strlen($str); $i++) { if($str[$i] == "@") { $hidden_email .= "&#" . ord("@") . ";"; continue; } if(rand(0, 1) == 0) $hidden_email .= $str[$i]; else $hidden_email .= "&#" . ord($str[$i]) . ";"; } return $hidden_email; } /* * @summary Checks to see if $haystack starts with $needle. * * @param $haystack {string} The string to search. * @param $needle {string} The string to search for at the beginning of $haystack. * * @returns {boolean} Whether $needle can be found at the beginning of $haystack. */ function starts_with($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function system_mime_type_extensions() { global $settings; # Returns the system MIME type mapping of MIME types to extensions, as defined in /etc/mime.types (considering the first # extension listed to be canonical). $out = array(); $file = fopen($settings->mime_extension_mappings_location, 'r'); while(($line = fgets($file)) !== false) { $line = trim(preg_replace('/#.*/', '', $line)); if(!$line) continue; $parts = preg_split('/\s+/', $line); if(count($parts) == 1) continue; $type = array_shift($parts); if(!isset($out[$type])) $out[$type] = array_shift($parts); } fclose($file); return $out; } function system_mime_type_extension($type) { # Returns the canonical file extension for the MIME type specified, as defined in /etc/mime.types (considering the first # extension listed to be canonical). # # $type - the MIME type static $exts; if(!isset($exts)) $exts = system_mime_type_extensions(); return isset($exts[$type]) ? $exts[$type] : null; } /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////// Security and Consistency Measures //////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// /* * Sort out the pageindex. We create it if it doesn't exist, and load and parse * it if it does. */ if(!file_exists("./pageindex.json")) { $existingpages = glob_recursive("*.md"); $pageindex = new stdClass(); // We use a for loop here because foreach doesn't loop over new values inserted // while we were looping for($i = 0; $i < count($existingpages); $i++) { $pagefilename = $existingpages[$i]; // Create a new entry $newentry = new stdClass(); $newentry->filename = utf8_encode($pagefilename); // Store the filename $newentry->size = filesize($pagefilename); // Store the page size $newentry->lastmodified = filemtime($pagefilename); // Store the date last modified // Todo find a way to keep the last editor independent of the page index $newentry->lasteditor = utf8_encode("unknown"); // Set the editor to "unknown" // Extract the name of the (sub)page without the ".md" $pagekey = utf8_encode(substr($pagefilename, 0, -3)); // Subpage parent checker if(strpos($pagekey, "/") !== false) { // We have a sub page people // Work out what our direct parent's key must be in order to check to // make sure that it actually exists. If it doesn't, then we need to // create it. $subpage_parent_key = substr($pagekey, 0, strrpos($pagekey, "/")); $subpage_parent_filename = "$subpage_parent_key.md"; if(array_search($subpage_parent_filename, $existingpages) === false) { // Our parent page doesn't acutally exist - create it touch($subpage_parent_filename, 0); // Furthermore, we should add this page to the list of existing pages // in order for it to be indexed $existingpages[] = $subpage_parent_filename; } } // Store the new entry in the new page index $pageindex->$pagekey = $newentry; } file_put_contents("./pageindex.json", json_encode($pageindex, JSON_PRETTY_PRINT)); unset($existingpages); } else { $pageindex = json_decode(file_get_contents("./pageindex.json")); } // Work around an Opera + Syntaxtic bug where there is no margin at the left hand side if there isn't a query string when accessing a .php file if(!isset($_GET["action"]) and !isset($_GET["page"])) { http_response_code(302); header("location: index.php?action=$settings->defaultaction&page=$settings->defaultpage"); exit(); } // Make sure that the action is set if(!isset($_GET["action"])) $_GET["action"] = $settings->defaultaction; // Make sure that the page is set if(!isset($_GET["page"]) or strlen($_GET["page"]) === 0) $_GET["page"] = $settings->defaultpage; // Redirect the user to the safe version of the path if they entered an unsafe character if(makepathsafe($_GET["page"]) !== $_GET["page"]) { http_response_code(301); header("location: index.php?action=" . rawurlencode($_GET["action"]) . "&page=" . makepathsafe($_GET["page"])); header("x-requested-page: " . $_GET["page"]); header("x-actual-page: " . makepathsafe($_GET["page"])); exit(); } $env->page = $_GET["page"]; $env->action = strtolower($_GET["action"]); /////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////// HTML fragments ////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////// class page_renderer { public static $html_template = " {title} {header-html} {body} "; public static $main_content_template = "{navigation-bar}

{sitename}

{content}
{navigation-bar-bottom} {all-pages-datalist}"; public static $minimal_content_template = "
{content}
"; // An array of functions that have been registered to process the // find / replace array before the page is rendered. Note that the function // should take a *reference* to an array as its only argument. protected static $part_processors = []; // Registers a function as a part post processor. public static function register_part_preprocessor($function) { global $settings; // Make sure that the function we are about to register is valid if(!is_callable($function)) { http_response_code(500); $admin_name = $settings->admindetails["name"]; $admin_email = hide_email($settings->admindetails["email"]); exit(page_renderer::render("$settings->sitename - Module Error", "

$settings->sitename has got a misbehaving module installed that tried to register an invalid HTML handler with the page renderer. Please contact $settings->sitename's administrator $admin_name at $admin_email.")); } self::$part_processors[] = $function; return true; } public static function render($title, $content, $body_template = false) { global $settings, $start_time, $version; if($body_template === false) $body_template = self::$main_content_template; if(strlen($settings->logo_url) > 0) { // A logo url has been specified $logo_html = ""; switch($settings->logo_position) { case "left": $logo_html = "$logo_html $settings->sitename"; break; case "right": $logo_html .= " $settings->sitename"; break; default: throw new Exception("Invalid logo_position '$settings->logo_position'. Valid values are either \"left\" or \"right\" and are case sensitive."); } } $parts = [ "{body}" => $body_template, "{sitename}" => $logo_html, "{version}" => $version, "{favicon-url}" => $settings->favicon, "{header-html}" => self::get_css_as_html(), "{navigation-bar}" => self::render_navigation_bar($settings->nav_links, $settings->nav_links_extra, "top"), "{navigation-bar-bottom}" => self::render_navigation_bar($settings->nav_links_bottom, [], "bottom"), "{admin-details-name}" => $settings->admindetails["name"], "{admin-details-email}" => $settings->admindetails["email"], "{admins-name-list}" => implode(", ", $settings->admins), "{generation-date}" => date("l jS \of F Y \a\\t h:ia T"), "{all-pages-datalist}" => self::generate_all_pages_datalist(), "{footer-message}" => $settings->footer_message ]; // Pass the parts through the part processors foreach(self::$part_processors as $function) { $function($parts); } $result = self::$html_template; $result = str_replace(array_keys($parts), array_values($parts), $result); $result = str_replace([ "{title}", "{content}" ], [ $title, $content ], $result); $result = str_replace("{generation-time-taken}", microtime(true) - $start_time, $result); return $result; } public static function render_main($title, $content) { return self::render($title, $content, self::$main_content_template); } public static function render_minimal($title, $content) { return self::render($title, $content, self::$minimal_content_template); } public static function get_css_as_html() { global $settings; if(preg_match("/^[^\/]*\/\/|^\//", $settings->css)) return ""; else return ""; } public static $nav_divider = " | "; /* * @summary Function to render a navigation bar from an array of links. See * $settings->nav_links for format information. * * @param $nav_links - The links to add to the navigation bar. * @param $nav_links_extra - The extra nav links to add to the "More..." * menu. */ public static function render_navigation_bar($nav_links, $nav_links_extra, $class = "") { global $settings, $env; $result = "

"; return $result; } public static function render_username($name) { global $settings; $result = ""; if(in_array($name, $settings->admins)) $result .= $settings->admindisplaychar; $result .= $name; return $result; } public static function generate_all_pages_datalist() { global $pageindex; $result = "\n"; foreach($pageindex as $pagename => $pagedetails) { $result .= "\t\t\t"; return $result; } } ////////////////////////// /// Module functions /// ////////////////////////// // These functions are // // used by modules to // // register themselves // // or new pages. // ////////////////////////// $modules = []; // List that contains all the loaded modules // Function to register a module function register_module($moduledata) { global $modules; //echo("registering module\n"); //var_dump($moduledata); $modules[] = $moduledata; } // Function to register an action handler $actions = new stdClass(); function add_action($action_name, $func) { global $actions; //echo("adding $action_name\n"); $actions->$action_name = $func; } // Function to register a new parser. $parsers = [ "none" => function() { throw new Exception("No parser registered!"); } ]; function add_parser($name, $parser_code) { global $parsers; if(isset($parsers[$name])) throw new Exception("Can't register parser with name '$name' because a parser with that name already exists."); $parsers[$name] = $parser_code; } function parse_page_source($source) { global $settings, $parsers; if(!isset($parsers[$settings->parser])) exit(page_renderer::render_main("Parsing error - $settings->sitename", "

Parsing some page source data failed. This is most likely because $settings->sitename has the parser setting set incorrectly. Please contact " . $settings->admindetails["name"] . ", your $settings->sitename Administrator.")); /* Not needed atm because escaping happens when saving, not when rendering * if($settings->clean_raw_html) $source = htmlentities($source, ENT_QUOTES | ENT_HTML5); */ return $parsers[$settings->parser]($source); } // Function to register a new proprocessor that will be executed just before // an edit is saved. $save_preprocessors = []; function register_save_preprocessor($func) { global $save_preprocessors; $save_preprocessors[] = $func; } ////////////////////////////////////////////////////////////////// // %next_module% // // Execute each module's code foreach($modules as $moduledata) { $moduledata["code"](); } // Make sure that the credits page exists if(!isset($actions->credits)) { exit(page_renderer::render_main("Error - $settings->$sitename", "

No credits page detected. The credits page is a required module!

")); } // Perform the appropriate action $action_name = $env->action; if(isset($actions->$action_name)) { $req_action_data = $actions->$action_name; $req_action_data(); } else { exit(page_renderer::render_main("Error - $settings->sitename", "

No action called " . strtolower($_GET["action"]) ." has been registered. Perhaps you are missing a module?

")); } ?>