mirror of
https://github.com/sbrl/Pepperminty-Wiki.git
synced 2024-12-23 01:55:01 +00:00
Starbeamrainbowlabs
f4f08d8066
As it turns out, we used @apiVersion for things that weren't HTTP API routes. In such cases, the recommended directive is @since, not @apiVersion
959 lines
32 KiB
PHP
959 lines
32 KiB
PHP
<?php
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
/**
|
|
* 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 )
|
|
{
|
|
global $env;
|
|
if($s === false) $s = $_SERVER;
|
|
$ssl = $env->is_secure;
|
|
$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'];
|
|
}
|
|
|
|
/**
|
|
* Get the stem URL at which this Pepperminty Wiki instance is located
|
|
* You can just append ?get_params_here to this and it will be a valid URL.
|
|
* Uses full_url() under the hood.
|
|
* Note that this is based on the URL of the current request.
|
|
* @param array $s The $_SERVER variable (defaults to $_SERVER)
|
|
* @param bool $use_forwarded_host Whether to use the x-forwarded-host header or ignore it.
|
|
* @return string The stem url, as described above
|
|
*/
|
|
function url_stem( $s = false, bool $use_forwarded_host = false) : string {
|
|
// Calculate the stem from the current full URL by stripping everything after the question mark ('?')
|
|
$url_stem = full_url();
|
|
if(mb_strpos($url_stem, "?") !== false) $url_stem = mb_substr($url_stem, 0, mb_strpos($url_stem, "?"));
|
|
return $url_stem;
|
|
}
|
|
|
|
/**
|
|
* 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 int $bytes The number of bytes to convert.
|
|
* @param int $decimals The number of decimal places to preserve.
|
|
* @return string A human-readable filesize.
|
|
*/
|
|
function human_filesize($bytes, $decimals = 2)
|
|
{
|
|
$sz = ["b", "kib", "mib", "gib", "tib", "pib", "eib", "yib", "zib"];
|
|
$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 int $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 int $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;
|
|
}
|
|
|
|
/**
|
|
* Resolves a relative path against a given base directory.
|
|
* @since 0.20.0
|
|
* @source https://stackoverflow.com/a/44312137/1460422
|
|
* @param string $path The relative path to resolve.
|
|
* @param string|null $basePath The base directory to resolve against.
|
|
* @return string An absolute path.
|
|
*/
|
|
function path_resolve(string $path, string $basePath = null) {
|
|
// Make absolute path
|
|
if (substr($path, 0, 1) !== DIRECTORY_SEPARATOR) {
|
|
if ($basePath === null) {
|
|
// Get PWD first to avoid getcwd() resolving symlinks if in symlinked folder
|
|
$path=(getenv('PWD') ?: getcwd()).DIRECTORY_SEPARATOR.$path;
|
|
} elseif (strlen($basePath)) {
|
|
$path=$basePath.DIRECTORY_SEPARATOR.$path;
|
|
}
|
|
}
|
|
|
|
// Resolve '.' and '..'
|
|
$components=array();
|
|
foreach(explode(DIRECTORY_SEPARATOR, rtrim($path, DIRECTORY_SEPARATOR)) as $name) {
|
|
if ($name === '..') {
|
|
array_pop($components);
|
|
} elseif ($name !== '.' && !(count($components) && $name === '')) {
|
|
// … && !(count($components) && $name === '') - we want to keep initial '/' for abs paths
|
|
$components[]=$name;
|
|
}
|
|
}
|
|
|
|
return implode(DIRECTORY_SEPARATOR, $components);
|
|
}
|
|
|
|
/**
|
|
* Determines if a directory is empty or not.
|
|
* @ref https://stackoverflow.com/a/7497848/1460422
|
|
* @param string $dir The path to the directory to check.
|
|
* @return boolean True if the directory is empty, or false if it is not empty.
|
|
*/
|
|
function is_directory_empty(string $dir) : bool {
|
|
$handle = opendir($dir);
|
|
while (false !== ($entry = readdir($handle))) {
|
|
if ($entry != "." && $entry != "..") {
|
|
closedir($handle);
|
|
return false;
|
|
}
|
|
}
|
|
closedir($handle);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Converts a filepath to a page name.
|
|
* @param string $filepath The filepath to convert.
|
|
* @return string The extracted pagename.
|
|
*/
|
|
function filepath_to_pagename(string $filepath) : string {
|
|
global $env;
|
|
// Strip the storage prefix, but only if it isn't a dot
|
|
if(starts_with($filepath, $env->storage_prefix) && $env->storage_prefix !== ".")
|
|
$filepath = mb_substr($filepath, mb_strlen($env->storage_prefix));
|
|
|
|
// If a revision number is detected, strip it
|
|
if(preg_match("/\.r[0-9]+$/", $filepath) > 0)
|
|
$filepath = mb_substr($filepath, 0, mb_strrpos($filepath, ".r"));
|
|
|
|
// Strip the .md file extension
|
|
if(ends_with($filepath, ".md"))
|
|
$filepath = mb_substr($filepath, 0, -3);
|
|
|
|
return $filepath;
|
|
}
|
|
|
|
/**
|
|
* Gets the name of the parent page to the specified page.
|
|
* @since 0.15.0
|
|
* @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 string $pagename The pagename to check.
|
|
* @param bool $create_dir Whether to create an associated directory for subpages or not.
|
|
*/
|
|
function check_subpage_parents(string $pagename, bool $create_dir = true)
|
|
{
|
|
global $pageindex, $paths, $env;
|
|
// Save the new pageindex and return if there aren't any more parent pages to check
|
|
if(strpos($pagename, "/") === false)
|
|
return save_pageindex();
|
|
|
|
$pagename = makepathsafe($pagename); // Just in case
|
|
|
|
$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;
|
|
}
|
|
if($create_dir) {
|
|
$dirname = $env->storage_prefix . $parent_pagename;
|
|
if(!file_exists($dirname))
|
|
mkdir($dirname, 0755, true);
|
|
}
|
|
|
|
check_subpage_parents($parent_pagename, $create_dir);
|
|
}
|
|
|
|
/**
|
|
* Makes a path (or page name) safe.
|
|
* A safe path / page name may not contain:
|
|
* Forward-slashes at the beginning
|
|
* Multiple dots in a row
|
|
* Odd characters (e.g. ?%*:|"<>() etc.)
|
|
* A safe path may, however, contain unicode characters such as éôà etc.
|
|
* @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, "\\/");
|
|
// Don't allow dots on their own
|
|
$string = preg_replace(["/^\.\\/|\\/\.$/", "/\\/\.\\//"], ["", "/"], $string);
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Slugifies a given string such that it can only contain a-z0-9-_.
|
|
* Also automatically makes it lowercase.
|
|
* @param string $text The text to operate on.
|
|
* @return string The slugified string.
|
|
*/
|
|
function slugify(string $text) : string {
|
|
return preg_replace("/[^a-zA-Z0-9\-_]/", "", $text);
|
|
}
|
|
|
|
/**
|
|
* Hides an email address from bots. Returns a fragment of HTML that contains the mangled email address.
|
|
* @package core
|
|
* @param string $str The original email address
|
|
* @param string $display_text The display text for the resulting HTML - if null then the original email address is used. Note that because it's base64 encoded and then textContent is used, one does not need to run either htmlentities() or rawurlencode() over this value as it's completely safe.
|
|
* @return string The mangled email address as a fragment of HTML.
|
|
*/
|
|
function hide_email(string $email, string $display_text = null) : string
|
|
{
|
|
$enc = json_encode([ $email, $display_text ]);
|
|
$len = strlen($enc);
|
|
$pool = []; for($i = 0; $i < $len; $i++) $pool[] = $i;
|
|
$a = []; $b = [];
|
|
for($i = 0; $i < $len; $i++) {
|
|
$n = random_int(0, $len - $i - 1);
|
|
$j = array_splice($pool, $n, 1)[0]; $b[] = $j;
|
|
// echo("chose ".$enc[$j].", index $j, n $n\n");
|
|
$a[] = $enc[$j];
|
|
}
|
|
$a = base64_encode(implode("|", $a));
|
|
$b = base64_encode(implode("|", $b));
|
|
$span_id = "he-".crypto_id(16);
|
|
return "<a href='#protected-with-javascript' id='$span_id'>[protected with javascript]</a><script>(() => {let c=\"$a|$b\".split('|').map(atob).map(s=>s.split('|'));let d=[],e=document.getElementById('$span_id');c[1].map((n,i)=>d[parseInt(n)]=c[0][i]);d=JSON.parse(d.join(''));e.textContent=d[1]==null?d[0]:d[1];e.setAttribute('href', 'mailto:'+d[0])})();</script>";
|
|
}
|
|
|
|
/**
|
|
* 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 bool Whether $needle can be found at the beginning of $haystack.
|
|
*/
|
|
function starts_with(string $haystack, string $needle) : bool {
|
|
$length = strlen($needle);
|
|
return (substr($haystack, 0, $length) === $needle);
|
|
}
|
|
/**
|
|
* Checks to see if $hackstack ends with $needle.
|
|
* The matching bookend to starts_with.
|
|
* @package core
|
|
* @param string $haystack The haystack to search..
|
|
* @param string $needle The needle to look for.
|
|
* @return bool
|
|
*/
|
|
function ends_with(string $haystack, string $needle) : bool {
|
|
$length = strlen($needle);
|
|
return (substr($haystack, -$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;
|
|
}
|
|
|
|
/**
|
|
* Creates an images containing the specified text.
|
|
* Useful for sending errors back to the client.
|
|
* @package core
|
|
* @param string $text The text to include in the image.
|
|
* @param int $target_size The target width to aim for when creating
|
|
* the image. Not not specified, a value is
|
|
* determined automatically.
|
|
* @return resource The handle to the generated GD image.
|
|
*/
|
|
function errorimage($text, $target_size = null)
|
|
{
|
|
$width = 0;
|
|
$height = 0;
|
|
$border_size = 10; // in px, if $target_size isn't null has no effect
|
|
$line_spacing = 2; // in px
|
|
$font_size = 5; // 1 - 5
|
|
|
|
$font_width = imagefontwidth($font_size); // in px
|
|
$font_height = imagefontheight($font_size); // in px
|
|
$text_lines = array_map("trim", explode("\n", $text));
|
|
|
|
if(!empty($target_size)) {
|
|
$width = $target_size;
|
|
$height = $target_size * (2 / 3);
|
|
}
|
|
else {
|
|
$height = count($text_lines) * $font_height +
|
|
(count($text_lines) - 1) * $line_spacing +
|
|
$border_size * 2;
|
|
foreach($text_lines as $line)
|
|
$width = max($width, $font_width * mb_strlen($line));
|
|
$width += $border_size * 2;
|
|
}
|
|
|
|
$image = imagecreatetruecolor($width, $height);
|
|
imagefill($image, 0, 0, imagecolorallocate($image, 250, 249, 251)); // Set the background to #faf8fb
|
|
|
|
$i = 0;
|
|
foreach($text_lines as $line) {
|
|
imagestring($image, $font_size,
|
|
($width / 2) - (($font_width * mb_strlen($line)) / 2),
|
|
$border_size + $i * ($font_height + $line_spacing),
|
|
$line,
|
|
imagecolorallocate($image, 68, 39, 113) // #442772
|
|
);
|
|
$i++;
|
|
}
|
|
|
|
return $image;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param boolean $absolute Whether the time should be displayed absolutely, or relative to the current time.
|
|
* @param boolean $html Whether the result should formatted as HTML (true) or plain text (false).
|
|
* @return string HTML representing the given timestamp.
|
|
*/
|
|
function render_timestamp($timestamp, $absolute = false, $html = true) {
|
|
$time_rendered = $absolute ? date("Y-m-d g:ia e", $timestamp) : human_time_since($timestamp);
|
|
if($html)
|
|
return "<time class='cursor-query' datetime='".date("c", $timestamp)."' title='" . date("l jS \of F Y \a\\t h:ia T", $timestamp) . "'>$time_rendered</time>";
|
|
else
|
|
return $time_rendered;
|
|
}
|
|
/**
|
|
* 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 = htmlentities($rchange->page);
|
|
if(isset($pageindex->$pageDisplayName) and !empty($pageindex->$pageDisplayName->redirect))
|
|
$pageDisplayName = "<em>$pageDisplayName</em>";
|
|
$pageDisplayLink = "<a href='?page=" . rawurlencode($rchange->page) . "'>$pageDisplayName</a>";
|
|
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. Note that this may contain ARBITRARY HTML! In other words, make sure that the editor name(s) are sanitized (e.g. htmlentities()'d) before padding to this function.
|
|
* @return string HTML representing the given editor's name.
|
|
*/
|
|
function render_editor($editorName) {
|
|
return "<span class='editor'>✎ $editorName</span>";
|
|
}
|
|
|
|
/**
|
|
* Minifies CSS. Uses simple computationally-cheap optimisations to reduce size.
|
|
* CSS Minification ideas by Jean from catswhocode.com
|
|
* @source http://www.catswhocode.com/blog/3-ways-to-compress-css-files-using-php
|
|
* @since 0.20.0
|
|
* @param string $css_str The string of CSS to minify.
|
|
* @return string The minified CSS string.
|
|
*/
|
|
function minify_css(string $css_str) : string {
|
|
// Remove comments
|
|
$result = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', "", $css_str);
|
|
// Cut down whitespace
|
|
$result = preg_replace('/\s+/', " ", $result);
|
|
// Remove whitespace after colons and semicolons
|
|
$result = str_replace([
|
|
" :", ": ", "; ",
|
|
" { ", " } ", "{ ", " {", "} ", " }",
|
|
", ", "0."
|
|
], [
|
|
":", ":", ";",
|
|
"{", "}", "{", "{", "}", "}",
|
|
",", "."
|
|
], $result);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Saves the settings file back to peppermint.json.
|
|
* @package core
|
|
* @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;
|
|
}
|
|
/**
|
|
* Save the page index back to disk, respecting $settings->minify_pageindex
|
|
* @package core
|
|
* @return bool Whether the page index was saved successfully or not.
|
|
*/
|
|
function save_pageindex() {
|
|
global $paths, $settings, $pageindex;
|
|
return file_put_contents(
|
|
$paths->pageindex,
|
|
json_encode($pageindex, $settings->minify_pageindex ? 0 : JSON_PRETTY_PRINT)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param bool $ignore_verification Whether to ignore user email verification status and send the email anyway. Defaults to false.
|
|
* @return bool 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(string $username, string $subject, string $body, bool $ignore_verification = false) : bool
|
|
{
|
|
global $version, $env, $settings;
|
|
|
|
static $literator = null;
|
|
if($literator == null) $literator = Transliterator::createFromRules(':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: NFC;', Transliterator::FORWARD);
|
|
|
|
// If the user doesn't have an email address, then we can't email them :P
|
|
if(empty($settings->users->{$username}->emailAddress))
|
|
return false;
|
|
|
|
// If email address verification is required but hasn't been done for this user, skip them
|
|
if(empty($env->user_data->emailAddressVerified) && !$ignore_verification)
|
|
return false;
|
|
|
|
|
|
$headers = [
|
|
"x-mailer" => ini_get("user_agent"),
|
|
"reply-to" => "$settings->admindetails_name <$settings->admindetails_email>"
|
|
];
|
|
|
|
// Correctly encode the subject
|
|
if($settings->email_subject_utf8)
|
|
$subject = "=?utf-8?B?" . base64_encode($username) . "?=";
|
|
else
|
|
$subject = $literator->transliterate($subject);
|
|
|
|
// Correctly encode the message body
|
|
if($settings->email_body_utf8)
|
|
$headers["content-type"] = "text/plain; charset=utf-8";
|
|
else {
|
|
$headers["content-type"] = "text/plain";
|
|
$body = $literator->transliterate($body);
|
|
}
|
|
|
|
$subject = str_replace("{username}", $username, $subject);
|
|
$body = str_replace("{username}", $username, $body);
|
|
|
|
$compiled_headers = "";
|
|
foreach($headers as $header => $value)
|
|
$compiled_headers .= "$header: $value\r\n";
|
|
|
|
if($settings->email_debug_dontsend) {
|
|
error_log("[PeppermintyWiki/$settings->sitename/email] Username: $username ({$settings->users->{$username}->emailAddress})
|
|
Subject: $subject
|
|
----- Headers -----
|
|
$compiled_headers
|
|
-------------------
|
|
----- Body -----
|
|
$body
|
|
----------------");
|
|
return true;
|
|
}
|
|
else
|
|
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 int The number of emails sent successfully.
|
|
*/
|
|
function email_users($usernames, string $subject, string $body) : int
|
|
{
|
|
$emailsSent = 0;
|
|
foreach($usernames as $username)
|
|
{
|
|
$emailsSent += email_user($username, $subject, $body) ? 1 : 0;
|
|
}
|
|
return $emailsSent;
|
|
}
|
|
|
|
/**
|
|
* Recursively deletes a directory and it's contents.
|
|
* Adapted by Starbeamrainbowlabs
|
|
* @param string $path The path to the directory to delete.
|
|
* @param bool $delete_self Whether to delete the top-level directory. Set this to false to delete only a directory's contents
|
|
* @source https://stackoverflow.com/questions/4490637/recursive-delete
|
|
*/
|
|
function delete_recursive($path, $delete_self = true) {
|
|
$it = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($path),
|
|
RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
foreach ($it as $file) {
|
|
if (in_array($file->getBasename(), [".", ".."]))
|
|
continue;
|
|
if($file->isDir())
|
|
rmdir($file->getPathname());
|
|
else
|
|
unlink($file->getPathname());
|
|
}
|
|
if($delete_self) rmdir($path);
|
|
}
|
|
|
|
/**
|
|
* Generates a crytographically-safe random id of the given length.
|
|
* @param int $length The length of id to generate.
|
|
* @return string The random id.
|
|
*/
|
|
function crypto_id(int $length) : string {
|
|
// It *should* be the right length already, but it doesn't hurt to be safe
|
|
return substr(strtr(
|
|
base64_encode(random_bytes($length * 0.75)),
|
|
[ "=" => "", "+" => "-", "/" => "_"]
|
|
), 0, $length);
|
|
}
|
|
|
|
/**
|
|
* Returns whether we are both on the cli AND the cli is enabled.
|
|
* @return boolean
|
|
*/
|
|
function is_cli() : bool {
|
|
global $settings;
|
|
return php_sapi_name() == "cli" &&
|
|
$settings->cli_enabled;
|
|
}
|
|
|
|
function metrics2servertiming(stdClass $perfdata) : string {
|
|
$result = [];
|
|
foreach($perfdata as $key => $value) {
|
|
$result[] = str_replace("_", "", $key).";dur=$value";
|
|
}
|
|
return "foo, ".implode(", ", $result);
|
|
}
|
|
|
|
/**
|
|
* Sets a cookie on the client via the set-cookie header.
|
|
* Uses setcookie() under-the-hood.
|
|
* @param string $key The cookie name to set.
|
|
* @param string $value The cookie value to set.
|
|
* @param int $expires The expiry time to set on the cookie.
|
|
* @return void
|
|
*/
|
|
function send_cookie(string $key, $value, int $expires) : void {
|
|
global $env, $settings;
|
|
|
|
$cookie_secure = true;
|
|
switch ($settings->cookie_secure) {
|
|
case "false":
|
|
$cookie_secure = false;
|
|
break;
|
|
case "auto":
|
|
default:
|
|
$cookie_secure = $env->is_secure;
|
|
break;
|
|
}
|
|
|
|
if(version_compare(PHP_VERSION, "7.3.0") >= 0) {
|
|
// Phew! We're running PHP 7.3+, so we're ok to use the array syntax
|
|
setcookie($key, $value, [
|
|
"expires" => $expires,
|
|
"secure" => $cookie_secure,
|
|
"httponly" => true,
|
|
"samesite" => "Strict"
|
|
]);
|
|
}
|
|
else {
|
|
if(!$env->is_secure) error_log("[pepperminty_wiki/$settings->sitename] Warning: You are using a version of PHP that is less than 7.3. This is not recommended - as the samesite cookie flag can't be set in PHP 7.3-, and this is insecure - as it opens you to session stealing attacks. In addition, browsers have deprecated non-samesite cookies in insecure contexts. Please upgrade today!");
|
|
setcookie($key, $value, $expires, "", "", $cookie_secure, true);
|
|
}
|
|
}
|