action = $settings->defaultaction; $env->page = ""; $env->user = "Anonymous"; $env->is_logged_in = false; $env->is_admin = false; $env->storage_prefix = $settings->data_storage_dir . DIRECTORY_SEPARATOR; /// Paths /// $paths = new stdClass(); $paths->pageindex = "pageindex.json"; // The pageindex $paths->searchindex = "invindex.json"; // The inverted index used for searching $paths->idindex = "idindex.json"; // The index that converts ids to page names // Prepend the storage data directory to all the defined paths. foreach ($paths as &$path) { $path = $env->storage_prefix . $path; } $paths->upload_file_prefix = "Files/"; // The prefix to append to uploaded files 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 ////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /** * Converts a filesize into a human-readable string. * From http://php.net/manual/en/function.filesize.php#106569 * Edited by Starbeamrainbowlabs. * @param number $bytes The number of bytes to convert. * @param number $decimals The number of decimal places to preserve. * @return string A human-readable filesize. */ function human_filesize($bytes, $decimals = 2) { $sz = ["b", "kb", "mb", "gb", "tb", "pb", "eb", "yb", "zb"]; $factor = floor((strlen($bytes) - 1) / 3); $result = round($bytes / pow(1024, $factor), $decimals); return $result . @$sz[$factor]; } /** * Calculates the time sincce a particular timestamp and returns a * human-readable result. * From http://goo.gl/zpgLgq. * @param integer $time The timestamp to convert. * @return string The time since the given timestamp as * 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'; } } /** * A recursive glob() function. * From http://in.php.net/manual/en/function.glob.php#106595 * @param string $pattern The glob pattern to use to find filenames. * @param integer $flags The glob flags to use when finding filenames. * @return array An array of the filepaths that match the given glob. */ 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; } /** * Gets a list of all the sub pages of the current page. * @param object $pageindex The pageindex to use to search. * @param string $pagename The name of the page to list the sub pages of. * @return object An object 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; } /** * 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, $paths; // Save the new pageindex and return if there aren't any more parent pages to check if(strpos($pagename, "/") === false) { file_put_contents($paths->pageindex, 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); } /** * Makes a path safe. * Paths may only contain alphanumeric characters, spaces, underscores, and * dashes. * @param string $string The string to make safe. * @return string A safe version of the given string. */ function makepathsafe($string) { $string = preg_replace("/[^0-9a-zA-Z\_\-\ \/\.]/i", "", $string); $string = preg_replace("/\.+/", ".", $string); return $string; } /** * Hides an email address from bots by adding random html entities. * @param string $str The original email address * @return string 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; } /** * Checks to see if $haystack starts with $needle. * @param string $haystack The string to search. * @param string $needle The string to search for at the beginning * of $haystack. * @return 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); } /** * Case-insensitively finds all occurrences of $needle in $haystack. Handles * UTF-8 characters correctly. * From http://www.pontikis.net/tip/?id=16, and * based on http://www.php.net/manual/en/function.strpos.php#87061 * @param string $haystack The string to search. * @param string $needle The string to find. * @return array || false An array of match indices, or false if * nothing was found. */ function mb_stripos_all($haystack, $needle) { $s = 0; $i = 0; while(is_integer($i)) { $i = function_exists("mb_stripos") ? mb_stripos($haystack, $needle, $s) : stripos($haystack, $needle, $s); if(is_integer($i)) { $aStrPos[] = $i; $s = $i + (function_exists("mb_strlen") ? mb_strlen($needle) : strlen($needle)); } } if(isset($aStrPos)) return $aStrPos; else return false; } /** * Returns the system's mime type mappings, considering the first extension * listed to be cacnonical. * @return array An array of mime type mappings. */ function system_mime_type_extensions() { global $settings; $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; } /** * Converts a given mime type to it's associated file extension. * @param string $type The mime type to convert. * @return string The extension for the given mime type. */ function system_mime_type_extension($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. Create it if it doesn't exist, and load + parse it * if it does. */ if(!file_exists($paths->pageindex)) { $glob_str = $env->storage_prefix . "*.md"; $existingpages = glob_recursive($glob_str); // Debug statements. Uncomment when debugging the pageindex regenerator. // var_dump($env->storage_prefix); // var_dump($glob_str); // var_dump($existingpages); $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(substr( // Store the filename, whilst trimming the storage prefix $pagefilename, strlen(preg_replace("/^\.\//i", "", $env->storage_prefix)) // glob_recursive trim the ./ from returned filenames , so we need to as well )); // Remove the `./` from the beginning if it's still hanging around if(substr($newentry->filename, 0, 2) == "./") $newentry->filename = substr($newentry->filename, 2); $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($newentry->filename, 0, -3)); if(file_exists($env->storage_prefix . $pagekey) && // If it exists... !is_dir($env->storage_prefix . $pagekey)) // ...and isn't a directory { // This page (potentially) has an associated file! // Let's investigate. // Blindly add the file to the pageindex for now. // Future We might want to do a security check on the file later on. // File a bug if you think we should do this. $newentry->uploadedfile = true; // Yes this page does have an uploaded file associated with it $newentry->uploadedfilepath = $pagekey; // It's stored here // Work out what kind of file it really is $mimechecker = finfo_open(FILEINFO_MIME_TYPE); $newentry->uploadedfilemime = finfo_file($mimechecker, $env->storage_prefix . $pagekey); } // Debug statements. Uncomment when debugging the pageindex regenerator. // echo("pagekey: "); // var_dump($pagekey); // echo("newentry: "); // var_dump($newentry); // 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 = "$env->storage_prefix$subpage_parent_key.md"; if(array_search($subpage_parent_filename, $existingpages) === false) { // Our parent page doesn't actually 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($paths->pageindex, json_encode($pageindex, JSON_PRETTY_PRINT)); unset($existingpages); } else { $pageindex_read_start = microtime(true); $pageindex = json_decode(file_get_contents($paths->pageindex)); header("x-pageindex-decode-time: " . round(microtime(true) - $pageindex_read_start, 6) . "ms"); } ////////////////////////// ///// Page id system ///// ////////////////////////// if(!file_exists($paths->idindex)) file_put_contents($paths->idindex, "{}"); $idindex = json_decode(file_get_contents($paths->idindex)); class ids { /* * @summary Gets the page id associated with the given pagename. */ public static function getid($pagename) { global $idindex; foreach ($idindex as $id => $entry) { if($entry == $pagename) return $id; } // This pagename doesn't have an id - assign it one quick! return self::assign($pagename); } /* * @summary Gets the page name associated with the given page id. */ public static function getpagename($id) { global $idindex; if(!isset($idindex->$id)) return false; else return $idindex->$id; } /* * @summary Moves a page in the id index from $oldpagename to $newpagename. * Note that this function doesn't perform any special checks to * make sure that the destination name doesn't already exist. */ public static function movepagename($oldpagename, $newpagename) { global $idindex, $paths; $pageid = self::getid($oldpagename); $idindex->$pageid = $newpagename; file_put_contents($paths->idindex, json_encode($idindex)); } /* * @summary Removes the given page name from the id index. Note that this * function doesn't handle multiple entries with the same name. */ public static function deletepagename($pagename) { global $idindex, $paths; // Get the id of the specified page $pageid = self::getid($pagename); // Remove it from the pageindex unset($idindex->$pageid); // Save the id index file_put_contents($paths->idindex, json_encode($idindex)); } /* * @summary Assigns an id to a pagename. Doesn't check to make sure that * pagename doesn't exist in the pageindex. */ protected static function assign($pagename) { global $idindex, $paths; $nextid = count(array_keys(get_object_vars($idindex))); if(isset($idindex->$nextid)) throw new Exception("The pageid is corrupt! Pepperminty Wiki generated the id $nextid, but that id is already in use."); // Update the id index $idindex->$nextid = utf8_encode($pagename); // Save the id index file_put_contents($paths->idindex, json_encode($idindex)); return $nextid; } } ////////////////////////// ////////////////////////// // 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(); } // Finish setting up the environment object $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_header_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, /// Secondary Parts /// "{content}" => $content, "{title}" => $title, ]; // 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([ ], [ ], $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_header_html() { global $settings; $result = self::get_css_as_html(); if(!empty($settings->enable_math_rendering)) $result .= " "; return $result; } public static function get_css_as_html() { global $settings; if(preg_match("/^[^\/]*\/\/|^\//", $settings->css)) return ""; else return ""; } public static $nav_divider = " | "; /** * Renders a navigation bar from an array of links. See * $settings->nav_links for format information. * @param array $nav_links The links to add to the navigation bar. * @param array $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; $arrayPageIndex = get_object_vars($pageindex); ksort($arrayPageIndex); $result = "\n"; foreach($arrayPageIndex as $pagename => $pagedetails) { $result .= "\t\t\t"; return $result; } } ////////////////////////////////////// ///// Extra consistency measures ///// ////////////////////////////////////// // Redirect to the search page if there isn't a page with the requested name if(!isset($pageindex->{$env->page}) and isset($_GET["search-redirect"])) { http_response_code(307); $url = "?action=search&query=" . rawurlencode($env->page); header("location: $url"); exit(page_renderer::render("Non existent page - $settings->sitename", "

There isn't a page on $settings->sitename with that name. However, you could search for this page name in other pages.

Alternatively, you could create this page.

")); } // Redirect the user to the login page if: // - A login is required to view this wiki // - The user isn't already requesting the login page // Note we use $_GET here because $env->action isn't populated at this point if($settings->require_login_view === true && // If this site requires a login in order to view pages !$env->is_logged_in && // And the user isn't logged in !in_array($_GET["action"], [ "login", "checklogin" ])) // And the user isn't trying to login { // Redirect the user to the login page http_response_code(307); $url = "?action=login&returnto=" . rawurlencode($_SERVER["REQUEST_URI"]) . "&required=true"; header("location: $url"); exit(page_renderer::render("Login required - $settings->sitename", "

$settings->sitename requires that you login before you are able to access it.

Login.

")); } ////////////////////////////////////// ////////////////////////////////////// ////////////////////////// /// 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; } /** * Checks to see whether a module with the given id exists. * @param string $id The id to search for. * @return bool Whether a module is currently loaded with the given id. */ function module_exists($id) { global $modules; foreach($modules as $module) { if($module["id"] == $id) return true; } return false; } // 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; } $parsers = [ "none" => function() { throw new Exception("No parser registered!"); } ]; /** * Registers a new parser. * @param string $name The name of the new parser to register. * @param function $parser_code The function to register as a new parser. */ 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 $save_preprocessors = []; /** * Register a new proprocessor that will be executed just before * an edit is saved. * @param function $func The function to register. */ function register_save_preprocessor($func) { global $save_preprocessors; $save_preprocessors[] = $func; } $help_sections = []; /** * Adds a new help section to the help page. * @param string $index The string to index the new section under. * @param string $title The title to display above the section. * @param string $content The content to display. */ function add_help_section($index, $title, $content) { global $help_sections; $help_sections[$index] = [ "title" => $title, "content" => $content ]; } if(!empty($settings->enable_math_rendering)) add_help_section("22-Mathematical-Expressions", "Methematical Expressions", "

$settings->sitename supports rendering of mathematical expressions. Mathematical expressions can be included practically anywhere in your page. Expressions should be written in LaTeX and enclosed in dollar signs like this: $x^2$.

Note that expression parsing is done on the viewer's computer with javascript (specifically MathJax) and not by $settings->sitename directly (also called client side rendering).

"); ////////////////////////////////////////////////////////////////// // %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?

")); } ?>