diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ecc03d9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lantern-build-engine"] + path = lantern-build-engine + url = https://gitlab.com/sbrl/lantern-build-engine.git diff --git a/Makefile b/Makefile deleted file mode 100644 index 4314a85..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -.DEFAULT_GOAL := peppermint - -.PHONY: setupApiDoc peppermint docs rest_docs module_api_docs - -ApiDocPresent := $(shell sh -c apidoc --help 1\>/dev/null && rm -rf doc/) - -peppermint: - @echo [peppermint/build] Rebuilding Pepperminty Wiki - php build.php - -docs: rest_docs module_api_docs - -rest_docs: setupApiDoc - @echo [peppermint/docs] Building docs - apidoc -o './docs/RestApi/' --config apidoc.json -f '.*\.php' -e 'index.php|ModuleApi' - rm -rf doc/ - -module_api_docs: phpdoc - @echo [peppermint/module api docs] Updating module api docs - php phpdoc run --directory . --target docs/ModuleApi --cache-folder docs/ModuleApiCache --ignore build/,php_error.php,Parsedown*,*.html --title "Pepperminty Wiki Module API" --visibility public - -phpdoc: - curl -L https://phpdoc.org/phpDocumentor.phar -o phpdoc - -setupApiDoc: - @echo [peppermint] Checking for apiDoc -ifndef ApiDocPresent - @echo [peppermint] Attempting to install ApiDoc, since it wasn't detected in your PATH - @echo [peppermint] Note that you may need to be root, and you'll need npm installed. - npm install apidoc --global -endif - @echo [peppermint] Check complete diff --git a/apidoc.json b/apidoc.json index e198fdc..c20e6ed 100644 --- a/apidoc.json +++ b/apidoc.json @@ -1,6 +1,6 @@ { "name": "Pepperminty Wiki", - "version": "0.13.0", + "version": "0.17.1", "description": "A wiki in a box. This is the API documentation.", "title": "Pepperminty Wiki (0.13-dev)" } diff --git a/build.bat b/build.bat deleted file mode 100644 index 7408eab..0000000 --- a/build.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -php build.php %* diff --git a/build.sh b/build.sh index ccf0156..3a6853a 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,147 @@ #!/usr/bin/env bash -php build.php $* +# Make sure the current directory is the location of this script to simplify matters +cd "$(dirname $(readlink -f $0))"; +################ +### Settings ### +################ + +# The name of this project +project_name="Pepperminty Wiki"; + +# The path to the lantern build engine git submodule +lantern_path="./lantern-build-engine"; + +### +# Custom Settings +### + +# Put any custom settings here. + +# The file to store the development server's PID in. +server_pid_file="/tmp/pepperminty-wiki-dev-server.pid"; + +############################################################################### + +# Check out the lantern git submodule if needed +if [ ! -d "${lantern_path}" ]; then git submodule update --init "${lantern_path}"; fi + +source "${lantern_path}/lantern.sh"; + +if [[ "$#" -lt 1 ]]; then + echo -e "${FBLE}${project_name}${RS} build script"; + echo -e " by Starbeamrainbowlabs"; + echo -e "${LC}Powered by the lantern build engine, v${version}${RS}"; + echo -e ""; + echo -e "${CSECTION}Usage${RS}"; + echo -e " ./build ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ..."; + echo -e ""; + echo -e "${CSECTION}Available actions${RS}"; + echo -e " ${CACTION}setup${RS} - Perform initial setup, check the environment (skip if only building Pepperminty Wiki itself)"; + echo -e " ${CACTION}build${RS} - Build Pepperminty Wiki"; + echo -e " ${CACTION}docs${RS} - Build the documentation"; + echo -e " ${CACTION}docs-livereload${RS} - Start the documentation livereload server"; + echo -e " ${CACTION}start-server${RS} - Start a development server"; + echo -e " ${CACTION}stop-server${RS} - Stop the development server"; + echo -e ""; + + exit 1; +fi + +############################################################################### + +function task_setup { + task_begin "Checking Environment"; + + check_command git true; + check_command npm true; + check_command npm true; + + task_end $?; + + task_begin "Initialising submodules"; + git submodule update --init; + task_end $?; + + task_begin "Installing packages"; + npm install; + task_end $?; + + task_begin "Creating build folders"; + mkdir -p build/_tmp; + echo "This folder contains build tools automatically downloaded." >build/_tmp/README.txt; + task_end $?; +} + +function task_build { + if [ -f "./build/index.php" ]; then + task_begin "Deleting old build result"; + rm build/index.php; + task_end "$?"; + fi + + task_begin "Building"; + php build.php + task_end $?; +} + +function task_docs { + task_begin "Building HTTP API Docs"; + node_modules/apidoc/bin/apidoc -o './docs/RestApi/' --config apidoc.json -f '.*\.php' -e 'index.php|ModuleApi' + exit_code="$?"; + rm -rf doc/; + task_end "${exit_code}"; + + task_begin "Building PHP Module API Docs"; + if [ ! -f "./build/_tmp/phpdoc" ]; then + subtask_begin "Downloading PHPDoc"; + curl -sSL https://phpdoc.org/phpDocumentor.phar -o ./build/_tmp/phpdoc + subtask_end $?; + fi + + php ./build/_tmp/phpdoc run \ + --directory . \ + --target docs/ModuleApi\ + --cache-folder docs/ModuleApiCache \ + --ignore build/,php_error.php,Parsedown*,*.html \ + --title "Pepperminty Wiki Module API" \ + --visibility public; + task_end $?; + + task_begin "Building Main Documentation"; + node_modules/docpress/bin/docpress build; + task_end $?; +} + +function task_docs-livereload { + task_begin "Starting Livereload Documentation Server"; + node_modules/docpress/bin/docpress serve; + task_end $?; +} + +function task_start-server { + task_begin "Starting Server"; + if [ -f "${server_pid_file}" ]; then + echo -e "${FRED}${HC}Error: A development server appears to be running already. Try running the 'stop-server' task before starting it again.${RS}"; + task_end 1; + fi + php -S [::]:35623 -t build/ & + exit_code=$?; pid=$!; + echo "${pid}" >"${server_pid_file}"; + task_end "${exit_code}"; + + task_begin "Opening Browser"; + sensible-browser [::]:35623; + task_end $?; +} + +function task_stop-server { + task_begin "Stopping Server"; + + kill "$(cat "${server_pid_file}")"; + rm "${server_pid_file}"; + + task_end $?; +} +############################################################################### + +tasks_run $@; diff --git a/build/index.php b/build/index.php index 84b86be..618f93e 100644 --- a/build/index.php +++ b/build/index.php @@ -1,8 +1,8 @@ -sessionprefix == "auto") $settings->sessionprefix = "pepperminty-wiki-" . preg_replace('/[^a-z0-9\-_]/', "-", strtolower($settings->sitename)); - - -///////////////////////////////////////////////////////////////////////////// -////// Do not edit below this line unless you know what you are doing! ////// -///////////////////////////////////////////////////////////////////////////// -/** The version of Pepperminty Wiki currently running. */ -$version = "v0.18-dev"; -$commit = "3a2d74227d0df78f17bb8f4da7f390a8ed6e45d2"; -/// Environment /// -/** Holds information about the current request environment. */ -$env = new stdClass(); -/** The action requested by the user. */ -$env->action = $settings->defaultaction; -/** The page name requested by the remote client. */ -$env->page = ""; -/** The filename that the page is stored in. */ -$env->page_filename = ""; -/** Whether we are looking at a history revision. */ -$env->is_history_revision = false; -/** An object holding history revision information for the current request */ -$env->history = new stdClass(); -/** The revision number requested of the current page */ -$env->history->revision_number = -1; -/** The revision data object from the page index for the requested revision */ -$env->history->revision_data = false; -/** The user's name if they are logged in. Defaults to `$settings->anonymous_user_name` if the user isn't currently logged in. @var string */ -$env->user = $settings->anonymous_user_name; -/** Whether the user is logged in */ -$env->is_logged_in = false; -/** Whether the user is an admin (moderator) @todo Refactor this to is_moderator, so that is_admin can be for the server owner. */ -$env->is_admin = false; -/** The currently logged in user's data. Please see $settings->users->username if you need to edit this - this is here for convenience :-) */ -$env->user_data = new stdClass(); -/** The data storage directory. Page filenames should be prefixed with this if you want their content. */ -$env->storage_prefix = $settings->data_storage_dir . DIRECTORY_SEPARATOR; -/** Contains performance data statistics for the current request. */ -$env->perfdata = new stdClass(); -/// Paths /// -/** - * Contains a bunch of useful paths to various important files. - * None of these need to be prefixed with `$env->storage_prefix`. - */ -$paths = new stdClass(); -/** The pageindex. Contains extensive information about all pages currently in this wiki. Individual entries for pages may be extended with arbitrary properties. */ -$paths->pageindex = "pageindex.json"; -/** The inverted index used for searching. Use the `search` class to interact with this - otherwise your brain might explode :P */ -$paths->searchindex = "invindex.json"; -/** The index that maps ids to page names. Use the `ids` class to interact with it :-) */ -$paths->idindex = "idindex.json"; -/** The cache of the most recently calculated statistics. */ -$paths->statsindex = "statsindex.json"; - -// Prepend the storage data directory to all the defined paths. -foreach ($paths as &$path) { - $path = $env->storage_prefix . $path; -} - -/** The master settings file @var string */ -$paths->settings_file = $settingsFilename; -/** The prefix to add to uploaded files */ -$paths->upload_file_prefix = "Files/"; - -session_start(); -// Make sure that the login cookie lasts beyond the end of the user's session -setcookie(session_name(), session_id(), time() + $settings->sessionlifetime, "", "", false, true); -///////// Login System ///////// -// Clear expired sessions -if(isset($_SESSION[$settings->sessionprefix . "-expiretime"]) and - $_SESSION[$settings->sessionprefix . "-expiretime"] < time()) -{ - // Clear the session variables - $_SESSION = []; - session_destroy(); -} - -if(isset($_SESSION[$settings->sessionprefix . "-user"]) and - isset($_SESSION[$settings->sessionprefix . "-pass"])) -{ - // Grab the session variables - $env->user = $_SESSION[$settings->sessionprefix . "-user"]; - - // The user is logged in - $env->is_logged_in = true; - $env->user_data = $settings->users->{$env->user}; - -} - -// 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 /////// - -//////////////////// -// APIDoc strings // -//////////////////// -/** - * @apiDefine Admin Only the wiki administrator may use this call. - */ -/** - * @apiDefine Moderator Only users loggged with a moderator account may use this call. - */ -/** - * @apiDefine User Only users loggged in may use this call. - */ -/** - * @apiDefine Anonymous Anybody may use this call. - */ -/** - * @apiDefine UserNotLoggedInError - * @apiError UserNotLoggedInError You didn't log in before sending this request. - */ -/** -* @apiDefine UserNotModeratorError -* @apiError UserNotModeratorError You weren't loggged in as a moderator before sending this request. -*/ -/** -* @apiDefine PageParameter -* @apiParam {string} page The page to operate on. -*/ -//////////////////// - -/////////////////////////////////////////////////////////////////////////////// -////////////////////////////////// Functions ////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// -/** - * Get the actual absolute origin of the request sent by the user. - * @package core - * @param array $s The $_SERVER variable contents. Defaults to $_SERVER. - * @param bool $use_forwarded_host Whether to utilise the X-Forwarded-Host header when calculating the actual origin. - * @return string The actual origin of the user's request. - */ -function url_origin( $s = false, $use_forwarded_host = false ) -{ - if($s === false) $s = $_SERVER; - $ssl = ( ! empty( $s['HTTPS'] ) && $s['HTTPS'] == 'on' ); - $sp = strtolower( $s['SERVER_PROTOCOL'] ); - $protocol = substr( $sp, 0, strpos( $sp, '/' ) ) . ( ( $ssl ) ? 's' : '' ); - $port = $s['SERVER_PORT']; - $port = ( ( ! $ssl && $port=='80' ) || ( $ssl && $port=='443' ) ) ? '' : ':'.$port; - $host = ( $use_forwarded_host && isset( $s['HTTP_X_FORWARDED_HOST'] ) ) ? $s['HTTP_X_FORWARDED_HOST'] : ( isset( $s['HTTP_HOST'] ) ? $s['HTTP_HOST'] : null ); - $host = isset( $host ) ? $host : $s['SERVER_NAME'] . $port; - return $protocol . '://' . $host; -} - -/** - * Get the full url, as requested by the client. - * @package core - * @see http://stackoverflow.com/a/8891890/1460422 This Stackoverflow answer. - * @param array $s The $_SERVER variable. Defaults to $_SERVER. - * @param bool $use_forwarded_host Whether to take the X-Forwarded-Host header into account. - * @return string The full url, as requested by the client. - */ -function full_url( $s = false, $use_forwarded_host = false ) -{ - if($s == false) $s = $_SERVER; - return url_origin( $s, $use_forwarded_host ) . $s['REQUEST_URI']; -} - -/** - * Converts a filesize into a human-readable string. - * @package core - * @see http://php.net/manual/en/function.filesize.php#106569 The original source - * @author rommel - * @author 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 since a particular timestamp and returns a - * human-readable result. - * @package core - * @see http://goo.gl/zpgLgq The original source. No longer exists, maybe the wayback machine caught it :-( - * @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) -{ - return human_time(time() - $time); -} -/** - * Renders a given number of seconds as something that humans can understand more easily. - * @package core - * @param int $seconds The number of seconds to render. - * @return string The rendered time. - */ -function human_time($seconds) + + +///////////////////////////////////////////////////////////////////////////// +////// Do not edit below this line unless you know what you are doing! ////// +///////////////////////////////////////////////////////////////////////////// +/** The version of Pepperminty Wiki currently running. */ +$version = "v0.18-dev"; +$commit = "905e970dc09d1f8b195389d60a958538ba22c37b"; +/// Environment /// +/** Holds information about the current request environment. */ +$env = new stdClass(); +/** The action requested by the user. */ +$env->action = $settings->defaultaction; +/** The page name requested by the remote client. */ +$env->page = ""; +/** The filename that the page is stored in. */ +$env->page_filename = ""; +/** Whether we are looking at a history revision. */ +$env->is_history_revision = false; +/** An object holding history revision information for the current request */ +$env->history = new stdClass(); +/** The revision number requested of the current page */ +$env->history->revision_number = -1; +/** The revision data object from the page index for the requested revision */ +$env->history->revision_data = false; +/** The user's name if they are logged in. Defaults to `$settings->anonymous_user_name` if the user isn't currently logged in. @var string */ +$env->user = $settings->anonymous_user_name; +/** Whether the user is logged in */ +$env->is_logged_in = false; +/** Whether the user is an admin (moderator) @todo Refactor this to is_moderator, so that is_admin can be for the server owner. */ +$env->is_admin = false; +/** The currently logged in user's data. Please see $settings->users->username if you need to edit this - this is here for convenience :-) */ +$env->user_data = new stdClass(); +/** The data storage directory. Page filenames should be prefixed with this if you want their content. */ +$env->storage_prefix = $settings->data_storage_dir . DIRECTORY_SEPARATOR; +/** Contains performance data statistics for the current request. */ +$env->perfdata = new stdClass(); +/// Paths /// +/** + * Contains a bunch of useful paths to various important files. + * None of these need to be prefixed with `$env->storage_prefix`. + */ +$paths = new stdClass(); +/** The pageindex. Contains extensive information about all pages currently in this wiki. Individual entries for pages may be extended with arbitrary properties. */ +$paths->pageindex = "pageindex.json"; +/** The inverted index used for searching. Use the `search` class to interact with this - otherwise your brain might explode :P */ +$paths->searchindex = "invindex.json"; +/** The index that maps ids to page names. Use the `ids` class to interact with it :-) */ +$paths->idindex = "idindex.json"; +/** The cache of the most recently calculated statistics. */ +$paths->statsindex = "statsindex.json"; + +// Prepend the storage data directory to all the defined paths. +foreach ($paths as &$path) { + $path = $env->storage_prefix . $path; +} + +/** The master settings file @var string */ +$paths->settings_file = $settingsFilename; +/** The prefix to add to uploaded files */ +$paths->upload_file_prefix = "Files/"; + +session_start(); +// Make sure that the login cookie lasts beyond the end of the user's session +setcookie(session_name(), session_id(), time() + $settings->sessionlifetime, "", "", false, true); +///////// Login System ///////// +// Clear expired sessions +if(isset($_SESSION[$settings->sessionprefix . "-expiretime"]) and + $_SESSION[$settings->sessionprefix . "-expiretime"] < time()) { - $tokens = array ( - 31536000 => 'year', - 2592000 => 'month', - 604800 => 'week', - 86400 => 'day', - 3600 => 'hour', - 60 => 'minute', - 1 => 'second' - ); - foreach ($tokens as $unit => $text) { - if ($seconds < $unit) continue; - $numberOfUnits = floor($seconds / $unit); - return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'').' ago'; - } -} - -/** - * A recursive glob() function. - * @package core - * @see http://in.php.net/manual/en/function.glob.php#106595 The original source - * @author Mike - * @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 the name of the parent page to the specified page. - * @since 0.15 - * @package core - * @param string $pagename The child page to get the parent - * page name for. - * @return string|bool - */ -function get_page_parent($pagename) { - if(mb_strpos($pagename, "/") === false) - return false; - return mb_substr($pagename, 0, mb_strrpos($pagename, "/")); -} - -/** - * Gets a list of all the sub pages of the current page. - * @package core - * @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. - * @package core - * @param $pagename The pagename to check. - */ -function check_subpage_parents($pagename) -{ - global $pageindex, $paths, $env; - // 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($env->storage_prefix . $parent_page_filename)) - { - // This parent page doesn't exist! Create it and add it to the page index. - touch($env->storage_prefix . $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. - * @package core - * @param string $string The string to make safe. - * @return string A safe version of the given string. - */ -function makepathsafe($string) -{ - // Old restrictive system - //$string = preg_replace("/[^0-9a-zA-Z\_\-\ \/\.]/i", "", $string); - // Remove reserved characters - $string = preg_replace("/[?%*:|\"><()\\[\\]]/i", "", $string); - // Collapse multiple dots into a single dot - $string = preg_replace("/\.+/", ".", $string); - // Don't allow slashes at the beginning - $string = ltrim($string, "\\/"); - return $string; -} - -/** - * Hides an email address from bots by adding random html entities. - * @todo Make this more clevererer :D - * @package core - * @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. - * @package core - * @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. - * @package core - * @see http://www.pontikis.net/tip/?id=16 the source - * @see http://www.php.net/manual/en/function.strpos.php#87061 the source that the above was based on - * @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 = mb_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; -} - -/** - * Tests whether a string starts with a specified substring. - * @package core - * @param string $haystack The string to check against. - * @param string $needle The substring to look for. - * @return bool Whether the string starts with the specified substring. - */ -function startsWith($haystack, $needle) { - return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false; -} -/** - * Tests whether a string ends with a given substring. - * @package core - * @param string $whole The string to test against. - * @param string $end The substring test for. - * @return bool Whether $whole ends in $end. - */ -function endsWith($whole, $end) -{ - return (strpos($whole, $end, strlen($whole) - strlen($end)) !== false); -} -/** - * Replaces the first occurrence of $find with $replace. - * @package core - * @param string $find The string to search for. - * @param string $replace The string to replace the search string with. - * @param string $subject The string ot perform the search and replace on. - * @return string The source string after the find and replace has been performed. - */ -function str_replace_once($find, $replace, $subject) -{ - $index = strpos($subject, $find); - if($index !== false) - return substr_replace($subject, $replace, $index, strlen($find)); - return $subject; -} - -/** - * Returns the system's mime type mappings, considering the first extension - * listed to be cacnonical. - * @package core - * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer - * @author chaos - * @author Edited by Starbeamrainbowlabs - * @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. - * @package core - * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer - * @author chaos - * @author Edited by Starbeamrainbowlabs - * @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; -} - -/** - * Returns the system MIME type mapping of extensions to MIME types. - * @package core - * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer - * @author chaos - * @author Edited by Starbeamrainbowlabs - * @return array An array mapping file extensions to their associated mime types. - */ -function system_extension_mime_types() -{ - 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); - foreach($parts as $part) - $out[$part] = $type; - } - fclose($file); - return $out; -} -/** - * Converts a given file extension to it's associated mime type. - * @package core - * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer - * @author chaos - * @author Edited by Starbeamrainbowlabs - * @param string $ext The extension to convert. - * @return string The mime type associated with the given extension. - */ -function system_extension_mime_type($ext) { - static $types; - if(!isset($types)) - $types = system_extension_mime_types(); - $ext = strtolower($ext); - return isset($types[$ext]) ? $types[$ext] : null; -} - -/** - * Generates a stack trace. - * @package core - * @param bool $log_trace Whether to send the stack trace to the error log. - * @param bool $full Whether to output a full description of all the variables involved. - * @return string A string prepresentation of a stack trace. - */ -function stack_trace($log_trace = true, $full = false) -{ - $result = ""; - $stackTrace = debug_backtrace(); - $stackHeight = count($stackTrace); - foreach ($stackTrace as $i => $stackEntry) - { - $result .= "#" . ($stackHeight - $i) . ": "; - $result .= (isset($stackEntry["file"]) ? $stackEntry["file"] : "(unknown file)") . ":" . (isset($stackEntry["line"]) ? $stackEntry["line"] : "(unknown line)") . " - "; - if(isset($stackEntry["function"])) - { - $result .= "(calling " . $stackEntry["function"]; - if(isset($stackEntry["args"]) && count($stackEntry["args"])) - { - $result .= ": "; - $result .= implode(", ", array_map($full ? "var_dump_ret" : "var_dump_short", $stackEntry["args"])); - } - } - $result .= ")\n"; - } - if($log_trace) - error_log($result); - return $result; -} -/** - * Calls var_dump() and returns the output. - * @package core - * @param mixed $var The thing to pass to var_dump(). - * @return string The output captured from var_dump(). - */ -function var_dump_ret($var) -{ - ob_start(); - var_dump($var); - return ob_get_clean(); -} - -/** - * Calls var_dump(), shortening the output for various types. - * @package core - * @param mixed $var The thing to pass to var_dump(). - * @return string A shortened version of the var_dump() output. - */ -function var_dump_short($var) -{ - $result = trim(var_dump_ret($var)); - if(substr($result, 0, 6) === "object" || substr($result, 0, 5) === "array") - { - $result = substr($result, 0, strpos($result, " ")) . " { ... }"; - } - return $result; -} - -if (!function_exists('getallheaders')) { - /** - * Polyfill for PHP's native getallheaders() function on platforms that - * don't have it. - * @package core - * @todo Identify which platforms don't have it and whether we still need this - */ - function getallheaders() - { - if (!is_array($_SERVER)) - return []; - - $headers = array(); - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } - return $headers; - } -} -/** - * Renders a timestamp in HTML. - * @package core - * @param int $timestamp The timestamp to render. - * @return string HTML representing the given timestamp. - */ -function render_timestamp($timestamp) -{ - return ""; -} -/** - * Renders a page name in HTML. - * @package core - * @param object $rchange The recent change to render as a page name - * @return string HTML representing the name of the given page. - */ -function render_pagename($rchange) -{ - global $pageindex; - $pageDisplayName = $rchange->page; - if(isset($pageindex->$pageDisplayName) and !empty($pageindex->$pageDisplayName->redirect)) - $pageDisplayName = "$pageDisplayName"; - $pageDisplayLink = "$pageDisplayName"; - return $pageDisplayName; -} -/** - * Renders an editor's or a group of editors name(s) in HTML. - * @package core - * @param string $editorName The name of the editor to render. - * @return string HTML representing the given editor's name. - */ -function render_editor($editorName) -{ - return "✎ $editorName"; -} - -/** - * Saves the settings file back to peppermint.json. - * @return bool Whether the settings were saved successfully. - */ -function save_settings() { - global $paths, $settings; - return file_put_contents($paths->settings_file, json_encode($settings, JSON_PRETTY_PRINT)) !== false; -} - -/** - * Saves the currently logged in user's data back to peppermint.json. - * @package core - * @return bool Whether the user's data was saved successfully. Returns false if the user isn't logged in. - */ -function save_userdata() -{ - global $env, $settings, $paths; - - if(!$env->is_logged_in) - return false; - - $settings->users->{$env->user} = $env->user_data; - - return save_settings(); -} - -/** - * Figures out the path to the user page for a given username. - * Does not check to make sure the user acutally exists. - * @package core - * @param string $username The username to get the path to their user page for. - * @return string The path to the given user's page. - */ -function get_user_pagename($username) { - global $settings; - return "$settings->user_page_prefix/$username"; -} -/** - * Extracts a username from a user page path. - * @package core - * @param string $userPagename The suer page path to extract from. - * @return string The name of the user that the user page belongs to. - */ -function extract_user_from_userpage($userPagename) { - global $settings; - $matches = []; - preg_match("/$settings->user_page_prefix\\/([^\\/]+)\\/?/", $userPagename, $matches); - - return $matches[1]; -} - -/** - * Sends a plain text email to a user, replacing {username} with the specified username. - * @package core - * @param string $username The username to send the email to. - * @param string $subject The subject of the email. - * @param string $body The body of the email. - * @return boolean Whether the email was sent successfully or not. Currently, this may fail if the user doesn't have a registered email address. - */ -function email_user($username, $subject, $body) -{ - global $version, $settings; - - // If the user doesn't have an email address, then we can't email them :P - if(empty($settings->users->{$username}->emailAddress)) - return false; - - $subject = str_replace("{username}", $username, $subject); - $body = str_replace("{username}", $username, $body); - - $headers = [ - "content-type" => "text/plain", - "x-mailer" => "$settings->sitename Pepperminty-Wiki/$version PHP/" . phpversion(), - "reply-to" => "$settings->admindetails_name <$settings->admindetails_email>" - ]; - $compiled_headers = ""; - foreach($headers as $header => $value) - $compiled_headers .= "$header: $value\r\n"; - - return mail($settings->users->{$username}->emailAddress, $subject, $body, $compiled_headers, "-t"); -} -/** - * Sends a plain text email to a list of users, replacing {username} with each user's name. - * @package core - * @param string[] $usernames A list of usernames to email. - * @param string $subject The subject of the email. - * @param string $body The body of the email. - * @return integer The number of emails sent successfully. - */ -function email_users($usernames, $subject, $body) -{ - $emailsSent = 0; - foreach($usernames as $username) - { - $emailsSent += email_user($username, $subject, $body) ? 1 : 0; - } - return $emailsSent; -} - -/////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////// -////////////////////// 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 = substr( // Store the filename, whilst trimming the storage prefix - $pagefilename, - mb_strlen(preg_replace("/^\.\//iu", "", $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 = "unknown"; // Set the editor to "unknown" - // Extract the name of the (sub)page without the ".md" - $pagekey = mb_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)); - $env->perfdata->pageindex_decode_time = round((microtime(true) - $pageindex_read_start)*1000, 3); - header("x-pageindex-decode-time: " . $env->perfdata->pageindex_decode_time . "ms"); -} - -////////////////////////// -///// Page id system ///// -////////////////////////// -if(!file_exists($paths->idindex)) - file_put_contents($paths->idindex, "{}"); -$idindex_decode_start = microtime(true); -$idindex = json_decode(file_get_contents($paths->idindex)); -$env->perfdata->idindex_decode_time = round((microtime(true) - $idindex_decode_start)*1000, 3); -/** - * Provides an interface to interact with page ids. - * @package core - */ -class ids -{ - /** - * Gets the page id associated with the given page name. - * If it doesn't exist in the id index, it will be added. - * @package core - * @param string $pagename The name of the page to fetch the id for. - * @return integer The id for the specified page name. - */ - public static function getid($pagename) - { - global $idindex; - - $pagename_norm = Normalizer::normalize($pagename, Normalizer::FORM_C); - foreach ($idindex as $id => $entry) - { - // We don't need to normalise here because we normralise when assigning ids - if($entry == $pagename_norm) - return $id; - } - - // This pagename doesn't have an id - assign it one quick! - return self::assign($pagename); - } - - /** - * Gets the page name associated with the given page id. - * Be warned that if the id index is cleared (e.g. when the search index is - * rebuilt from scratch), the id associated with a page name may change! - * @package core - * @param int $id The id to fetch the page name for. - * @return string The page name currently associated with the specified id. - */ - public static function getpagename($id) - { - global $idindex; - - if(!isset($idindex->$id)) - return false; - else - return $idindex->$id; - } - - /** - * 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. - * @package core - * @param string $oldpagename The old page name to move. - * @param string $newpagename The new page name to move the old page name to. - */ - public static function movepagename($oldpagename, $newpagename) - { - global $idindex, $paths; - - $pageid = self::getid(Normalizer::normalize($oldpagename, Normalizer::FORM_C)); - $idindex->$pageid = Normalizer::normalize($newpagename, Normalizer::FORM_C); - - file_put_contents($paths->idindex, json_encode($idindex)); - } - - /** - * Removes the given page name from the id index. - * Note that this function doesn't handle multiple entries with the same - * name. Also note that it may get re-added during a search reindex if the - * page still exists. - * @package core - * @param string $pagename The page name to delete from the id index. - */ - 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)); - } - - /** - * Clears the id index completely. - * Will break the inverted search index! Make sure you rebuild the search - * index (if the search module is installed, of course) if you want search - * to still work. Of course, note that will re-add all the pages to the id - * index. - * @package core - */ - public static function clear() - { - global $paths, $idindex; - // Delete the old id index - unlink($paths->idindex); - // Create the new id index - file_put_contents($paths->idindex, "{}"); - // Reset the in-memory id index - $idindex = new stdClass(); - } - - /** - * Assigns an id to a pagename. Doesn't check to make sure that - * pagename doesn't already exist in the id index. - * @package core - * @param string $pagename The page name to assign an id to. - * @return integer The id assigned to the specified page name. - */ - protected static function assign($pagename) - { - global $idindex, $paths; - - $pagename = Normalizer::normalize($pagename, Normalizer::FORM_C); - - $nextid = count(array_keys(get_object_vars($idindex))); - // Increment the generated id until it's unique - while(isset($idindex->nextid)) - $nextid++; - - // Update the id index - $idindex->$nextid = $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"]) and basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) == "index.php") -{ - http_response_code(302); - header("location: " . dirname(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))); - exit(); -} - -// Make sure that the action is set -if(empty($_GET["action"])) - $_GET["action"] = $settings->defaultaction; -// Make sure that the page is set -if(empty($_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(); -} - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////// HTML fragments //////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -/** - * Renders the HTML page that is sent to the client. - * @package core - */ -class page_renderer -{ - /** - * The root HTML template that all pages are built from. - * @var string - * @package core - */ - public static $html_template = " - - - - {title} - - - - - {header-html} - - - {body} - - - -"; - /** - * The main content template that is used to render normal wiki pages. - * @var string - * @package core - */ - public static $main_content_template = "{navigation-bar} -

{sitename}

-
- {content} -
- {extra} - - {navigation-bar-bottom} - {all-pages-datalist}"; - /** - * A specially minified content template that doesn't include the navbar and - * other elements not suitable for printing. - * @var string - * @package core - */ - public static $minimal_content_template = "
{content}
- "; - - /** - * An array of items indicating the resources to ask the web server to push - * down to the client with HTTP/2.0 server push. - * Format: [ [type, path], [type, path], .... ] - * @var array[] - */ - protected static $http2_push_items = []; - - /** - * 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. - * @var array - * @package core - */ - protected static $part_processors = []; - - /** - * Registers a function as a part post processor. - * This function's use is more complicated to explain. Pepperminty Wiki - * renders pages with a very simple templating system. For example, in the - * template a page's content is denoted by `{content}`. A function - * registered here will be passed all the components of a page _just_ - * before they are dropped into the template. Note that the function you - * pass in here should take a *reference* to the components, as the return - * value of the function passed is discarded. - * @package core - * @param function $function The part preprocessor to register. - */ - 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; - } - - /** - * Renders a HTML page with the content specified. - * @package core - * @param string $title The title of the page. - * @param string $content The (HTML) content of the page. - * @param boolean $body_template The HTML content template to use. - * @return string The rendered HTML, ready to send to the client :-) - */ - 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."); - } - } - - // Push the logo via HTTP/2.0 if possible - if($settings->favicon[0] === "/") self::$http2_push_items[] = ["image", $settings->favicon]; - - $parts = [ - "{body}" => $body_template, - - "{sitename}" => $logo_html, - "v0.18-dev" => $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(", ", array_map(function($username) { return page_renderer::render_username($username); }, $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, - "{extra}" => "", - "{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("{generation-time-taken}", round((microtime(true) - $start_time)*1000, 2), $result); - // Send the HTTP/2.0 server push indicators if possible - but not if we're sending a redirect page - if(!headers_sent() && (http_response_code() < 300 || http_response_code() >= 400)) self::send_server_push_indicators(); - return $result; - } - /** - * Renders a normal HTML page. - * @package core - * @param string $title The title of the page. - * @param string $content The content of the page. - * @return string The rendered page. - */ - public static function render_main($title, $content) - { - return self::render($title, $content, self::$main_content_template); - } - /** - * Renders a minimal HTML page. Useful for printable pages. - * @package core - * @param string $title The title of the page. - * @param string $content The content of the page. - * @return string The rendered page. - */ - public static function render_minimal($title, $content) - { - return self::render($title, $content, self::$minimal_content_template); - } - - /** - * Sends the currently registered HTTP2 server push items to the client. - * @return integer|FALSE The number of resource hints included in the link: header, or false if server pushing is disabled. - */ - public static function send_server_push_indicators() { - global $settings; - if(!$settings->http2_server_push) - return false; - - // Render the preload directives - $link_header_parts = []; - foreach(self::$http2_push_items as $push_item) - $link_header_parts[] = "<{$push_item[1]}>; rel=preload; as={$push_item[0]}"; - - // Send them in a link: header - if(!empty($link_header_parts)) - header("link: " . implode(", ", $link_header_parts)); - - return count(self::$http2_push_items); - } - - /** - * Renders the header HTML. - * @package core - * @return string The rendered HTML that goes in the header. - */ - public static function get_header_html() - { - global $settings; - $result = self::get_css_as_html(); - $result .= self::getJS(); - - // We can't use module_exists here because sometimes global $modules - // hasn't populated yet when we get called O.o - if(class_exists("search")) - $result .= "\t\t\n"; - - if(!empty($settings->enable_math_rendering)) - { - $result .= ""; - } - - return $result; - } - /** - * Figures out whether $settings->css is a url, or a string of css. - * A url is something starting with "protocol://" or simply a "/". - * @return boolean True if it's a url - false if we assume it's a string of css. - */ - public static function is_css_url() { - global $settings; - return preg_match("/^[^\/]*\/\/|^\//", $settings->css); - } - /** - * Renders all the CSS as HTML. - * @package core - * @return string The css as HTML, ready to be included in the HTML header. - */ - public static function get_css_as_html() - { - global $settings, $defaultCSS; - - if(self::is_css_url()) { - if($settings->css[0] === "/") // Push it if it's a relative resource - self::AddServerPushIndicator("style", $settings->css); - return "\n"; - } else { - $css = $settings->css == "auto" ? $defaultCSS : $settings->css; - if(!empty($settings->optimize_pages)) - { - // CSS Minification ideas by Jean from catswhocode.com - // Link: http://www.catswhocode.com/blog/3-ways-to-compress-css-files-using-php - // Remove comments - $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', "", $css); - // Cut down whitespace - $css = preg_replace('/\s+/', " ", $css); - // Remove whitespace after colons and semicolons - $css = str_replace([ - " :", - ": ", - "; ", - " { ", - " } " - ], [ - ":", - ":", - ";", - "{", - "}" - ], $css); - - } - return "\n"; - } - } - /** - * The javascript snippets that will be included in the page. - * @var string[] - * @package core - */ - private static $jsSnippets = []; - /** - * The urls of the external javascript files that should be referenced - * by the page. - * @var string[] - * @package core - */ - private static $jsLinks = []; - /** - * Adds the specified url to a javascript file as a reference to the page. - * @package core - * @param string $scriptUrl The url of the javascript file to reference. - */ - public function AddJSLink(string $scriptUrl) - { - static::$jsLinks[] = $scriptUrl; - } - /** - * Adds a javascript snippet to the page. - * @package core - * @param string $script The snippet of javascript to add. - */ - public function AddJSSnippet(string $script) - { - static::$jsSnippets[] = $script; - } - /** - * Renders the included javascript header for inclusion in the final - * rendered page. - * @package core - * @return string The rendered javascript ready for inclusion in the page. - */ - private static function getJS() - { - $result = "\n"; - foreach(static::$jsSnippets as $snippet) - $result .= "\n"; - foreach(static::$jsLinks as $link) { - // Push it via HTTP/2.0 if it's relative - if($link[0] === "/") self::AddServerPushIndicator("script", $link); - $result .= "\n"; - } - return $result; - } - - // ~ - - /** - * Adds a resource to the list of items to indicate that the web server should send via HTTP/2.0 Server Push. - * Note: Only specify static files here, as you might end up with strange (and possibly dangerous) results! - * @param string $type The resource type. See https://fetch.spec.whatwg.org/#concept-request-destination for more information. - * @param string $path The *relative url path* to the resource. - */ - public static function AddServerPushIndicator($type, $path) { - self::$http2_push_items[] = [ $type, $path ]; - } - - // ~ - - /** - * The navigation bar divider. - * @package core - * @var string - */ - public static $nav_divider = " | "; - - /** - * Renders a navigation bar from an array of links. See - * $settings->nav_links for format information. - * @package core - * @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. - * @param string $class The class(es) to assign to the rendered - * navigation bar. - */ - public static function render_navigation_bar($nav_links, $nav_links_extra, $class = "") - { - global $settings, $env; - $result = "

"; - return $result; - } - /** - * Renders a username for inclusion in a page. - * @package core - * @param string $name The username to render. - * @return string The username rendered in HTML. - */ - public static function render_username($name) - { - global $settings; - $result = ""; - $result .= ""; - if($settings->avatars_show) - $result .= " "; - if(in_array($name, $settings->admins)) - $result .= $settings->admindisplaychar; - $result .= htmlentities($name); - $result .= ""; - - return $result; - } - - // ~ - - /** - * Renders the datalist for the search box as HTML. - * @package core - * @return string The search box datalist as HTML. - */ - public static function generate_all_pages_datalist() - { - global $settings, $pageindex; - $arrayPageIndex = get_object_vars($pageindex); - ksort($arrayPageIndex); - $result = "\n"; - - // If dynamic page sugggestions are enabled, then we should send a loading message instead. - if($settings->dynamic_page_suggestion_count > 0) - { - $result .= ""; - - return $result; - } -} - -// HTTP/2.0 Server Push static items -foreach($settings->http2_server_push_items as $push_item) { - page_renderer::AddServerPushIndicator($push_item[0], $push_item[1]); -} - -// Math rendering support -if(!empty($settings->enable_math_rendering)) -{ - page_renderer::AddJSLink("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"); -} -// alt+enter support in the search box -page_renderer::AddJSSnippet('// Alt + Enter support in the top search box -window.addEventListener("load", function(event) { - document.querySelector("input[type=search]").addEventListener("keyup", function(event) { - // Listen for Alt + Enter - if(event.keyCode == 13 && event.altKey) { - event.stopPropagation(); - event.preventDefault(); - event.cancelBubble = true; - event.target.form.setAttribute("target", "_blank"); - event.target.form.submit(); - event.target.form.removeAttribute("target"); - return false; // Required by some browsers - } - }); -}); -'); - -/// Finish setting up the environment object /// -$env->page = $_GET["page"]; -if(isset($_GET["revision"]) and is_numeric($_GET["revision"])) -{ - // We have a revision number! - $env->is_history_revision = true; - $env->history->revision_number = intval($_GET["revision"]); - - // Make sure that the revision exists for later on - if(!isset($pageindex->{$env->page}->history[$env->history->revision_number])) - { - http_response_code(404); - exit(page_renderer::render_main("404: Revision Not Found - $env->page - $settings->sitename", "

Revision #{$env->history->revision_number} of $env->page doesn't appear to exist. Try viewing the list of revisions for $env->page, or viewing the latest revision instead.

")); - } - - $env->history->revision_data = $pageindex->{$env->page}->history[$env->history->revision_number]; -} -// Construct the page's filename -$env->page_filename = $env->storage_prefix; -if($env->is_history_revision) - $env->page_filename .= $pageindex->{$env->page}->history[$env->history->revision_number]->filename; -else if(isset($pageindex->{$env->page})) - $env->page_filename .= $pageindex->{$env->page}->filename; - -$env->action = strtolower($_GET["action"]); - -//////////////////////////////////////////////// - -////////////////////////////////////// -///// Extra consistency measures ///// -////////////////////////////////////// - -// CHANGED: The search redirector has now been moved to below the module registration system, as it was causing a warning here - -// 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", "opensearch-description", "invindex-rebuild", "stats-update" ])) // And the user isn't trying to login, or get the opensearch description, or access actions that apply their own access rules -{ - // Redirect the user to the login page - http_response_code(307); - header("x-login-required: yes"); - $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.

")); -} -////////////////////////////////////// -////////////////////////////////////// - -$remote_files = []; -/** - * Registers a request for a remote file to be downloaded before execution. Will block until all files are downloaded. - * Example definition: - * [ "local_filename" => "file.ext", "remote_url": "https://example.com" ] - * @param array $remote_file_def The remote file definition to register. - * @throws Exception Exception Throws an exception if a definition for the requested local file already exists. - */ -function register_remote_file($remote_file_def) { - global $remote_files; - - foreach($remote_files as $ex_remote_file_def) { - if($ex_remote_file_def["local_filename"] == $remote_file_def["local_filename"]) - throw new Exception("Error: A remote file with the local filename '{$remote_file_def["local_filename"]}' is already registered."); - } - - $remote_files[] = $remote_file_def; -} - -////////////////////////// -/// Module functions /// -////////////////////////// -// These functions are // -// used by modules to // -// register themselves // -// or new pages. // -////////////////////////// -/** A list of all the currently loaded modules. Not guaranteed to be populated until an action is executed. */ -$modules = []; -/** - * Registers a module. - * @package core - * @param array $moduledata The module data to register. - */ -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. - * @package core - * @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; -} - -$actions = new stdClass(); -/** - * Registers a new action handler. - * @package core - * @param string $action_name The action to register. - * @param function $func The function to call when the specified - * action is requested. - */ -function add_action($action_name, $func) -{ - global $actions; - $actions->$action_name = $func; -} -/** - * Figures out whether a given action is currently registered. - * Only guaranteed to be accurate in inside an existing action function - * @package core - * @param string $action_name The name of the action to search for - * @return boolean Whether an action with the specified name exists. - */ -function has_action($action_name) -{ - global $actions; - return !empty($actions->$action_name); -} - -$parsers = [ - "none" => function() { - throw new Exception("No parser registered!"); - } -]; -/** - * Registers a new parser. - * @package core - * @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; -} -/** - * Parses the specified page source using the parser specified in the settings - * into HTML. - * The specified parser may (though it's unilkely) render it to other things. - * @package core - * @param string $source The source to render. - * @return string The source rendered to HTML. - */ -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. - * @package core - * @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. - * @package core - * @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-mxpressions", "Mathematical 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).

"); - -/** An array of the currently registerd statistic calculators. Not guaranteed to be populated until the requested action function is called. */ -$statistic_calculators = []; -/** - * Registers a statistic calculator against the system. - * @package core - * @param array $stat_data The statistic object to register. - */ -function statistic_add($stat_data) { - global $statistic_calculators; - $statistic_calculators[$stat_data["id"]] = $stat_data; -} -/** - * Checks whether a specified statistic has been registered. - * @package core - * @param string $stat_id The id of the statistic to check the existence of. - * @return boolean Whether the specified statistic has been registered. - */ -function has_statistic($stat_id) { - global $statistic_calculators; - return !empty($statistic_calculators[$stat_id]); -} - -////////////////////////////////////////////////////////////////// - + // Clear the session variables + $_SESSION = []; + session_destroy(); +} + +if(isset($_SESSION[$settings->sessionprefix . "-user"]) and + isset($_SESSION[$settings->sessionprefix . "-pass"])) +{ + // Grab the session variables + $env->user = $_SESSION[$settings->sessionprefix . "-user"]; + + // The user is logged in + $env->is_logged_in = true; + $env->user_data = $settings->users->{$env->user}; + +} + +// 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 /////// + +//////////////////// +// APIDoc strings // +//////////////////// +/** + * @apiDefine Admin Only the wiki administrator may use this call. + */ +/** + * @apiDefine Moderator Only users loggged with a moderator account may use this call. + */ +/** + * @apiDefine User Only users loggged in may use this call. + */ +/** + * @apiDefine Anonymous Anybody may use this call. + */ +/** + * @apiDefine UserNotLoggedInError + * @apiError UserNotLoggedInError You didn't log in before sending this request. + */ +/** +* @apiDefine UserNotModeratorError +* @apiError UserNotModeratorError You weren't loggged in as a moderator before sending this request. +*/ +/** +* @apiDefine PageParameter +* @apiParam {string} page The page to operate on. +*/ +//////////////////// + +/////////////////////////////////////////////////////////////////////////////// +////////////////////////////////// Functions ////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +/** + * Get the actual absolute origin of the request sent by the user. + * @package core + * @param array $s The $_SERVER variable contents. Defaults to $_SERVER. + * @param bool $use_forwarded_host Whether to utilise the X-Forwarded-Host header when calculating the actual origin. + * @return string The actual origin of the user's request. + */ +function url_origin( $s = false, $use_forwarded_host = false ) +{ + if($s === false) $s = $_SERVER; + $ssl = ( ! empty( $s['HTTPS'] ) && $s['HTTPS'] == 'on' ); + $sp = strtolower( $s['SERVER_PROTOCOL'] ); + $protocol = substr( $sp, 0, strpos( $sp, '/' ) ) . ( ( $ssl ) ? 's' : '' ); + $port = $s['SERVER_PORT']; + $port = ( ( ! $ssl && $port=='80' ) || ( $ssl && $port=='443' ) ) ? '' : ':'.$port; + $host = ( $use_forwarded_host && isset( $s['HTTP_X_FORWARDED_HOST'] ) ) ? $s['HTTP_X_FORWARDED_HOST'] : ( isset( $s['HTTP_HOST'] ) ? $s['HTTP_HOST'] : null ); + $host = isset( $host ) ? $host : $s['SERVER_NAME'] . $port; + return $protocol . '://' . $host; +} + +/** + * Get the full url, as requested by the client. + * @package core + * @see http://stackoverflow.com/a/8891890/1460422 This Stackoverflow answer. + * @param array $s The $_SERVER variable. Defaults to $_SERVER. + * @param bool $use_forwarded_host Whether to take the X-Forwarded-Host header into account. + * @return string The full url, as requested by the client. + */ +function full_url( $s = false, $use_forwarded_host = false ) +{ + if($s == false) $s = $_SERVER; + return url_origin( $s, $use_forwarded_host ) . $s['REQUEST_URI']; +} + +/** + * Converts a filesize into a human-readable string. + * @package core + * @see http://php.net/manual/en/function.filesize.php#106569 The original source + * @author rommel + * @author 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 since a particular timestamp and returns a + * human-readable result. + * @package core + * @see http://goo.gl/zpgLgq The original source. No longer exists, maybe the wayback machine caught it :-( + * @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) +{ + return human_time(time() - $time); +} +/** + * Renders a given number of seconds as something that humans can understand more easily. + * @package core + * @param int $seconds The number of seconds to render. + * @return string The rendered time. + */ +function human_time($seconds) +{ + $tokens = array ( + 31536000 => 'year', + 2592000 => 'month', + 604800 => 'week', + 86400 => 'day', + 3600 => 'hour', + 60 => 'minute', + 1 => 'second' + ); + foreach ($tokens as $unit => $text) { + if ($seconds < $unit) continue; + $numberOfUnits = floor($seconds / $unit); + return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'').' ago'; + } +} + +/** + * A recursive glob() function. + * @package core + * @see http://in.php.net/manual/en/function.glob.php#106595 The original source + * @author Mike + * @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 the name of the parent page to the specified page. + * @since 0.15 + * @package core + * @param string $pagename The child page to get the parent + * page name for. + * @return string|bool + */ +function get_page_parent($pagename) { + if(mb_strpos($pagename, "/") === false) + return false; + return mb_substr($pagename, 0, mb_strrpos($pagename, "/")); +} + +/** + * Gets a list of all the sub pages of the current page. + * @package core + * @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. + * @package core + * @param $pagename The pagename to check. + */ +function check_subpage_parents($pagename) +{ + global $pageindex, $paths, $env; + // 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($env->storage_prefix . $parent_page_filename)) + { + // This parent page doesn't exist! Create it and add it to the page index. + touch($env->storage_prefix . $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. + * @package core + * @param string $string The string to make safe. + * @return string A safe version of the given string. + */ +function makepathsafe($string) +{ + // Old restrictive system + //$string = preg_replace("/[^0-9a-zA-Z\_\-\ \/\.]/i", "", $string); + // Remove reserved characters + $string = preg_replace("/[?%*:|\"><()\\[\\]]/i", "", $string); + // Collapse multiple dots into a single dot + $string = preg_replace("/\.+/", ".", $string); + // Don't allow slashes at the beginning + $string = ltrim($string, "\\/"); + return $string; +} + +/** + * Hides an email address from bots by adding random html entities. + * @todo Make this more clevererer :D + * @package core + * @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. + * @package core + * @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. + * @package core + * @see http://www.pontikis.net/tip/?id=16 the source + * @see http://www.php.net/manual/en/function.strpos.php#87061 the source that the above was based on + * @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 = mb_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; +} + +/** + * Tests whether a string starts with a specified substring. + * @package core + * @param string $haystack The string to check against. + * @param string $needle The substring to look for. + * @return bool Whether the string starts with the specified substring. + */ +function startsWith($haystack, $needle) { + return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false; +} +/** + * Tests whether a string ends with a given substring. + * @package core + * @param string $whole The string to test against. + * @param string $end The substring test for. + * @return bool Whether $whole ends in $end. + */ +function endsWith($whole, $end) +{ + return (strpos($whole, $end, strlen($whole) - strlen($end)) !== false); +} +/** + * Replaces the first occurrence of $find with $replace. + * @package core + * @param string $find The string to search for. + * @param string $replace The string to replace the search string with. + * @param string $subject The string ot perform the search and replace on. + * @return string The source string after the find and replace has been performed. + */ +function str_replace_once($find, $replace, $subject) +{ + $index = strpos($subject, $find); + if($index !== false) + return substr_replace($subject, $replace, $index, strlen($find)); + return $subject; +} + +/** + * Returns the system's mime type mappings, considering the first extension + * listed to be cacnonical. + * @package core + * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer + * @author chaos + * @author Edited by Starbeamrainbowlabs + * @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. + * @package core + * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer + * @author chaos + * @author Edited by Starbeamrainbowlabs + * @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; +} + +/** + * Returns the system MIME type mapping of extensions to MIME types. + * @package core + * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer + * @author chaos + * @author Edited by Starbeamrainbowlabs + * @return array An array mapping file extensions to their associated mime types. + */ +function system_extension_mime_types() +{ + 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); + foreach($parts as $part) + $out[$part] = $type; + } + fclose($file); + return $out; +} +/** + * Converts a given file extension to it's associated mime type. + * @package core + * @see http://stackoverflow.com/a/1147952/1460422 From this stackoverflow answer + * @author chaos + * @author Edited by Starbeamrainbowlabs + * @param string $ext The extension to convert. + * @return string The mime type associated with the given extension. + */ +function system_extension_mime_type($ext) { + static $types; + if(!isset($types)) + $types = system_extension_mime_types(); + $ext = strtolower($ext); + return isset($types[$ext]) ? $types[$ext] : null; +} + +/** + * Generates a stack trace. + * @package core + * @param bool $log_trace Whether to send the stack trace to the error log. + * @param bool $full Whether to output a full description of all the variables involved. + * @return string A string prepresentation of a stack trace. + */ +function stack_trace($log_trace = true, $full = false) +{ + $result = ""; + $stackTrace = debug_backtrace(); + $stackHeight = count($stackTrace); + foreach ($stackTrace as $i => $stackEntry) + { + $result .= "#" . ($stackHeight - $i) . ": "; + $result .= (isset($stackEntry["file"]) ? $stackEntry["file"] : "(unknown file)") . ":" . (isset($stackEntry["line"]) ? $stackEntry["line"] : "(unknown line)") . " - "; + if(isset($stackEntry["function"])) + { + $result .= "(calling " . $stackEntry["function"]; + if(isset($stackEntry["args"]) && count($stackEntry["args"])) + { + $result .= ": "; + $result .= implode(", ", array_map($full ? "var_dump_ret" : "var_dump_short", $stackEntry["args"])); + } + } + $result .= ")\n"; + } + if($log_trace) + error_log($result); + return $result; +} +/** + * Calls var_dump() and returns the output. + * @package core + * @param mixed $var The thing to pass to var_dump(). + * @return string The output captured from var_dump(). + */ +function var_dump_ret($var) +{ + ob_start(); + var_dump($var); + return ob_get_clean(); +} + +/** + * Calls var_dump(), shortening the output for various types. + * @package core + * @param mixed $var The thing to pass to var_dump(). + * @return string A shortened version of the var_dump() output. + */ +function var_dump_short($var) +{ + $result = trim(var_dump_ret($var)); + if(substr($result, 0, 6) === "object" || substr($result, 0, 5) === "array") + { + $result = substr($result, 0, strpos($result, " ")) . " { ... }"; + } + return $result; +} + +if (!function_exists('getallheaders')) { + /** + * Polyfill for PHP's native getallheaders() function on platforms that + * don't have it. + * @package core + * @todo Identify which platforms don't have it and whether we still need this + */ + function getallheaders() + { + if (!is_array($_SERVER)) + return []; + + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} +/** + * Renders a timestamp in HTML. + * @package core + * @param int $timestamp The timestamp to render. + * @return string HTML representing the given timestamp. + */ +function render_timestamp($timestamp) +{ + return ""; +} +/** + * Renders a page name in HTML. + * @package core + * @param object $rchange The recent change to render as a page name + * @return string HTML representing the name of the given page. + */ +function render_pagename($rchange) +{ + global $pageindex; + $pageDisplayName = $rchange->page; + if(isset($pageindex->$pageDisplayName) and !empty($pageindex->$pageDisplayName->redirect)) + $pageDisplayName = "$pageDisplayName"; + $pageDisplayLink = "$pageDisplayName"; + return $pageDisplayName; +} +/** + * Renders an editor's or a group of editors name(s) in HTML. + * @package core + * @param string $editorName The name of the editor to render. + * @return string HTML representing the given editor's name. + */ +function render_editor($editorName) +{ + return "✎ $editorName"; +} + +/** + * Saves the settings file back to peppermint.json. + * @return bool Whether the settings were saved successfully. + */ +function save_settings() { + global $paths, $settings; + return file_put_contents($paths->settings_file, json_encode($settings, JSON_PRETTY_PRINT)) !== false; +} + +/** + * Saves the currently logged in user's data back to peppermint.json. + * @package core + * @return bool Whether the user's data was saved successfully. Returns false if the user isn't logged in. + */ +function save_userdata() +{ + global $env, $settings, $paths; + + if(!$env->is_logged_in) + return false; + + $settings->users->{$env->user} = $env->user_data; + + return save_settings(); +} + +/** + * Figures out the path to the user page for a given username. + * Does not check to make sure the user acutally exists. + * @package core + * @param string $username The username to get the path to their user page for. + * @return string The path to the given user's page. + */ +function get_user_pagename($username) { + global $settings; + return "$settings->user_page_prefix/$username"; +} +/** + * Extracts a username from a user page path. + * @package core + * @param string $userPagename The suer page path to extract from. + * @return string The name of the user that the user page belongs to. + */ +function extract_user_from_userpage($userPagename) { + global $settings; + $matches = []; + preg_match("/$settings->user_page_prefix\\/([^\\/]+)\\/?/", $userPagename, $matches); + + return $matches[1]; +} + +/** + * Sends a plain text email to a user, replacing {username} with the specified username. + * @package core + * @param string $username The username to send the email to. + * @param string $subject The subject of the email. + * @param string $body The body of the email. + * @return boolean Whether the email was sent successfully or not. Currently, this may fail if the user doesn't have a registered email address. + */ +function email_user($username, $subject, $body) +{ + global $version, $settings; + + // If the user doesn't have an email address, then we can't email them :P + if(empty($settings->users->{$username}->emailAddress)) + return false; + + $subject = str_replace("{username}", $username, $subject); + $body = str_replace("{username}", $username, $body); + + $headers = [ + "content-type" => "text/plain", + "x-mailer" => "$settings->sitename Pepperminty-Wiki/$version PHP/" . phpversion(), + "reply-to" => "$settings->admindetails_name <$settings->admindetails_email>" + ]; + $compiled_headers = ""; + foreach($headers as $header => $value) + $compiled_headers .= "$header: $value\r\n"; + + return mail($settings->users->{$username}->emailAddress, $subject, $body, $compiled_headers, "-t"); +} +/** + * Sends a plain text email to a list of users, replacing {username} with each user's name. + * @package core + * @param string[] $usernames A list of usernames to email. + * @param string $subject The subject of the email. + * @param string $body The body of the email. + * @return integer The number of emails sent successfully. + */ +function email_users($usernames, $subject, $body) +{ + $emailsSent = 0; + foreach($usernames as $username) + { + $emailsSent += email_user($username, $subject, $body) ? 1 : 0; + } + return $emailsSent; +} + +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////////////// +////////////////////// 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 = substr( // Store the filename, whilst trimming the storage prefix + $pagefilename, + mb_strlen(preg_replace("/^\.\//iu", "", $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 = "unknown"; // Set the editor to "unknown" + // Extract the name of the (sub)page without the ".md" + $pagekey = mb_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)); + $env->perfdata->pageindex_decode_time = round((microtime(true) - $pageindex_read_start)*1000, 3); + header("x-pageindex-decode-time: " . $env->perfdata->pageindex_decode_time . "ms"); +} + +////////////////////////// +///// Page id system ///// +////////////////////////// +if(!file_exists($paths->idindex)) + file_put_contents($paths->idindex, "{}"); +$idindex_decode_start = microtime(true); +$idindex = json_decode(file_get_contents($paths->idindex)); +$env->perfdata->idindex_decode_time = round((microtime(true) - $idindex_decode_start)*1000, 3); +/** + * Provides an interface to interact with page ids. + * @package core + */ +class ids +{ + /** + * Gets the page id associated with the given page name. + * If it doesn't exist in the id index, it will be added. + * @package core + * @param string $pagename The name of the page to fetch the id for. + * @return integer The id for the specified page name. + */ + public static function getid($pagename) + { + global $idindex; + + $pagename_norm = Normalizer::normalize($pagename, Normalizer::FORM_C); + foreach ($idindex as $id => $entry) + { + // We don't need to normalise here because we normralise when assigning ids + if($entry == $pagename_norm) + return $id; + } + + // This pagename doesn't have an id - assign it one quick! + return self::assign($pagename); + } + + /** + * Gets the page name associated with the given page id. + * Be warned that if the id index is cleared (e.g. when the search index is + * rebuilt from scratch), the id associated with a page name may change! + * @package core + * @param int $id The id to fetch the page name for. + * @return string The page name currently associated with the specified id. + */ + public static function getpagename($id) + { + global $idindex; + + if(!isset($idindex->$id)) + return false; + else + return $idindex->$id; + } + + /** + * 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. + * @package core + * @param string $oldpagename The old page name to move. + * @param string $newpagename The new page name to move the old page name to. + */ + public static function movepagename($oldpagename, $newpagename) + { + global $idindex, $paths; + + $pageid = self::getid(Normalizer::normalize($oldpagename, Normalizer::FORM_C)); + $idindex->$pageid = Normalizer::normalize($newpagename, Normalizer::FORM_C); + + file_put_contents($paths->idindex, json_encode($idindex)); + } + + /** + * Removes the given page name from the id index. + * Note that this function doesn't handle multiple entries with the same + * name. Also note that it may get re-added during a search reindex if the + * page still exists. + * @package core + * @param string $pagename The page name to delete from the id index. + */ + 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)); + } + + /** + * Clears the id index completely. + * Will break the inverted search index! Make sure you rebuild the search + * index (if the search module is installed, of course) if you want search + * to still work. Of course, note that will re-add all the pages to the id + * index. + * @package core + */ + public static function clear() + { + global $paths, $idindex; + // Delete the old id index + unlink($paths->idindex); + // Create the new id index + file_put_contents($paths->idindex, "{}"); + // Reset the in-memory id index + $idindex = new stdClass(); + } + + /** + * Assigns an id to a pagename. Doesn't check to make sure that + * pagename doesn't already exist in the id index. + * @package core + * @param string $pagename The page name to assign an id to. + * @return integer The id assigned to the specified page name. + */ + protected static function assign($pagename) + { + global $idindex, $paths; + + $pagename = Normalizer::normalize($pagename, Normalizer::FORM_C); + + $nextid = count(array_keys(get_object_vars($idindex))); + // Increment the generated id until it's unique + while(isset($idindex->nextid)) + $nextid++; + + // Update the id index + $idindex->$nextid = $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"]) and basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) == "index.php") +{ + http_response_code(302); + header("location: " . dirname(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))); + exit(); +} + +// Make sure that the action is set +if(empty($_GET["action"])) + $_GET["action"] = $settings->defaultaction; +// Make sure that the page is set +if(empty($_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(); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////// HTML fragments //////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/** + * Renders the HTML page that is sent to the client. + * @package core + */ +class page_renderer +{ + /** + * The root HTML template that all pages are built from. + * @var string + * @package core + */ + public static $html_template = " + + + + {title} + + + + + {header-html} + + + {body} + + + +"; + /** + * The main content template that is used to render normal wiki pages. + * @var string + * @package core + */ + public static $main_content_template = "{navigation-bar} +

{sitename}

+
+ {content} +
+ {extra} + + {navigation-bar-bottom} + {all-pages-datalist}"; + /** + * A specially minified content template that doesn't include the navbar and + * other elements not suitable for printing. + * @var string + * @package core + */ + public static $minimal_content_template = "
{content}
+ "; + + /** + * An array of items indicating the resources to ask the web server to push + * down to the client with HTTP/2.0 server push. + * Format: [ [type, path], [type, path], .... ] + * @var array[] + */ + protected static $http2_push_items = []; + + /** + * 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. + * @var array + * @package core + */ + protected static $part_processors = []; + + /** + * Registers a function as a part post processor. + * This function's use is more complicated to explain. Pepperminty Wiki + * renders pages with a very simple templating system. For example, in the + * template a page's content is denoted by `{content}`. A function + * registered here will be passed all the components of a page _just_ + * before they are dropped into the template. Note that the function you + * pass in here should take a *reference* to the components, as the return + * value of the function passed is discarded. + * @package core + * @param function $function The part preprocessor to register. + */ + 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; + } + + /** + * Renders a HTML page with the content specified. + * @package core + * @param string $title The title of the page. + * @param string $content The (HTML) content of the page. + * @param boolean $body_template The HTML content template to use. + * @return string The rendered HTML, ready to send to the client :-) + */ + 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."); + } + } + + // Push the logo via HTTP/2.0 if possible + if($settings->favicon[0] === "/") self::$http2_push_items[] = ["image", $settings->favicon]; + + $parts = [ + "{body}" => $body_template, + + "{sitename}" => $logo_html, + "v0.18-dev" => $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(", ", array_map(function($username) { return page_renderer::render_username($username); }, $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, + "{extra}" => "", + "{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("{generation-time-taken}", round((microtime(true) - $start_time)*1000, 2), $result); + // Send the HTTP/2.0 server push indicators if possible - but not if we're sending a redirect page + if(!headers_sent() && (http_response_code() < 300 || http_response_code() >= 400)) self::send_server_push_indicators(); + return $result; + } + /** + * Renders a normal HTML page. + * @package core + * @param string $title The title of the page. + * @param string $content The content of the page. + * @return string The rendered page. + */ + public static function render_main($title, $content) + { + return self::render($title, $content, self::$main_content_template); + } + /** + * Renders a minimal HTML page. Useful for printable pages. + * @package core + * @param string $title The title of the page. + * @param string $content The content of the page. + * @return string The rendered page. + */ + public static function render_minimal($title, $content) + { + return self::render($title, $content, self::$minimal_content_template); + } + + /** + * Sends the currently registered HTTP2 server push items to the client. + * @return integer|FALSE The number of resource hints included in the link: header, or false if server pushing is disabled. + */ + public static function send_server_push_indicators() { + global $settings; + if(!$settings->http2_server_push) + return false; + + // Render the preload directives + $link_header_parts = []; + foreach(self::$http2_push_items as $push_item) + $link_header_parts[] = "<{$push_item[1]}>; rel=preload; as={$push_item[0]}"; + + // Send them in a link: header + if(!empty($link_header_parts)) + header("link: " . implode(", ", $link_header_parts)); + + return count(self::$http2_push_items); + } + + /** + * Renders the header HTML. + * @package core + * @return string The rendered HTML that goes in the header. + */ + public static function get_header_html() + { + global $settings; + $result = self::get_css_as_html(); + $result .= self::getJS(); + + // We can't use module_exists here because sometimes global $modules + // hasn't populated yet when we get called O.o + if(class_exists("search")) + $result .= "\t\t\n"; + + if(!empty($settings->enable_math_rendering)) + { + $result .= ""; + } + + return $result; + } + /** + * Figures out whether $settings->css is a url, or a string of css. + * A url is something starting with "protocol://" or simply a "/". + * @return boolean True if it's a url - false if we assume it's a string of css. + */ + public static function is_css_url() { + global $settings; + return preg_match("/^[^\/]*\/\/|^\//", $settings->css); + } + /** + * Renders all the CSS as HTML. + * @package core + * @return string The css as HTML, ready to be included in the HTML header. + */ + public static function get_css_as_html() + { + global $settings, $defaultCSS; + + if(self::is_css_url()) { + if($settings->css[0] === "/") // Push it if it's a relative resource + self::AddServerPushIndicator("style", $settings->css); + return "\n"; + } else { + $css = $settings->css == "auto" ? $defaultCSS : $settings->css; + if(!empty($settings->optimize_pages)) + { + // CSS Minification ideas by Jean from catswhocode.com + // Link: http://www.catswhocode.com/blog/3-ways-to-compress-css-files-using-php + // Remove comments + $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', "", $css); + // Cut down whitespace + $css = preg_replace('/\s+/', " ", $css); + // Remove whitespace after colons and semicolons + $css = str_replace([ + " :", + ": ", + "; ", + " { ", + " } " + ], [ + ":", + ":", + ";", + "{", + "}" + ], $css); + + } + return "\n"; + } + } + /** + * The javascript snippets that will be included in the page. + * @var string[] + * @package core + */ + private static $jsSnippets = []; + /** + * The urls of the external javascript files that should be referenced + * by the page. + * @var string[] + * @package core + */ + private static $jsLinks = []; + /** + * Adds the specified url to a javascript file as a reference to the page. + * @package core + * @param string $scriptUrl The url of the javascript file to reference. + */ + public function AddJSLink(string $scriptUrl) + { + static::$jsLinks[] = $scriptUrl; + } + /** + * Adds a javascript snippet to the page. + * @package core + * @param string $script The snippet of javascript to add. + */ + public function AddJSSnippet(string $script) + { + static::$jsSnippets[] = $script; + } + /** + * Renders the included javascript header for inclusion in the final + * rendered page. + * @package core + * @return string The rendered javascript ready for inclusion in the page. + */ + private static function getJS() + { + $result = "\n"; + foreach(static::$jsSnippets as $snippet) + $result .= "\n"; + foreach(static::$jsLinks as $link) { + // Push it via HTTP/2.0 if it's relative + if($link[0] === "/") self::AddServerPushIndicator("script", $link); + $result .= "\n"; + } + return $result; + } + + // ~ + + /** + * Adds a resource to the list of items to indicate that the web server should send via HTTP/2.0 Server Push. + * Note: Only specify static files here, as you might end up with strange (and possibly dangerous) results! + * @param string $type The resource type. See https://fetch.spec.whatwg.org/#concept-request-destination for more information. + * @param string $path The *relative url path* to the resource. + */ + public static function AddServerPushIndicator($type, $path) { + self::$http2_push_items[] = [ $type, $path ]; + } + + // ~ + + /** + * The navigation bar divider. + * @package core + * @var string + */ + public static $nav_divider = " | "; + + /** + * Renders a navigation bar from an array of links. See + * $settings->nav_links for format information. + * @package core + * @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. + * @param string $class The class(es) to assign to the rendered + * navigation bar. + */ + public static function render_navigation_bar($nav_links, $nav_links_extra, $class = "") + { + global $settings, $env; + $result = "

"; + return $result; + } + /** + * Renders a username for inclusion in a page. + * @package core + * @param string $name The username to render. + * @return string The username rendered in HTML. + */ + public static function render_username($name) + { + global $settings; + $result = ""; + $result .= ""; + if($settings->avatars_show) + $result .= " "; + if(in_array($name, $settings->admins)) + $result .= $settings->admindisplaychar; + $result .= htmlentities($name); + $result .= ""; + + return $result; + } + + // ~ + + /** + * Renders the datalist for the search box as HTML. + * @package core + * @return string The search box datalist as HTML. + */ + public static function generate_all_pages_datalist() + { + global $settings, $pageindex; + $arrayPageIndex = get_object_vars($pageindex); + ksort($arrayPageIndex); + $result = "\n"; + + // If dynamic page sugggestions are enabled, then we should send a loading message instead. + if($settings->dynamic_page_suggestion_count > 0) + { + $result .= ""; + + return $result; + } +} + +// HTTP/2.0 Server Push static items +foreach($settings->http2_server_push_items as $push_item) { + page_renderer::AddServerPushIndicator($push_item[0], $push_item[1]); +} + +// Math rendering support +if(!empty($settings->enable_math_rendering)) +{ + page_renderer::AddJSLink("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"); +} +// alt+enter support in the search box +page_renderer::AddJSSnippet('// Alt + Enter support in the top search box +window.addEventListener("load", function(event) { + document.querySelector("input[type=search]").addEventListener("keyup", function(event) { + // Listen for Alt + Enter + if(event.keyCode == 13 && event.altKey) { + event.stopPropagation(); + event.preventDefault(); + event.cancelBubble = true; + event.target.form.setAttribute("target", "_blank"); + event.target.form.submit(); + event.target.form.removeAttribute("target"); + return false; // Required by some browsers + } + }); +}); +'); + +/// Finish setting up the environment object /// +$env->page = $_GET["page"]; +if(isset($_GET["revision"]) and is_numeric($_GET["revision"])) +{ + // We have a revision number! + $env->is_history_revision = true; + $env->history->revision_number = intval($_GET["revision"]); + + // Make sure that the revision exists for later on + if(!isset($pageindex->{$env->page}->history[$env->history->revision_number])) + { + http_response_code(404); + exit(page_renderer::render_main("404: Revision Not Found - $env->page - $settings->sitename", "

Revision #{$env->history->revision_number} of $env->page doesn't appear to exist. Try viewing the list of revisions for $env->page, or viewing the latest revision instead.

")); + } + + $env->history->revision_data = $pageindex->{$env->page}->history[$env->history->revision_number]; +} +// Construct the page's filename +$env->page_filename = $env->storage_prefix; +if($env->is_history_revision) + $env->page_filename .= $pageindex->{$env->page}->history[$env->history->revision_number]->filename; +else if(isset($pageindex->{$env->page})) + $env->page_filename .= $pageindex->{$env->page}->filename; + +$env->action = strtolower($_GET["action"]); + +//////////////////////////////////////////////// + +////////////////////////////////////// +///// Extra consistency measures ///// +////////////////////////////////////// + +// CHANGED: The search redirector has now been moved to below the module registration system, as it was causing a warning here + +// 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", "opensearch-description", "invindex-rebuild", "stats-update" ])) // And the user isn't trying to login, or get the opensearch description, or access actions that apply their own access rules +{ + // Redirect the user to the login page + http_response_code(307); + header("x-login-required: yes"); + $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.

")); +} +////////////////////////////////////// +////////////////////////////////////// + +$remote_files = []; +/** + * Registers a request for a remote file to be downloaded before execution. Will block until all files are downloaded. + * Example definition: + * [ "local_filename" => "file.ext", "remote_url": "https://example.com" ] + * @param array $remote_file_def The remote file definition to register. + * @throws Exception Exception Throws an exception if a definition for the requested local file already exists. + */ +function register_remote_file($remote_file_def) { + global $remote_files; + + foreach($remote_files as $ex_remote_file_def) { + if($ex_remote_file_def["local_filename"] == $remote_file_def["local_filename"]) + throw new Exception("Error: A remote file with the local filename '{$remote_file_def["local_filename"]}' is already registered."); + } + + $remote_files[] = $remote_file_def; +} + +////////////////////////// +/// Module functions /// +////////////////////////// +// These functions are // +// used by modules to // +// register themselves // +// or new pages. // +////////////////////////// +/** A list of all the currently loaded modules. Not guaranteed to be populated until an action is executed. */ +$modules = []; +/** + * Registers a module. + * @package core + * @param array $moduledata The module data to register. + */ +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. + * @package core + * @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; +} + +$actions = new stdClass(); +/** + * Registers a new action handler. + * @package core + * @param string $action_name The action to register. + * @param function $func The function to call when the specified + * action is requested. + */ +function add_action($action_name, $func) +{ + global $actions; + $actions->$action_name = $func; +} +/** + * Figures out whether a given action is currently registered. + * Only guaranteed to be accurate in inside an existing action function + * @package core + * @param string $action_name The name of the action to search for + * @return boolean Whether an action with the specified name exists. + */ +function has_action($action_name) +{ + global $actions; + return !empty($actions->$action_name); +} + +$parsers = [ + "none" => function() { + throw new Exception("No parser registered!"); + } +]; +/** + * Registers a new parser. + * @package core + * @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; +} +/** + * Parses the specified page source using the parser specified in the settings + * into HTML. + * The specified parser may (though it's unilkely) render it to other things. + * @package core + * @param string $source The source to render. + * @return string The source rendered to HTML. + */ +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. + * @package core + * @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. + * @package core + * @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-mxpressions", "Mathematical 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).

"); + +/** An array of the currently registerd statistic calculators. Not guaranteed to be populated until the requested action function is called. */ +$statistic_calculators = []; +/** + * Registers a statistic calculator against the system. + * @package core + * @param array $stat_data The statistic object to register. + */ +function statistic_add($stat_data) { + global $statistic_calculators; + $statistic_calculators[$stat_data["id"]] = $stat_data; +} +/** + * Checks whether a specified statistic has been registered. + * @package core + * @param string $stat_id The id of the statistic to check the existence of. + * @return boolean Whether the specified statistic has been registered. + */ +function has_statistic($stat_id) { + global $statistic_calculators; + return !empty($statistic_calculators[$stat_id]); +} + +////////////////////////////////////////////////////////////////// + register_module([ "name" => "Password hashing action", @@ -9460,65 +9460,65 @@ class PeppermintParsedown extends ParsedownExtra -// %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!

")); -} - -// Download all the requested remote files -ini_set("user_agent", "$settings->sitename (Pepperminty-Wiki-Downloader; PHP/" . phpversion() . "; +https://github.com/sbrl/Pepperminty-Wiki/) Pepperminty-Wiki/$version"); -foreach($remote_files as $remote_file_def) { - if(file_exists($remote_file_def["local_filename"]) && filesize($remote_file_def["local_filename"]) > 0) - continue; - - error_log("[ Pepperminty-Wiki/$settings->sitename ] Downloading {$remote_file_def["local_filename"]} from {$remote_file_def["remote_url"]}"); - file_put_contents($remote_file_def["local_filename"], fopen($remote_file_def["remote_url"], "rb")); -} - -////////////////////////////////// -/// Final Consistency Measures /// -////////////////////////////////// - -if(!isset($pageindex->{$env->page}) && isset($pageindex->{ucwords($env->page)})) { - http_response_code(307); - header("location: ?page=" . ucwords($env->page)); - header("content-type: text/plain"); - exit("$env->page doesn't exist on $settings->sitename, but " . ucwords($env->page) . " does. You should be redirected there automatically."); -} - -// 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_minimal("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.

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

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

")); +} + +// Download all the requested remote files +ini_set("user_agent", "$settings->sitename (Pepperminty-Wiki-Downloader; PHP/" . phpversion() . "; +https://github.com/sbrl/Pepperminty-Wiki/) Pepperminty-Wiki/$version"); +foreach($remote_files as $remote_file_def) { + if(file_exists($remote_file_def["local_filename"]) && filesize($remote_file_def["local_filename"]) > 0) + continue; + + error_log("[ Pepperminty-Wiki/$settings->sitename ] Downloading {$remote_file_def["local_filename"]} from {$remote_file_def["remote_url"]}"); + file_put_contents($remote_file_def["local_filename"], fopen($remote_file_def["remote_url"], "rb")); +} + +////////////////////////////////// +/// Final Consistency Measures /// +////////////////////////////////// + +if(!isset($pageindex->{$env->page}) && isset($pageindex->{ucwords($env->page)})) { + http_response_code(307); + header("location: ?page=" . ucwords($env->page)); + header("content-type: text/plain"); + exit("$env->page doesn't exist on $settings->sitename, but " . ucwords($env->page) . " does. You should be redirected there automatically."); +} + +// 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_minimal("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.

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

")); +} + +?> diff --git a/docs/05-Getting-A-Copy.md b/docs/05-Getting-A-Copy.md index 26b5bb3..d255adb 100644 --- a/docs/05-Getting-A-Copy.md +++ b/docs/05-Getting-A-Copy.md @@ -20,22 +20,24 @@ Pepperminty Wiki has a downloader that you can use to select the modules you wan You can also you the downloader offline. Simply clone this repository to your web server and then point your web browser at `your.server.com/path/to/pepperminty/wiki/download.php`. ## Method 4: Building from source -Pepperminty Wiki can also be built from source (and I do this all the time when testing). Start by cloning the repository. Then go into the `modules` folder and append `.disabled` to the names of any modules you don't want to be included (e.g. `modules/page-edit.php` would become `modules/page-edit.php.disabled`). Then follow the instructions for your platform below. The resulting file will be located at `build/index.php`. +Pepperminty Wiki can also be built from source (and I do this all the time when testing). Start by cloning the repository. Then go into the `modules` folder and append `.disabled` to the names of any modules you don't want to be included (e.g. `modules/page-edit.php` would become `modules/page-edit.php.disabled`). Then follow the instructions below. The resulting file will be located at `build/index.php`. -### Windows -Simply run the `build.bat` script in the root of the repository. It will handle everything for you. - -### Linux and Everyone Else -Run the following commands from the root of the repository in order, adjusting them for your specific platform (these are for a standard Ubuntu Server install): +Run the following commands from the root of the repository in order, adjusting them for your specific platform if required: ```bash rm build/index.php -php rebuild_module_index.php php build.php ``` -These commands are also in `build.sh`. You can run that if you want. Here's an explanation of what each command does: +These commands are also in `build.sh`. If you have bash installed (i.e. Linux and macOS users), you can run that instead like this: + +```bash +./build.sh build +``` + +The extra `build` is because the build script can do other things. Omit the `build` for a full list of tricks it has up its sleeve :D + +Here's an explanation of what each command does: 1. Deletes the old `index.php` in the build folder that comes with the repository -2. Rebuilds the module index that the build scripts uses to determine what modules it should include when building -3. Actually builds Pepperminty Wiki. Outputs to `index.php`. +2. Rebuilds the module index that the build scripts uses to determine what modules it should include when building, and then actually builds Pepperminty Wiki. Outputs to `index.php`. diff --git a/Module_API_Docs.md b/docs/07-Writing-Modules.md similarity index 94% rename from Module_API_Docs.md rename to docs/07-Writing-Modules.md index b0f4ccc..f05f292 100644 --- a/Module_API_Docs.md +++ b/docs/07-Writing-Modules.md @@ -2,7 +2,7 @@ The core of Pepperminty Wiki exposes several global objects, classes, functions, and miscellaneous files that you can use to write your own modules. This page documents these them so that you can create your own modules more easily. ## Table of Contents - - [Rest API](#rest-api) + - [HTTP API](#http-api) - [Module API](#module-api) - [Global Variables](#global-variables) - [Files](#files) @@ -12,11 +12,11 @@ The core of Pepperminty Wiki exposes several global objects, classes, functions, - [`recent-changes.json`](#recent-changesjson) - [`statsindex.json`](#statsindexjson) -## Rest API -The REST api provided by Pepperminty Wiki itself is documented for bot owners and software developers alike over on GitHub pages [here](https://sbrl.github.io/Pepperminty-Wiki/docs/RestApi/). +## HTTP API +The HTTP API provided by Pepperminty Wiki itself is documented for bot owners and software developers alike. Find it via the _HTTP API_ section in the sidebar of this page. ## Module API -The main PHP-based module API is documented with php documentor. The docs can be found [here](https://sbrl.github.io/Pepperminty-Wiki/docs/ModuleApi/), hosted on GitHub Pages. +The main PHP-based module API is documented with _PHPDoc_. It can be found via the _PHP Module API_ section in the sidebar of this page. This documentation covers all the functions and classes available in both the Pepperminty Wiki core, and the modules stored in this repository - as well as mentioning which module they are a part of. diff --git a/Development.md b/docs/08-Making-A-Release.md similarity index 53% rename from Development.md rename to docs/08-Making-A-Release.md index 32b47cf..18534f9 100644 --- a/Development.md +++ b/docs/08-Making-A-Release.md @@ -1,14 +1,17 @@ -# Development Notes -This page contains a few notes about Pepperminty Wiki development. These notes are intended to remind me of things I need to do, but you may find them useful. +# Making a Release +This page contains a few notes about making a release of Pepperminty Wiki. These notes are intended to remind me of things I need to do, but you may find them useful. -## Preparing for a release The following things need to be done to prepare for a release: - Check for outstanding issues - Check the changelog - Make sure that the README is up to date - Make sure that Pepperminty Wiki actually works - - Bump the version + - Bump the version: + - In the `version` file + - In the changelog + - In `apidoc.json` (TODO: Automate this?) + - In the README.md (TODO: Automate this?) - (Stable releases only) Pull down changes to update online downloader at starbeamrainbowlabs.com/labs/peppermint/download.php - Update wikimatrix - Write & publish the release diff --git a/docs/ModuleApi/classes/PeppermintParsedown.html b/docs/ModuleApi/classes/PeppermintParsedown.html index 4a0f06c..f0c8941 100644 --- a/docs/ModuleApi/classes/PeppermintParsedown.html +++ b/docs/ModuleApi/classes/PeppermintParsedown.html @@ -106,12 +106,12 @@