diff --git a/build/index.php b/build/index.php index 6ab42cc..89fab92 100644 --- a/build/index.php +++ b/build/index.php @@ -1,348 +1,370 @@ - * #8 - Rogue tag - nibreh - */ -// Initialises a new object to store your wiki's settings in. Please don't touch this. -$settings = new stdClass(); - -// The site's name. Used all over the place. -// Note that by default the session cookie is perfixed with a variant of the -// sitename so changing this will log everyone out! -$settings->sitename = "Pepperminty Wiki"; - -// A url that points to the site's logo. Leave blank to disable. When enabled -// the logo will be inserted next to the site name on every page. -$settings->logo_url = "//starbeamrainbowlabs.com/images/logos/peppermint.png"; - -// The side of the site name at which the logo should be placed. May be set to -// either "left" or "right". Only has an effect if the above is not set to an -// empty string. -$settings->logo_position = "left"; - -// The url from which to fetch updates. Defaults to the master (development) -// branch If there is sufficient demand, a separate stable branch will be -// created. Note that if you use the automatic updater currently it won't save -// your module choices. -// MAKE SURE THAT THIS POINTS TO A *HTTPS* URL, OTHERWISE SOMEONE COULD INJECT A VIRUS INTO YOUR WIKI -$settings->updateurl = "https://raw.githubusercontent.com/sbrl/pepperminty-wiki/master/index.php"; - -// The secret key used to perform 'dangerous' actions, like updating the wiki, -// and deleting pages. It is strongly advised that you change this! -$settings->sitesecret = "ed420502615bac9037f8f12abd4c9f02"; - -// Determined whether edit is enabled. Set to false to disable disting for all -// users (anonymous or otherwise). -$settings->editing = true; - -// The maximum number of characters allowed in a single page. The default is -// 135,000 characters, which is about 50 pages. -$settings->maxpagesize = 135000; - -// Whether page sources should be cleaned of HTML before rendering. If set to -// true any raw HTML will be escaped before rendering. Note that this shouldn't -// affect code blocks - they should alwys be escaped. It is STRONGLY -// recommended that you keep this option turned on, *ESPECIALLY* if you allow -// anonymous edits as no sanitizing what so ever is performed on the HTML. -// Also note that some parsers may override this setting and escape HTML -// sequences anyway. -$settings->clean_raw_html = true; - -// Determined whether users who aren't logged in are allowed to edit your wiki. -// Set to true to allow anonymous users to log in. -$settings->anonedits = false; - -// The name of the page that will act as the home page for the wiki. This page -// will be served if the user didn't specify a page. -$settings->defaultpage = "Main Page"; - -// The default action. This action will be performed if no other action is -// specified. It is recommended you set this to "view" - that way the user -// automatically views the default page (see above). -$settings->defaultaction = "view"; - -// The parser to use when rendering pages. Defaults to 'default', which is a -// modified version of slimdown, originally written by -// Johnny Broadway . -$settings->parser = "default"; - -// Whether to show a list of subpages at the bottom of the page. -$settings->show_subpages = true; - -// The depth to which we should display when listing subpages at the bottom of -// the page. -$settings->subpages_display_depth = 3; - -// An array of usernames and passwords - passwords should be hashed with -// sha256. Put one user / password on each line, remembering the comma at the -// end. The last user in the list doesn't need a comma after their details though. -$settings->users = [ - "admin" => "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", //password - "user" => "873ac9ffea4dd04fa719e8920cd6938f0c23cd678af330939cff53c3d2855f34" //cheese -]; - -// Whether to use the new sha3 hashing algorithm that was standardised on the -// 8th August 2015. Only works if you have strawbrary's sha3 extension -// installed. Get it here: https://github.com/strawbrary/php-sha3 -// Note: If you change this settings, make sure to update the password hashes -// above! Note that the hash action is aware of this option and will hash -// passwords appropriately based on this setting. -$settings->use_sha3 = false; - -// An array of usernames that are administrators. Administrators can delete and -// move pages. -$settings->admins = [ "admin" ]; - -// The string that is prepended before an admin's name on the nav bar. Defaults -// to a diamond shape (◆). -$settings->admindisplaychar = "◆"; - -// The string that is prepended a page's name in the page title if it is -// protected. Defaults to a lock symbol. -$settings->protectedpagechar = "🔒"; - -// Contact details for the site administrator. Since users can only be added by -// editing this file, people will need a contact address to use to ask for an -// account. Displayed at the bottom of the page, and will be appropriately -// obfusticated to deter spammers. -$settings->admindetails = [ - "name" => "Administrator", - "email" => "admin@localhost" -]; - -// Whether to only allow adminstrators to export the your wiki as a zip using -// the page-export module. -$settings->export_allow_only_admins = false; - -// Array of links and display text to display at the top of the site. -// Format: -// [ "Display Text", "Link" ] -// You can also use strings here and they will be printed as-is, except the -// following special strings: -// user-status Expands to the user's login information -// e.g. "Logged in as {name}. | Logout". -// e.g. "Browsing as Anonymous. | Login". -// -// search Expands to a search box. -// -// divider Expands to a divider to separate stuff. -// -// more Expands to the "More..." submenu. -$settings->nav_links = [ - "user-status", - [ "Home", "index.php" ], -// [ "Login", "index.php?action=login" ], - "search", - [ "Read", "index.php?page={page}" ], - [ "Edit", "index.php?action=edit&page={page}" ], - [ "Printable", "index.php?action=view&printable=yes&page={page}" ], - //"divider", - [ "All Pages", "index.php?action=list" ], - "menu" -]; -// An array of additional links in the above format that will be shown under -// "More" subsection. -$settings->nav_links_extra = [ - [ "Upload", "index.php?action=upload" ], - [ $settings->admindisplaychar . "Delete", "index.php?action=delete&page={page}" ], - [ $settings->admindisplaychar . "Move", "index.php?action=move&page={page}" ], - [ $settings->admindisplaychar . "Toggle Protection", "index.php?action=protect&page={page}" ] -]; - -// An array of links in the above format that will be shown at the bottom of -// the page. -$settings->nav_links_bottom = [ - [ "Credits", "index.php?action=credits" ], - [ "Help", "index.php?action=help" ] -]; - -// A message that will appear at the bottom of every page. May contain HTML. -$settings->footer_message = "All content is under this license. Please make sure that you read and understand the license, especially if you are thinking about copying some (or all) of this site's content, as it may restrict you from doing so."; - -// A message that will appear just before the submit button on the editing -// page. May contain HTML. -$settings->editing_message = "By submitting your edit, you are agreeing to release your changes under this license. Also note that if you don't want your work to be edited by other users of this site, please don't submit it here!"; - -// Whether to allow image uploads to the server. Currently disabled temporarily -// for security reasons while I finish writing the file uploader. -$settings->upload_enabled = true; - -// An array of mime types that are allowed to be uploaded. -$settings->upload_allowed_file_types = [ - "image/jpeg", - "image/png", - "image/gif", - "image/webp" -]; - -// The default file type for previews. Defaults to image/png. Also supports -// image/jpeg and image/webp. image/webp is a new image format that reduces -// image sizez by ~20%, but PHP still has some issues with invalid webp images. -$settings->preview_file_type = "image/png"; - -// The default size of preview images. -$settings->default_preview_size = 640; - -// The location of a file that maps mime types onto file extensions and vice -// versa. Used to generate the file extension for an uploaded file. Set to the -// default location of the mime.types file on Linux. If you aren't using linux, -// download this pastebin and point this setting at it instead: -// http://pastebin.com/mjM3zKjz -$settings->mime_extension_mappings_location = "/etc/mime.types"; - -// The minimum and maximum sizes of generated preview images in pixels. -$settings->min_preview_size = 1; -$settings->max_preview_size = 2048; - -// The maximum distance terms should be apart in the context display below -// search results. This is purely aesthetical - it doesn't affect the search -// algorithm. -$settings->search_max_distance_context_display = 100; - -// The number of characters that should be displayed either side of a matching -// term in the context below each search result. -$settings->search_characters_context = 200; - -// The weighting to give to search term matches found in a page's title. -$settings->search_title_matches_weighting = 10; - -// The weighting to give to search term matches found in a page's tags. -$settings->search_tags_matches_weighting = 3; - -// A string of css to include. Will be included in the of every page -// inside a "; } - + public static $nav_divider = " | "; - + /* * @summary Function to render a navigation bar from an array of links. See * $settings->nav_links for format information. - * + * * @param $nav_links - The links to add to the navigation bar. * @param $nav_links_extra - The extra nav links to add to the "More..." * menu. @@ -973,7 +968,7 @@ class page_renderer { global $settings, $env; $result = ""; return $result; } @@ -1031,21 +1026,21 @@ class page_renderer if(in_array($name, $settings->admins)) $result .= $settings->admindisplaychar; $result .= $name; - + return $result; } - + public static function generate_all_pages_datalist() { global $pageindex; - + $result = "\n"; foreach($pageindex as $pagename => $pagedetails) { $result .= "\t\t\t"; - + return $result; } } @@ -1102,7 +1097,7 @@ function add_parser($name, $parser_code) global $parsers; if(isset($parsers[$name])) throw new Exception("Can't register parser with name '$name' because a parser with that name already exists."); - + $parsers[$name] = $parser_code; } function parse_page_source($source) @@ -1110,7 +1105,7 @@ 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); @@ -1374,787 +1369,787 @@ register_module([ - -register_module([ - "name" => "Search", - "version" => "0.1", - "author" => "Starbeamrainbowlabs", - "description" => "Adds proper search functionality to Pepperminty Wiki. Note that this module, at the moment, just contains test code while I figure out how best to write a search engine.", - "id" => "feature-search", - "code" => function() { - add_action("index", function() { - global $settings, $env; - - $breakable_chars = "\r\n\t .,\\/!\"£$%^&*[]()+`_~#"; - - header("content-type: text/plain"); - - $source = file_get_contents("$env->page.md"); - - $index = search::index($source); - - var_dump($env->page); - var_dump($source); - - var_dump($index); - }); - - add_action("invindex-rebuild", function() { - search::rebuild_invindex(); - }); - - add_action("search", function() { - global $settings, $env, $pageindex; - - if(!isset($_GET["query"])) - exit(page_renderer::render("No Search Terms - Error - $settings->sitename", "

You didn't specify any search terms. Try typing some into the box above.

")); - - $search_start = microtime(true); - - $invindex = search::load_invindex("invindex.json"); - $results = search::query_invindex($_GET["query"], $invindex); - - $search_end = microtime(true) - $search_start; - - $title = $_GET["query"] . " - Search results - $settings->sitename"; - - $content = "
\n"; - $content .= "

Search Results

"; - - /// Search Box /// - $content .= "
\n"; - $content .= " \n"; - $content .= " \n"; - $content .= "
"; - - $query = $_GET["query"]; - if(isset($pageindex->$query)) - { - $content .= "

There's a page on $settings->sitename called $query.

"; - } - else - { - $content .= "

There isn't a page called $query on $settings->sitename, but you can create it.

"; - } - - $i = 0; // todo use $_GET["offset"] and $_GET["result-count"] or something - foreach($results as $result) - { - $link = "?page=" . rawurlencode($result["pagename"]); - $pagesource = file_get_contents($result["pagename"] . ".md"); - $context = search::extract_context($_GET["query"], $pagesource); - $context = search::highlight_context($_GET["query"], $context); - /*if(strlen($context) == 0) - { - $context = search::strip_markup(file_get_contents("$env->page.md", null, null, null, $settings->search_characters_context * 2)); - if($pageindex->{$env->page}->size > $settings->search_characters_context * 2) - $context .= "..."; - }*/ - - - // We add 1 to $i here to convert it from an index to a result - // number as people expect it to start from 1 - $content .= "
\n"; - $content .= "

" . $result["pagename"] . "

\n"; - $content .= "

$context

\n"; - $content .= "
\n"; - - $i++; - } - - $content .= "
\n"; - - exit(page_renderer::render($title, $content)); - - //header("content-type: text/plain"); - //var_dump($results); - }); - } -]); - -class search -{ - // Words that we should exclude from the inverted index. - public static $stop_words = [ - "a", "about", "above", "above", "across", "after", "afterwards", "again", - "against", "all", "almost", "alone", "along", "already", "also", - "although", "always", "am", "among", "amongst", "amoungst", "amount", - "an", "and", "another", "any", "anyhow", "anyone", "anything", "anyway", - "anywhere", "are", "around", "as", "at", "back", "be", "became", - "because", "become", "becomes", "becoming", "been", "before", - "beforehand", "behind", "being", "below", "beside", "besides", - "between", "beyond", "bill", "both", "bottom", "but", "by", "call", - "can", "cannot", "cant", "co", "con", "could", "couldnt", "cry", "de", - "describe", "detail", "do", "done", "down", "due", "during", "each", - "eg", "eight", "either", "eleven", "else", "elsewhere", "empty", - "enough", "etc", "even", "ever", "every", "everyone", "everything", - "everywhere", "except", "few", "fifteen", "fify", "fill", "find", - "fire", "first", "five", "for", "former", "formerly", "forty", "found", - "four", "from", "front", "full", "further", "get", "give", "go", "had", - "has", "hasnt", "have", "he", "hence", "her", "here", "hereafter", - "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", - "his", "how", "however", "hundred", "ie", "if", "in", "inc", "indeed", - "interest", "into", "is", "it", "its", "itself", "keep", "last", - "latter", "latterly", "least", "less", "ltd", "made", "many", "may", - "me", "meanwhile", "might", "mine", "more", "moreover", "most", - "mostly", "move", "much", "must", "my", "myself", "name", "namely", - "neither", "never", "nevertheless", "next", "nine", "no", "none", - "nor", "not", "nothing", "now", "nowhere", "of", "off", "often", "on", - "once", "one", "only", "onto", "or", "other", "others", "otherwise", - "our", "ours", "ourselves", "out", "over", "own", "part", "per", - "perhaps", "please", "put", "rather", "re", "same", "see", "seem", - "seemed", "seeming", "seems", "serious", "several", "she", "should", - "show", "side", "since", "sincere", "six", "sixty", "so", "some", - "somehow", "someone", "something", "sometime", "sometimes", - "somewhere", "still", "such", "system", "take", "ten", "than", "that", - "the", "their", "them", "themselves", "then", "thence", "there", - "thereafter", "thereby", "therefore", "therein", "thereupon", "these", - "they", "thickv", "thin", "third", "this", "those", "though", "three", - "through", "throughout", "thru", "thus", "to", "together", "too", "top", - "toward", "towards", "twelve", "twenty", "two", "un", "under", "until", - "up", "upon", "us", "very", "via", "was", "we", "well", "were", "what", - "whatever", "when", "whence", "whenever", "where", "whereafter", - "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", - "which", "while", "whither", "who", "whoever", "whole", "whom", "whose", - "why", "will", "with", "within", "without", "would", "yet", "you", - "your", "yours", "yourself", "yourselves" - ]; - - public static function index($source) - { - $source = html_entity_decode($source, ENT_QUOTES); - $source_length = strlen($source); - - $index = []; - - $terms = self::tokenize($source); - $i = 0; - foreach($terms as $term) - { - $nterm = $term; - - // Skip over stop words (see https://en.wikipedia.org/wiki/Stop_words) - if(in_array($nterm, self::$stop_words)) continue; - - if(!isset($index[$nterm])) - { - $index[$nterm] = [ "freq" => 0, "offsets" => [] ]; - } - - $index[$nterm]["freq"]++; - $index[$nterm]["offsets"][] = $i; - - $i++; - } - - return $index; - } - - public static function tokenize($source) - { - $source = strtolower($source); - return preg_split("/((^\p{P}+)|(\p{P}*\s+\p{P}*)|(\p{P}+$))|\|/", $source, -1, PREG_SPLIT_NO_EMPTY); - } - - public static function strip_markup($source) - { - return str_replace([ "[", "]", "\"", "*", "_", " - ", "`" ], "", $source); - } - - public static function rebuild_invindex() - { - global $pageindex; - - $invindex = []; - foreach($pageindex as $pagename => $pagedetails) - { - $pagesource = file_get_contents("$pagename.md"); - $index = self::index($pagesource); - - self::merge_into_invindex($invindex, ids::getid($pagename), $index); - } - - self::save_invindex("invindex.json", $invindex); - } - - /* - * @summary Sorts an index alphabetically. Will also sort an inverted index. - * This allows us to do a binary search instead of a regular - * sequential search. - */ - public static function sort_index(&$index) - { - ksort($index, SORT_NATURAL); - } - - /* - * @summary Compares two *regular* indexes to find the differences between them. - * - * @param {array} $indexa - The old index. - * @param {array} $indexb - The new index. - * @param {array} $changed - An array to be filled with the nterms of all - * the changed entries. - * @param {array} $removed - An array to be filled with the nterms of all - * the removed entries. - */ - public static function compare_indexes($oldindex, $newindex, &$changed, &$removed) - { - foreach($oldindex as $nterm => $entry) - { - if(!isset($newindex[$nterm])) - $removed[] = $nterm; - } - foreach($newindex as $nterm => $entry) - { - if(!isset($oldindex[$nterm]) or // If this world is new - $newindex[$nterm] !== $oldindex[$nterm]) // If this word has changed - $changed[$nterm] = $newindex[$nterm]; - } - } - - /* - * @summary Reads in and parses an inverted index. - */ - // Todo remove this function and make everything streamable - public static function load_invindex($invindex_filename) { - $invindex = json_decode(file_get_contents($invindex_filename), true); - return $invindex; - } - - /* - * @summary Merge an index into an inverted index. - */ - public static function merge_into_invindex(&$invindex, $pageid, &$index, &$removals = []) - { - // Remove all the subentries that were removed since last time - foreach($removals as $nterm) - { - unset($invindex[$nterm][$pageid]); - } - - // Merge all the new / changed index entries into the inverted index - foreach($index as $nterm => $newentry) - { - // If the nterm isn't in the inverted index, then create a space for it - if(!isset($invindex[$nterm])) $invindex[$nterm] = []; - $invindex[$nterm][$pageid] = $newentry; - - // Sort the page entries for this word by frequency - uasort($invindex[$nterm], function($a, $b) { - if($a["freq"] == $b["freq"]) return 0; - return ($a["freq"] < $b["freq"]) ? +1 : -1; - }); - } - - // Sort the inverted index by rank - uasort($invindex, function($a, $b) { - $ac = count($a); $bc = count($b); - if($ac == $bc) return 0; - return ($ac < $bc) ? +1 : -1; - }); - } - - public static function save_invindex($filename, &$invindex) - { - file_put_contents($filename, json_encode($invindex)); - } - - public static function query_invindex($query, &$invindex) - { - global $settings, $pageindex; - - $query_terms = self::tokenize($query); - $matching_pages = []; - - - // Loop over each term in the query and find the matching page entries - for($i = 0; $i < count($query_terms); $i++) - { - $qterm = $query_terms[$i]; - - // Only search the inverted index if it actually exists there - if(isset($invindex[$qterm])) - { - // Loop over each page in the inverted index entry - foreach($invindex[$qterm] as $pageid => $page_entry) - { - // Create an entry in the matching pages array if it doesn't exist - if(!isset($matching_pages[$pageid])) - $matching_pages[$pageid] = [ "nterms" => [] ]; - $matching_pages[$pageid]["nterms"][$qterm] = $page_entry; - } - } - - - // Loop over the pageindex and search the titles / tags - foreach ($pageindex as $pagename => $pagedata) - { - // Get the current page's id - $pageid = ids::getid($pagename); - // Consider matches in the page title - if(stripos($pagename, $qterm) !== false) - { - // We found the qterm in the title - if(!isset($matching_pages[$pageid])) - $matching_pages[$pageid] = [ "nterms" => [] ]; - - // Set up a counter for page title matches if it doesn't exist already - if(!isset($matching_pages[$pageid]["title-matches"])) - $matching_pages[$pageid]["title-matches"] = 0; - - $matching_pages[$pageid]["title-matches"] += count(mb_stripos_all($pagename, $qterm)); - } - - // Consider matches in the page's tags - if(isset($pagedata->tags) and // If this page has tags - stripos(implode(" ", $pagedata->tags), $qterm) !== false) // And we found the qterm in the tags - { - if(!isset($matching_pages[$pageid])) - $matching_pages[$pageid] = [ "nterms" => [] ]; - - // Set up a counter for tag match if there isn't one already - if(!isset($matching_pages[$pageid]["tag-matches"])) - $matching_pages[$pageid]["tag-matches"] = 0; - $matching_pages[$pageid]["tag-matches"] += count(mb_stripos_all(implode(" ", $pagedata->tags), $qterm)); - } - } - } - - - foreach($matching_pages as $pageid => &$pagedata) - { - $pagedata["pagename"] = ids::getpagename($pageid); - $pagedata["rank"] = 0; - - foreach($pagedata["nterms"] as $pterm => $entry) - { - $pagedata["rank"] += $entry["freq"]; - - // todo rank by context here - } - - // Consider matches in the title / tags - if(isset($pagedata["title-matches"])) - $pagedata["rank"] += $pagedata["title-matches"] * $settings->search_title_matches_weighting; - if(isset($pagedata["tag-matches"])) - $pagedata["rank"] += $pagedata["tag-matches"] * $settings->search_tags_matches_weighting; - - // todo remove items if the rank is below a threshold - } - - // todo sort by rank here - uasort($matching_pages, function($a, $b) { - if($a["rank"] == $b["rank"]) return 0; - return ($a["rank"] < $b["rank"]) ? +1 : -1; - }); - - return $matching_pages; - } - - public static function extract_context($query, $source) - { - global $settings; - - $nterms = self::tokenize($query); - $matches = []; - // Loop over each nterm and find it in the source - foreach($nterms as $nterm) - { - $all_offsets = mb_stripos_all($source, $nterm); - // Skip over adding matches if there aren't any - if($all_offsets === false) - continue; - foreach($all_offsets as $offset) - { - $matches[] = [ $nterm, $offset ]; - } - } - - usort($matches, function($a, $b) { - if($a[1] == $b[1]) return 0; - return ($a[1] < $b[1]) ? +1 : -1; - }); - - $contexts = []; - $basepos = 0; - $matches_count = count($matches); - while($basepos < $matches_count) - { - // Store the next match along - all others will be relative to that - // one - $group = [$matches[$basepos]]; - - // Start scanning at the next one along - we always store the first match - $scanpos = $basepos + 1; - $distance = 0; - - while(true) - { - // Break out if we reach the end - if($scanpos >= $matches_count) break; - - // Find the distance between the current one and the last one - $distance = $matches[$scanpos][1] - $matches[$scanpos - 1][1]; - - // Store it if the distance is below the threshold - if($distance < $settings->search_characters_context) - $group[] = $matches[$scanpos]; - else - break; - - $scanpos++; - } - - $context_start = $group[0][1] - $settings->search_characters_context; - $context_end = $group[count($group) - 1][1] + $settings->search_characters_context; - - $context = substr($source, $context_start, $context_end - $context_start); - - // Strip the markdown from the context - it's most likely going to - // be broken anyway. - $context = self::strip_markup($context); - - $contexts[] = $context; - - $basepos = $scanpos + 1; - } - - return implode(" ... ", $contexts); - } - - public static function highlight_context($query, $context) - { - $qterms = self::tokenize($query); - - foreach($qterms as $qterm) - { - // From http://stackoverflow.com/a/2483859/1460422 - $context = preg_replace("/" . preg_quote($qterm) . "/i", "$0", $context); - } - - return $context; - } -} - - - -register_module([ - "name" => "Uploader", - "version" => "0.1", - "author" => "Starbeamrainbowlabs", - "description" => "Adds the ability to upload files to Pepperminty Wiki. Uploaded files act as pages and have the special 'File:' prefix.", - "id" => "feature-upload", - "code" => function() { - add_action("upload", function() { - global $settings, $env, $pageindex; - - - switch($_SERVER["REQUEST_METHOD"]) - { - case "GET": - // Send upload page - - if(!$settings->upload_enabled) - exit(page_renderer::render("Upload Disabled - $setting->sitename", "

You can't upload anything at the moment because $settings->sitename has uploads disabled. Try contacting " . $settings->admindetails["name"] . ", your site Administrator. Go back.

")); - if(!$env->is_logged_in) - exit(page_renderer::render("Upload Error - $settings->sitename", "

You are not currently logged in, so you can't upload anything.

-

Try logging in first.

")); - - exit(page_renderer::render("Upload - $settings->sitename", "

Select an image below, and then type a name for it in the box. This server currently supports uploads up to " . human_filesize(get_max_upload_size()) . " in size.

-

$settings->sitename currently supports uploading of the following file types: " . implode(", ", $settings->upload_allowed_file_types) . ".

-
- - -
- - -
- - -
- -
")); - - break; - - case "POST": - // Recieve file - - // Make sure uploads are enabled - if(!$settings->upload_enabled) - { - unlink($_FILES["file"]["tmp_name"]); - http_response_code(412); - exit(page_renderer::render("Upload failed - $settings->sitename", "

Your upload couldn't be processed because uploads are currently disabled on $settings->sitename. Go back to the main page.

")); - } - - // Make sure that the user is logged in - if(!$env->is_logged_in) - { - unlink($_FILES["file"]["tmp_name"]); - http_response_code(401); - exit(page_renderer::render("Upload failed - $settings->sitename", "

Your upload couldn't be processed because you are not logged in.

Try logging in first.")); - } - - // Calculate the target ename, removing any characters we - // are unsure about. - $target_name = makepathsafe($_POST["name"]); - $temp_filename = $_FILES["file"]["tmp_name"]; - - $mimechecker = finfo_open(FILEINFO_MIME_TYPE); - $mime_type = finfo_file($mimechecker, $temp_filename); - finfo_close($mimechecker); - - // Perform appropriate checks based on the *real* filetype - switch(substr($mime_type, 0, strpos($mime_type, "/"))) - { - case "image": - $extra_data = []; - $imagesize = getimagesize($temp_filename, $extra_data); - // Make sure that the image size is defined - if(!is_int($imagesize[0]) or !is_int($imagesize[1])) - { - http_response_code(415); - exit(page_renderer::render("Upload Error - $settings->sitename", "

The file that you uploaded doesn't appear to be an image. $settings->sitename currently only supports uploading images (videos coming soon). Go back to try again.

")); - } - - break; - - case "video": - http_response_code(501); - exit(page_renderer::render("Upload Error - $settings->sitename", "

You uploaded a video, but $settings->sitename doesn't support them yet. Please try again later.

")); - - default: - http_response_code(415); - exit(page_renderer::render("Upload Error - $settings->sitename", "

You uploaded an unnknown file type which couldn't be processed. $settings->sitename thinks that the file you uploaded was a(n) '$mime_type', which isn't supported.

")); - } - - $file_extension = system_mime_type_extension($mime_type); - - $new_filename = "Files/$target_name.$file_extension"; - $new_description_filename = "$new_filename.md"; - - if(isset($pageindex->$new_filename)) - exit(page_renderer::render("Upload Error - $settings->sitename", "

A page or file has already been uploaded with the name '$new_filename'. Try deleting it first. If you do not have permission to delete things, try contacting one of the moderators.

")); - - if(!file_exists("Files")) - mkdir("Files", 0664); - - if(!move_uploaded_file($temp_filename, $new_filename)) - { - http_response_code(409); - exit(page_renderer::render("Upload Error - $settings->sitename", "

The file you uploaded was valid, but $settings->sitename couldn't verify that it was tampered with during the upload process. This probably means that $settings->sitename has been attacked. Please contact " . $settings->admindetails . ", your $settings->sitename Administrator.

")); - } - - file_put_contents($new_description_filename, $_POST["description"]); - - $description = $_POST["description"]; - - if($settings->clean_raw_html) - $description = htmlentities($description, ENT_QUOTES); - - file_put_contents($new_description_filename, $description); - - // Construct a new entry for the pageindex - $entry = new stdClass(); - // Point to the description's filepath since this property - // should point to a markdown file - $entry->filename = $new_description_filename; - $entry->size = strlen($description); - $entry->lastmodified = time(); - $entry->lasteditor = $env->user; - $entry->uploadedfile = true; - $entry->uploadedfilepath = $new_filename; - $entry->uploadedfilemime = $mime_type; - // Add the new entry to the pageindex - // Assign the new entry to the image's filepath as that - // should be the page name. - $pageindex->$new_filename = $entry; - - // Save the pageindex - file_put_contents("pageindex.json", json_encode($pageindex, JSON_PRETTY_PRINT)); - - header("location: ?action=view&page=$new_filename&upload=success"); - - break; - } - }); - add_action("preview", function() { - global $settings, $env, $pageindex; - - $filepath = $pageindex->{$env->page}->uploadedfilepath; - $mime_type = $pageindex->{$env->page}->uploadedfilemime; - - // Determine the target size of the image - $target_size = 512; - if(isset($_GET["size"])) - $target_size = intval($_GET["size"]); - if($target_size < $settings->min_preview_size) - $target_size = $settings->min_preview_size; - if($target_size > $settings->max_preview_size) - $target_size = $settings->max_preview_size; - - // Determine the output file type - $output_mime = $settings->preview_file_type; - if(isset($_GET["type"]) and in_array($_GET["type"], [ "image/png", "image/jpeg", "image/webp" ])) - $output_mime = $_GET["type"]; - - switch(substr($mime_type, 0, strpos($mime_type, "/"))) - { - case "image": - $image = false; - switch($mime_type) - { - case "image/jpeg": - $image = imagecreatefromjpeg($filepath); - break; - case "image/gif": - $image = imagecreatefromgif($filepath); - break; - case "image/png": - $image = imagecreatefrompng($filepath); - break; - case "image/webp": - $image = imagecreatefromwebp($filepath); - break; - default: - http_response_code(415); - $image = errorimage("Unsupported image type."); - break; - } - - $raw_width = imagesx($image); - $raw_height = imagesy($image); - - $preview_image = resize_image($image, $target_size); - header("content-type: $output_mime"); - switch($output_mime) - { - case "image/jpeg": - imagejpeg($preview_image); - break; - case "image/png": - imagepng($preview_image); - break; - default: - case "image/webp": - imagewebp($preview_image); - break; - } - imagedestroy($preview_image); - break; - - default: - http_response_code(501); - exit("Unrecognised file type."); - } - }); - - page_renderer::register_part_preprocessor(function(&$parts) { - global $pageindex, $env, $settings; - // Todo add the preview to the top of the page here, but only if the current action is view and we are on a page that is a file - if(isset($pageindex->{$env->page}->uploadedfile) and $pageindex->{$env->page}->uploadedfile == true) - { - // We are looking at a page that is paired with an uploaded file - $filepath = $pageindex->{$env->page}->uploadedfilepath; - $mime_type = $pageindex->{$env->page}->uploadedfilemime; - $image_link = "//" . $_SERVER["SERVER_NAME"] . dirname($_SERVER["SCRIPT_NAME"]) . $filepath; - - $preview_sizes = [ 256, 512, 768, 1024, 1536 ]; - $preview_html = "
- - -
-

File Information

- - - -
Name" . str_replace("File/", "", $filepath) . "
Type$mime_type
Size" . human_filesize(filesize($filepath)) . "
Uploaded by" . $pageindex->{$env->page}->lasteditor . "
-

Description

"; - - $parts["{content}"] = str_replace("", "\n$preview_html", $parts["{content}"]); - } - }); - } -]); - -//// Pair of functions to calculate the actual maximum upload size supported by the server -//// Lifted from Drupal by @meustrus from Stackoverflow. Link to answer: -//// http://stackoverflow.com/a/25370978/1460422 -// Returns a file size limit in bytes based on the PHP upload_max_filesize -// and post_max_size -function get_max_upload_size() -{ - static $max_size = -1; - if ($max_size < 0) { - // Start with post_max_size. - $max_size = parse_size(ini_get('post_max_size')); - // If upload_max_size is less, then reduce. Except if upload_max_size is - // zero, which indicates no limit. - $upload_max = parse_size(ini_get('upload_max_filesize')); - if ($upload_max > 0 && $upload_max < $max_size) { - $max_size = $upload_max; - } - } - return $max_size; -} - -function parse_size($size) { - $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size. - $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size. - if ($unit) { - // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. - return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); - } else { - return round($size); - } -} - -function errorimage($text) -{ - $width = 640; - $height = 480; - $image = imagecreatetruecolor($width, $height); - imagefill($image, 0, 0, imagecolorallocate($image, 238, 232, 242)); // Set the background to #eee8f2 - $fontwidth = imagefontwidth(3); - imagetext($image, 3, - ($width / 2) - (($fontwidth * strlen($text)) / 2), - ($height / 2) - (imagefontheight(3) / 2), - $text, - imagecolorallocate($image, 17, 17, 17) // #111111 - ); - - return $image; -} - -function resize_image($image, $size) -{ - $cur_width = imagesx($image); - $cur_height = imagesy($image); - - if($cur_width < $size and $cur_height < $size) - return $image; - - $width_ratio = $size / $cur_width; - $height_ratio = $size / $cur_height; - $ratio = min($width_ratio, $height_ratio); - - $new_height = floor($cur_height * $ratio); - $new_width = floor($cur_width * $ratio); - - header("x-resize-to: $new_width x $new_height\n"); - - return imagescale($image, $new_width, $new_height); -} - - +register_module([ + "name" => "Search", + "version" => "0.1", + "author" => "Starbeamrainbowlabs", + "description" => "Adds proper search functionality to Pepperminty Wiki. Note that this module, at the moment, just contains test code while I figure out how best to write a search engine.", + "id" => "feature-search", + "code" => function() { + add_action("index", function() { + global $settings, $env; + + $breakable_chars = "\r\n\t .,\\/!\"£$%^&*[]()+`_~#"; + + header("content-type: text/plain"); + + $source = file_get_contents("$env->page.md"); + + $index = search::index($source); + + var_dump($env->page); + var_dump($source); + + var_dump($index); + }); + + add_action("invindex-rebuild", function() { + search::rebuild_invindex(); + }); + + add_action("search", function() { + global $settings, $env, $pageindex; + + if(!isset($_GET["query"])) + exit(page_renderer::render("No Search Terms - Error - $settings->sitename", "

You didn't specify any search terms. Try typing some into the box above.

")); + + $search_start = microtime(true); + + $invindex = search::load_invindex("invindex.json"); + $results = search::query_invindex($_GET["query"], $invindex); + + $search_end = microtime(true) - $search_start; + + $title = $_GET["query"] . " - Search results - $settings->sitename"; + + $content = "
\n"; + $content .= "

Search Results

"; + + /// Search Box /// + $content .= "
\n"; + $content .= " \n"; + $content .= " \n"; + $content .= "
"; + + $query = $_GET["query"]; + if(isset($pageindex->$query)) + { + $content .= "

There's a page on $settings->sitename called $query.

"; + } + else + { + $content .= "

There isn't a page called $query on $settings->sitename, but you can create it.

"; + } + + $i = 0; // todo use $_GET["offset"] and $_GET["result-count"] or something + foreach($results as $result) + { + $link = "?page=" . rawurlencode($result["pagename"]); + $pagesource = file_get_contents($result["pagename"] . ".md"); + $context = search::extract_context($_GET["query"], $pagesource); + $context = search::highlight_context($_GET["query"], $context); + /*if(strlen($context) == 0) + { + $context = search::strip_markup(file_get_contents("$env->page.md", null, null, null, $settings->search_characters_context * 2)); + if($pageindex->{$env->page}->size > $settings->search_characters_context * 2) + $context .= "..."; + }*/ + + + // We add 1 to $i here to convert it from an index to a result + // number as people expect it to start from 1 + $content .= "
\n"; + $content .= "

" . $result["pagename"] . "

\n"; + $content .= "

$context

\n"; + $content .= "
\n"; + + $i++; + } + + $content .= "
\n"; + + exit(page_renderer::render($title, $content)); + + //header("content-type: text/plain"); + //var_dump($results); + }); + } +]); + +class search +{ + // Words that we should exclude from the inverted index. + public static $stop_words = [ + "a", "about", "above", "above", "across", "after", "afterwards", "again", + "against", "all", "almost", "alone", "along", "already", "also", + "although", "always", "am", "among", "amongst", "amoungst", "amount", + "an", "and", "another", "any", "anyhow", "anyone", "anything", "anyway", + "anywhere", "are", "around", "as", "at", "back", "be", "became", + "because", "become", "becomes", "becoming", "been", "before", + "beforehand", "behind", "being", "below", "beside", "besides", + "between", "beyond", "bill", "both", "bottom", "but", "by", "call", + "can", "cannot", "cant", "co", "con", "could", "couldnt", "cry", "de", + "describe", "detail", "do", "done", "down", "due", "during", "each", + "eg", "eight", "either", "eleven", "else", "elsewhere", "empty", + "enough", "etc", "even", "ever", "every", "everyone", "everything", + "everywhere", "except", "few", "fifteen", "fify", "fill", "find", + "fire", "first", "five", "for", "former", "formerly", "forty", "found", + "four", "from", "front", "full", "further", "get", "give", "go", "had", + "has", "hasnt", "have", "he", "hence", "her", "here", "hereafter", + "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", + "his", "how", "however", "hundred", "ie", "if", "in", "inc", "indeed", + "interest", "into", "is", "it", "its", "itself", "keep", "last", + "latter", "latterly", "least", "less", "ltd", "made", "many", "may", + "me", "meanwhile", "might", "mine", "more", "moreover", "most", + "mostly", "move", "much", "must", "my", "myself", "name", "namely", + "neither", "never", "nevertheless", "next", "nine", "no", "none", + "nor", "not", "nothing", "now", "nowhere", "of", "off", "often", "on", + "once", "one", "only", "onto", "or", "other", "others", "otherwise", + "our", "ours", "ourselves", "out", "over", "own", "part", "per", + "perhaps", "please", "put", "rather", "re", "same", "see", "seem", + "seemed", "seeming", "seems", "serious", "several", "she", "should", + "show", "side", "since", "sincere", "six", "sixty", "so", "some", + "somehow", "someone", "something", "sometime", "sometimes", + "somewhere", "still", "such", "system", "take", "ten", "than", "that", + "the", "their", "them", "themselves", "then", "thence", "there", + "thereafter", "thereby", "therefore", "therein", "thereupon", "these", + "they", "thickv", "thin", "third", "this", "those", "though", "three", + "through", "throughout", "thru", "thus", "to", "together", "too", "top", + "toward", "towards", "twelve", "twenty", "two", "un", "under", "until", + "up", "upon", "us", "very", "via", "was", "we", "well", "were", "what", + "whatever", "when", "whence", "whenever", "where", "whereafter", + "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", + "which", "while", "whither", "who", "whoever", "whole", "whom", "whose", + "why", "will", "with", "within", "without", "would", "yet", "you", + "your", "yours", "yourself", "yourselves" + ]; + + public static function index($source) + { + $source = html_entity_decode($source, ENT_QUOTES); + $source_length = strlen($source); + + $index = []; + + $terms = self::tokenize($source); + $i = 0; + foreach($terms as $term) + { + $nterm = $term; + + // Skip over stop words (see https://en.wikipedia.org/wiki/Stop_words) + if(in_array($nterm, self::$stop_words)) continue; + + if(!isset($index[$nterm])) + { + $index[$nterm] = [ "freq" => 0, "offsets" => [] ]; + } + + $index[$nterm]["freq"]++; + $index[$nterm]["offsets"][] = $i; + + $i++; + } + + return $index; + } + + public static function tokenize($source) + { + $source = strtolower($source); + return preg_split("/((^\p{P}+)|(\p{P}*\s+\p{P}*)|(\p{P}+$))|\|/", $source, -1, PREG_SPLIT_NO_EMPTY); + } + + public static function strip_markup($source) + { + return str_replace([ "[", "]", "\"", "*", "_", " - ", "`" ], "", $source); + } + + public static function rebuild_invindex() + { + global $pageindex; + + $invindex = []; + foreach($pageindex as $pagename => $pagedetails) + { + $pagesource = file_get_contents("$pagename.md"); + $index = self::index($pagesource); + + self::merge_into_invindex($invindex, ids::getid($pagename), $index); + } + + self::save_invindex("invindex.json", $invindex); + } + + /* + * @summary Sorts an index alphabetically. Will also sort an inverted index. + * This allows us to do a binary search instead of a regular + * sequential search. + */ + public static function sort_index(&$index) + { + ksort($index, SORT_NATURAL); + } + + /* + * @summary Compares two *regular* indexes to find the differences between them. + * + * @param {array} $indexa - The old index. + * @param {array} $indexb - The new index. + * @param {array} $changed - An array to be filled with the nterms of all + * the changed entries. + * @param {array} $removed - An array to be filled with the nterms of all + * the removed entries. + */ + public static function compare_indexes($oldindex, $newindex, &$changed, &$removed) + { + foreach($oldindex as $nterm => $entry) + { + if(!isset($newindex[$nterm])) + $removed[] = $nterm; + } + foreach($newindex as $nterm => $entry) + { + if(!isset($oldindex[$nterm]) or // If this world is new + $newindex[$nterm] !== $oldindex[$nterm]) // If this word has changed + $changed[$nterm] = $newindex[$nterm]; + } + } + + /* + * @summary Reads in and parses an inverted index. + */ + // Todo remove this function and make everything streamable + public static function load_invindex($invindex_filename) { + $invindex = json_decode(file_get_contents($invindex_filename), true); + return $invindex; + } + + /* + * @summary Merge an index into an inverted index. + */ + public static function merge_into_invindex(&$invindex, $pageid, &$index, &$removals = []) + { + // Remove all the subentries that were removed since last time + foreach($removals as $nterm) + { + unset($invindex[$nterm][$pageid]); + } + + // Merge all the new / changed index entries into the inverted index + foreach($index as $nterm => $newentry) + { + // If the nterm isn't in the inverted index, then create a space for it + if(!isset($invindex[$nterm])) $invindex[$nterm] = []; + $invindex[$nterm][$pageid] = $newentry; + + // Sort the page entries for this word by frequency + uasort($invindex[$nterm], function($a, $b) { + if($a["freq"] == $b["freq"]) return 0; + return ($a["freq"] < $b["freq"]) ? +1 : -1; + }); + } + + // Sort the inverted index by rank + uasort($invindex, function($a, $b) { + $ac = count($a); $bc = count($b); + if($ac == $bc) return 0; + return ($ac < $bc) ? +1 : -1; + }); + } + + public static function save_invindex($filename, &$invindex) + { + file_put_contents($filename, json_encode($invindex)); + } + + public static function query_invindex($query, &$invindex) + { + global $settings, $pageindex; + + $query_terms = self::tokenize($query); + $matching_pages = []; + + + // Loop over each term in the query and find the matching page entries + for($i = 0; $i < count($query_terms); $i++) + { + $qterm = $query_terms[$i]; + + // Only search the inverted index if it actually exists there + if(isset($invindex[$qterm])) + { + // Loop over each page in the inverted index entry + foreach($invindex[$qterm] as $pageid => $page_entry) + { + // Create an entry in the matching pages array if it doesn't exist + if(!isset($matching_pages[$pageid])) + $matching_pages[$pageid] = [ "nterms" => [] ]; + $matching_pages[$pageid]["nterms"][$qterm] = $page_entry; + } + } + + + // Loop over the pageindex and search the titles / tags + foreach ($pageindex as $pagename => $pagedata) + { + // Get the current page's id + $pageid = ids::getid($pagename); + // Consider matches in the page title + if(stripos($pagename, $qterm) !== false) + { + // We found the qterm in the title + if(!isset($matching_pages[$pageid])) + $matching_pages[$pageid] = [ "nterms" => [] ]; + + // Set up a counter for page title matches if it doesn't exist already + if(!isset($matching_pages[$pageid]["title-matches"])) + $matching_pages[$pageid]["title-matches"] = 0; + + $matching_pages[$pageid]["title-matches"] += count(mb_stripos_all($pagename, $qterm)); + } + + // Consider matches in the page's tags + if(isset($pagedata->tags) and // If this page has tags + stripos(implode(" ", $pagedata->tags), $qterm) !== false) // And we found the qterm in the tags + { + if(!isset($matching_pages[$pageid])) + $matching_pages[$pageid] = [ "nterms" => [] ]; + + // Set up a counter for tag match if there isn't one already + if(!isset($matching_pages[$pageid]["tag-matches"])) + $matching_pages[$pageid]["tag-matches"] = 0; + $matching_pages[$pageid]["tag-matches"] += count(mb_stripos_all(implode(" ", $pagedata->tags), $qterm)); + } + } + } + + + foreach($matching_pages as $pageid => &$pagedata) + { + $pagedata["pagename"] = ids::getpagename($pageid); + $pagedata["rank"] = 0; + + foreach($pagedata["nterms"] as $pterm => $entry) + { + $pagedata["rank"] += $entry["freq"]; + + // todo rank by context here + } + + // Consider matches in the title / tags + if(isset($pagedata["title-matches"])) + $pagedata["rank"] += $pagedata["title-matches"] * $settings->search_title_matches_weighting; + if(isset($pagedata["tag-matches"])) + $pagedata["rank"] += $pagedata["tag-matches"] * $settings->search_tags_matches_weighting; + + // todo remove items if the rank is below a threshold + } + + // todo sort by rank here + uasort($matching_pages, function($a, $b) { + if($a["rank"] == $b["rank"]) return 0; + return ($a["rank"] < $b["rank"]) ? +1 : -1; + }); + + return $matching_pages; + } + + public static function extract_context($query, $source) + { + global $settings; + + $nterms = self::tokenize($query); + $matches = []; + // Loop over each nterm and find it in the source + foreach($nterms as $nterm) + { + $all_offsets = mb_stripos_all($source, $nterm); + // Skip over adding matches if there aren't any + if($all_offsets === false) + continue; + foreach($all_offsets as $offset) + { + $matches[] = [ $nterm, $offset ]; + } + } + + usort($matches, function($a, $b) { + if($a[1] == $b[1]) return 0; + return ($a[1] < $b[1]) ? +1 : -1; + }); + + $contexts = []; + $basepos = 0; + $matches_count = count($matches); + while($basepos < $matches_count) + { + // Store the next match along - all others will be relative to that + // one + $group = [$matches[$basepos]]; + + // Start scanning at the next one along - we always store the first match + $scanpos = $basepos + 1; + $distance = 0; + + while(true) + { + // Break out if we reach the end + if($scanpos >= $matches_count) break; + + // Find the distance between the current one and the last one + $distance = $matches[$scanpos][1] - $matches[$scanpos - 1][1]; + + // Store it if the distance is below the threshold + if($distance < $settings->search_characters_context) + $group[] = $matches[$scanpos]; + else + break; + + $scanpos++; + } + + $context_start = $group[0][1] - $settings->search_characters_context; + $context_end = $group[count($group) - 1][1] + $settings->search_characters_context; + + $context = substr($source, $context_start, $context_end - $context_start); + + // Strip the markdown from the context - it's most likely going to + // be broken anyway. + $context = self::strip_markup($context); + + $contexts[] = $context; + + $basepos = $scanpos + 1; + } + + return implode(" ... ", $contexts); + } + + public static function highlight_context($query, $context) + { + $qterms = self::tokenize($query); + + foreach($qterms as $qterm) + { + // From http://stackoverflow.com/a/2483859/1460422 + $context = preg_replace("/" . preg_quote($qterm) . "/i", "$0", $context); + } + + return $context; + } +} + + + + +register_module([ + "name" => "Uploader", + "version" => "0.1", + "author" => "Starbeamrainbowlabs", + "description" => "Adds the ability to upload files to Pepperminty Wiki. Uploaded files act as pages and have the special 'File:' prefix.", + "id" => "feature-upload", + "code" => function() { + add_action("upload", function() { + global $settings, $env, $pageindex; + + + switch($_SERVER["REQUEST_METHOD"]) + { + case "GET": + // Send upload page + + if(!$settings->upload_enabled) + exit(page_renderer::render("Upload Disabled - $setting->sitename", "

You can't upload anything at the moment because $settings->sitename has uploads disabled. Try contacting " . $settings->admindetails["name"] . ", your site Administrator. Go back.

")); + if(!$env->is_logged_in) + exit(page_renderer::render("Upload Error - $settings->sitename", "

You are not currently logged in, so you can't upload anything.

+

Try logging in first.

")); + + exit(page_renderer::render("Upload - $settings->sitename", "

Select an image below, and then type a name for it in the box. This server currently supports uploads up to " . human_filesize(get_max_upload_size()) . " in size.

+

$settings->sitename currently supports uploading of the following file types: " . implode(", ", $settings->upload_allowed_file_types) . ".

+
+ + +
+ + +
+ + +
+ +
")); + + break; + + case "POST": + // Recieve file + + // Make sure uploads are enabled + if(!$settings->upload_enabled) + { + unlink($_FILES["file"]["tmp_name"]); + http_response_code(412); + exit(page_renderer::render("Upload failed - $settings->sitename", "

Your upload couldn't be processed because uploads are currently disabled on $settings->sitename. Go back to the main page.

")); + } + + // Make sure that the user is logged in + if(!$env->is_logged_in) + { + unlink($_FILES["file"]["tmp_name"]); + http_response_code(401); + exit(page_renderer::render("Upload failed - $settings->sitename", "

Your upload couldn't be processed because you are not logged in.

Try logging in first.")); + } + + // Calculate the target ename, removing any characters we + // are unsure about. + $target_name = makepathsafe($_POST["name"]); + $temp_filename = $_FILES["file"]["tmp_name"]; + + $mimechecker = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($mimechecker, $temp_filename); + finfo_close($mimechecker); + + // Perform appropriate checks based on the *real* filetype + switch(substr($mime_type, 0, strpos($mime_type, "/"))) + { + case "image": + $extra_data = []; + $imagesize = getimagesize($temp_filename, $extra_data); + // Make sure that the image size is defined + if(!is_int($imagesize[0]) or !is_int($imagesize[1])) + { + http_response_code(415); + exit(page_renderer::render("Upload Error - $settings->sitename", "

The file that you uploaded doesn't appear to be an image. $settings->sitename currently only supports uploading images (videos coming soon). Go back to try again.

")); + } + + break; + + case "video": + http_response_code(501); + exit(page_renderer::render("Upload Error - $settings->sitename", "

You uploaded a video, but $settings->sitename doesn't support them yet. Please try again later.

")); + + default: + http_response_code(415); + exit(page_renderer::render("Upload Error - $settings->sitename", "

You uploaded an unnknown file type which couldn't be processed. $settings->sitename thinks that the file you uploaded was a(n) '$mime_type', which isn't supported.

")); + } + + $file_extension = system_mime_type_extension($mime_type); + + $new_filename = "Files/$target_name.$file_extension"; + $new_description_filename = "$new_filename.md"; + + if(isset($pageindex->$new_filename)) + exit(page_renderer::render("Upload Error - $settings->sitename", "

A page or file has already been uploaded with the name '$new_filename'. Try deleting it first. If you do not have permission to delete things, try contacting one of the moderators.

")); + + if(!file_exists("Files")) + mkdir("Files", 0664); + + if(!move_uploaded_file($temp_filename, $new_filename)) + { + http_response_code(409); + exit(page_renderer::render("Upload Error - $settings->sitename", "

The file you uploaded was valid, but $settings->sitename couldn't verify that it was tampered with during the upload process. This probably means that $settings->sitename has been attacked. Please contact " . $settings->admindetails . ", your $settings->sitename Administrator.

")); + } + + file_put_contents($new_description_filename, $_POST["description"]); + + $description = $_POST["description"]; + + if($settings->clean_raw_html) + $description = htmlentities($description, ENT_QUOTES); + + file_put_contents($new_description_filename, $description); + + // Construct a new entry for the pageindex + $entry = new stdClass(); + // Point to the description's filepath since this property + // should point to a markdown file + $entry->filename = $new_description_filename; + $entry->size = strlen($description); + $entry->lastmodified = time(); + $entry->lasteditor = $env->user; + $entry->uploadedfile = true; + $entry->uploadedfilepath = $new_filename; + $entry->uploadedfilemime = $mime_type; + // Add the new entry to the pageindex + // Assign the new entry to the image's filepath as that + // should be the page name. + $pageindex->$new_filename = $entry; + + // Save the pageindex + file_put_contents("pageindex.json", json_encode($pageindex, JSON_PRETTY_PRINT)); + + header("location: ?action=view&page=$new_filename&upload=success"); + + break; + } + }); + add_action("preview", function() { + global $settings, $env, $pageindex; + + $filepath = $pageindex->{$env->page}->uploadedfilepath; + $mime_type = $pageindex->{$env->page}->uploadedfilemime; + + // Determine the target size of the image + $target_size = 512; + if(isset($_GET["size"])) + $target_size = intval($_GET["size"]); + if($target_size < $settings->min_preview_size) + $target_size = $settings->min_preview_size; + if($target_size > $settings->max_preview_size) + $target_size = $settings->max_preview_size; + + // Determine the output file type + $output_mime = $settings->preview_file_type; + if(isset($_GET["type"]) and in_array($_GET["type"], [ "image/png", "image/jpeg", "image/webp" ])) + $output_mime = $_GET["type"]; + + switch(substr($mime_type, 0, strpos($mime_type, "/"))) + { + case "image": + $image = false; + switch($mime_type) + { + case "image/jpeg": + $image = imagecreatefromjpeg($filepath); + break; + case "image/gif": + $image = imagecreatefromgif($filepath); + break; + case "image/png": + $image = imagecreatefrompng($filepath); + break; + case "image/webp": + $image = imagecreatefromwebp($filepath); + break; + default: + http_response_code(415); + $image = errorimage("Unsupported image type."); + break; + } + + $raw_width = imagesx($image); + $raw_height = imagesy($image); + + $preview_image = resize_image($image, $target_size); + header("content-type: $output_mime"); + switch($output_mime) + { + case "image/jpeg": + imagejpeg($preview_image); + break; + case "image/png": + imagepng($preview_image); + break; + default: + case "image/webp": + imagewebp($preview_image); + break; + } + imagedestroy($preview_image); + break; + + default: + http_response_code(501); + exit("Unrecognised file type."); + } + }); + + page_renderer::register_part_preprocessor(function(&$parts) { + global $pageindex, $env, $settings; + // Todo add the preview to the top of the page here, but only if the current action is view and we are on a page that is a file + if(isset($pageindex->{$env->page}->uploadedfile) and $pageindex->{$env->page}->uploadedfile == true) + { + // We are looking at a page that is paired with an uploaded file + $filepath = $pageindex->{$env->page}->uploadedfilepath; + $mime_type = $pageindex->{$env->page}->uploadedfilemime; + $image_link = "//" . $_SERVER["SERVER_NAME"] . dirname($_SERVER["SCRIPT_NAME"]) . $filepath; + + $preview_sizes = [ 256, 512, 768, 1024, 1536 ]; + $preview_html = "
+ + +
+

File Information

+ + + +
Name" . str_replace("File/", "", $filepath) . "
Type$mime_type
Size" . human_filesize(filesize($filepath)) . "
Uploaded by" . $pageindex->{$env->page}->lasteditor . "
+

Description

"; + + $parts["{content}"] = str_replace("", "\n$preview_html", $parts["{content}"]); + } + }); + } +]); + +//// Pair of functions to calculate the actual maximum upload size supported by the server +//// Lifted from Drupal by @meustrus from Stackoverflow. Link to answer: +//// http://stackoverflow.com/a/25370978/1460422 +// Returns a file size limit in bytes based on the PHP upload_max_filesize +// and post_max_size +function get_max_upload_size() +{ + static $max_size = -1; + if ($max_size < 0) { + // Start with post_max_size. + $max_size = parse_size(ini_get('post_max_size')); + // If upload_max_size is less, then reduce. Except if upload_max_size is + // zero, which indicates no limit. + $upload_max = parse_size(ini_get('upload_max_filesize')); + if ($upload_max > 0 && $upload_max < $max_size) { + $max_size = $upload_max; + } + } + return $max_size; +} + +function parse_size($size) { + $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size. + $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size. + if ($unit) { + // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. + return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); + } else { + return round($size); + } +} + +function errorimage($text) +{ + $width = 640; + $height = 480; + $image = imagecreatetruecolor($width, $height); + imagefill($image, 0, 0, imagecolorallocate($image, 238, 232, 242)); // Set the background to #eee8f2 + $fontwidth = imagefontwidth(3); + imagetext($image, 3, + ($width / 2) - (($fontwidth * strlen($text)) / 2), + ($height / 2) - (imagefontheight(3) / 2), + $text, + imagecolorallocate($image, 17, 17, 17) // #111111 + ); + + return $image; +} + +function resize_image($image, $size) +{ + $cur_width = imagesx($image); + $cur_height = imagesy($image); + + if($cur_width < $size and $cur_height < $size) + return $image; + + $width_ratio = $size / $cur_width; + $height_ratio = $size / $cur_height; + $ratio = min($width_ratio, $height_ratio); + + $new_height = floor($cur_height * $ratio); + $new_width = floor($cur_width * $ratio); + + header("x-resize-to: $new_width x $new_height\n"); + + return imagescale($image, $new_width, $new_height); +} + + register_module([ diff --git a/module_index.json b/module_index.json index 3391c79..b8ccead 100644 --- a/module_index.json +++ b/module_index.json @@ -5,7 +5,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a utility action (that anyone can use) called hash that hashes a given string. Useful when changing a user's password.", "id": "action-hash", - "lastupdate": 1444481636, + "lastupdate": 1445170746, "optional": false }, { @@ -14,7 +14,7 @@ "author": "Starbeamrainbowlabs", "description": "Exposes Pepperminty Wiki's new page protection mechanism and makes the protect button in the 'More...' menu on the top bar work.", "id": "action-protect", - "lastupdate": 1443596834, + "lastupdate": 1445170746, "optional": false }, { @@ -23,7 +23,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a 'raw' action that shows you the raw source of a page.", "id": "action-raw", - "lastupdate": 1442907118, + "lastupdate": 1445170746, "optional": false }, { @@ -32,7 +32,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a sidebar to the left hand side of every page. Add '$settings->sidebar_show = true;' to your configuration, or append '&sidebar=yes' to the url to enable. Adding to the url sets a cookie to remember your setting.", "id": "extra-sidebar", - "lastupdate": 1438780254, + "lastupdate": 1445170746, "optional": false }, { @@ -41,7 +41,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds support for redirect pages. Uses the same syntax that Mediawiki does.", "id": "feature-redirect", - "lastupdate": 1444299144, + "lastupdate": 1445170746, "optional": false }, { @@ -50,7 +50,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds proper search functionality to Pepperminty Wiki. Note that this module, at the moment, just contains test code while I figure out how best to write a search engine.", "id": "feature-search", - "lastupdate": 1446473866, + "lastupdate": 1446717614, "optional": false }, { @@ -59,7 +59,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds the ability to upload files to Pepperminty Wiki. Uploaded files act as pages and have the special 'File:' prefix.", "id": "feature-upload", - "lastupdate": 1446021592, + "lastupdate": 1445716955, "optional": false }, { @@ -68,7 +68,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds the credits page. You *must* have this module :D", "id": "page-credits", - "lastupdate": 1444327084, + "lastupdate": 1445170746, "optional": false }, { @@ -77,7 +77,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds an action to allow administrators to delete pages.", "id": "page-delete", - "lastupdate": 1446021592, + "lastupdate": 1445771075, "optional": false }, { @@ -86,7 +86,7 @@ "author": "Starbeamrainbowlabs", "description": "Allows you to edit pages by adding the edit and save actions. You should probably include this one.", "id": "page-edit", - "lastupdate": 1446470780, + "lastupdate": 1446388267, "optional": false }, { @@ -95,7 +95,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a page that you can use to export your wiki as a .zip file. Uses $settings->export_only_allow_admins, which controls whether only admins are allowed to export the wiki.", "id": "page-export", - "lastupdate": 1442931546, + "lastupdate": 1445170746, "optional": false }, { @@ -104,7 +104,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds the help action. You really want this one.", "id": "page-help", - "lastupdate": 1432664722, + "lastupdate": 1445170746, "optional": false }, { @@ -113,7 +113,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a page that lists all the pages in the index along with their metadata.", "id": "page-list", - "lastupdate": 1446021592, + "lastupdate": 1445787342, "optional": false }, { @@ -122,7 +122,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a pair of actions (login and checklogin) that allow users to login. You need this one if you want your users to be able to login.", "id": "page-login", - "lastupdate": 1444481426, + "lastupdate": 1445170746, "optional": false }, { @@ -131,7 +131,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds an action to let users user out. For security reasons it is wise to add this module since logging in automatically opens a session that is valid for 30 days.", "id": "page-logout", - "lastupdate": 1442931824, + "lastupdate": 1445170746, "optional": false }, { @@ -140,7 +140,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds an action to allow administrators to move pages.", "id": "page-move", - "lastupdate": 1446021592, + "lastupdate": 1445771483, "optional": false }, { @@ -149,7 +149,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds an update page that downloads the latest stable version of Pepperminty Wiki. This module is currently outdated as it doesn't save your module preferences.", "id": "page-update", - "lastupdate": 1442932002, + "lastupdate": 1445170746, "optional": false }, { @@ -158,7 +158,7 @@ "author": "Starbeamrainbowlabs", "description": "Allows you to view pages. You reallyshould include this one.", "id": "page-view", - "lastupdate": 1446021592, + "lastupdate": 1445789184, "optional": false }, { @@ -167,7 +167,7 @@ "author": "Johnny Broadway & Starbeamrainbowlabs", "description": "The default parser for Pepperminty Wiki. Based on Johnny Broadway's Slimdown (with more than a few modifications). This parser's features are documented in the help page.", "id": "parser-default", - "lastupdate": 1446470780, + "lastupdate": 1446116543, "optional": false }, { @@ -176,7 +176,7 @@ "author": "Johnny Broadway, Emanuil Rusev & Starbeamrainbowlabs", "description": "An upgraded parser based on Emanuil Rusev's Parsedown Extra PHP library (https:\/\/github.com\/erusev\/parsedown-extra), which is licensed MIT. Also uses a modified Slimdown engine by Johnny Broadway in order to add support for internal links etc. Please be careful, as this module adds a _ton_ of weight to your installation.", "id": "parser-parsedown", - "lastupdate": 1443972016, + "lastupdate": 1445170746, "optional": true } ] \ No newline at end of file