diff --git a/core/05-functions.php b/core/05-functions.php index 07f9ecd..94a1049 100644 --- a/core/05-functions.php +++ b/core/05-functions.php @@ -130,6 +130,31 @@ function glob_recursive($pattern, $flags = 0) return $files; } +/** + * Normalize file name & path. + * Used to convert filenames returned by glob_recursive() to a format used in pageindex. + * + * @package core + * @author Alx84 + * @param string $filename A filename with storage prefix as returned by glob_recursive() + * @return string Normalized filename + */ +function normalize_filename($filename) +{ + global $env; + // glob_recursive() returns values like "./storage_prefix/folder/filename.md" + // in the pageindex we save them as "folder/filename.md" + $result = mb_substr( // Store the filename, whilst trimming the storage prefix + $filename, + mb_strlen(preg_replace("/^\.\//iu", "", $env->storage_prefix)) // glob_recursive trim the ./ from returned filenames , so we need to as well + ); + // Remove the `./` from the beginning if it's still hanging around + if(mb_substr($result, 0, 2) == "./") + $result = mb_substr($result, 2); + + return $result; +} + /** * Resolves a relative path against a given base directory. * @since 0.20.0 diff --git a/core/20-pageindex-loader.php b/core/20-pageindex-loader.php index f3a0e7b..b94b47e 100644 --- a/core/20-pageindex-loader.php +++ b/core/20-pageindex-loader.php @@ -3,68 +3,73 @@ * 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/. */ +/** +* Rebuilds the page index based on what files are found +* @param bool $output Whether to send progress information to the user's browser. +*/ +function pageindex_rebuild(bool $output = true) : void { + + global $env, $pageindex; + + if($output && !is_cli()) { + header("content-type: text/event-stream"); + ob_end_flush(); + } -/* - * Sort out the pageindex. Create it if it doesn't exist, and load + parse it - * if it does. - */ -if(!file_exists($paths->pageindex)) -{ $glob_str = $env->storage_prefix . "*.md"; $existingpages = glob_recursive($glob_str); $existingpages_count = count($existingpages); + // Debug statements. Uncomment when debugging the pageindex regenerator. // var_dump($env->storage_prefix); // var_dump($glob_str); // var_dump($existingpages); + + // save our existing pageindex, if it is available at this point + // we will use it to salvage some data out of it, like tags and authors + if (is_a($pageindex, 'stdClass')) $old_pageindex = $pageindex; + else $old_pageindex = new stdClass(); + + + // compose a new pageindex into a global variable $pageindex = new stdClass(); // We use a for loop here because foreach doesn't loop over new values inserted // while we were looping for($i = 0; $i < $existingpages_count; $i++) { $pagefilename = $existingpages[$i]; - - // Create a new entry + + // Create a new entry for each md file we found $newentry = new stdClass(); - $newentry->filename = mb_substr( // Store the filename, whilst trimming the storage prefix - $pagefilename, - mb_strlen(preg_replace("/^\.\//iu", "", $env->storage_prefix)) // glob_recursive trim the ./ from returned filenames , so we need to as well - ); - // Remove the `./` from the beginning if it's still hanging around - if(mb_substr($newentry->filename, 0, 2) == "./") - $newentry->filename = mb_substr($newentry->filename, 2); + + // glob_recursive() returns values like "./storage_prefix/folder/filename.md" + // in the pageindex we save them as "folder/filename.md" + $newentry->filename = normalize_filename($pagefilename); + $newentry->size = filesize($pagefilename); // Store the page size $newentry->lastmodified = filemtime($pagefilename); // Store the date last modified - // Todo find a way to keep the last editor independent of the page index - $newentry->lasteditor = "unknown"; // Set the editor to "unknown" - + // Extract the name of the (sub)page without the ".md" $pagekey = filepath_to_pagename($newentry->filename); error_log("pagename '$newentry->filename' → filepath '$pagekey'"); - + if(file_exists($env->storage_prefix . $pagekey) && // If it exists... !is_dir($env->storage_prefix . $pagekey)) // ...and isn't a directory { // This page (potentially) has an associated file! // Let's investigate. - + // Blindly add the file to the pageindex for now. // Future We might want to do a security check on the file later on. // File a bug if you think we should do this. $newentry->uploadedfile = true; // Yes this page does have an uploaded file associated with it $newentry->uploadedfilepath = $pagekey; // It's stored here - + // Work out what kind of file it really is $mimechecker = finfo_open(FILEINFO_MIME_TYPE); $newentry->uploadedfilemime = finfo_file($mimechecker, $env->storage_prefix . $pagekey); } - - // Debug statements. Uncomment when debugging the pageindex regenerator. - // echo("pagekey: "); - // var_dump($pagekey); - // echo("newentry: "); - // var_dump($newentry); - + // Subpage parent checker if(strpos($pagekey, "/") !== false) { @@ -83,7 +88,15 @@ if(!file_exists($paths->pageindex)) $existingpages[] = $subpage_parent_filename; } } - + + // Attempt to salvage tags and lasteditor from the previous pageindex + if (@$old_pageindex->$pagekey->tags) + $newentry->tags = $old_pageindex->$pagekey->tags; + $newentry->lasteditor = "unknown"; + if (@$old_pageindex->$pagekey->lasteditor) + $newentry->lasteditor = $old_pageindex->$pagekey->lasteditor; + + // If the initial revision doesn't exist on disk, create it (if it does, then we handle that later) if(function_exists("history_add_revision") && !file_exists("{$pagefilename}.r0")) { // Can't use module_exists - too early copy($pagefilename, "{$pagefilename}.r0"); @@ -91,20 +104,31 @@ if(!file_exists($paths->pageindex)) "type" => "edit", "rid" => 0, "timestamp" => $newentry->lastmodified, - "filename" => "{$pagefilename}.r0", + "filename" => normalize_filename("{$pagefilename}.r0"), "newsize" => $newentry->size, "sizediff" => $newentry->size, - "editor" => "unknown" + "editor" => $newentry->lasteditor ] ]; } // Store the new entry in the new page index $pageindex->$pagekey = $newentry; + + if($output) { + $message = "[" . ($i + 1) . " / $existingpages_count] Added $pagefilename to the pageindex."; + if(!is_cli()) $message = "data: $message\n\n"; + else $message = "$message\r"; + echo($message); + flush(); + } } - + if(function_exists("history_add_revision")) { - $history_revs = glob_recursive($env->storage_prefix . "*.r*"); - // It's very important that we read the history revisions in the right order and that we don't skip any + + // collect from the filesystem what revision files we have + $history_revs = glob_recursive($env->storage_prefix . "*.md.r*"); + + // sort them in the ascending order of their revision numbers - it's very important for further processing usort($history_revs, function($a, $b) { preg_match("/[0-9]+$/", $a, $revid_a); $revid_a = intval($revid_a[0]); @@ -112,29 +136,40 @@ if(!file_exists($paths->pageindex)) $revid_b = intval($revid_b[0]); return $revid_a - $revid_b; }); - // We can guarantee that the direcotry separator is present on the end - it's added explicitly earlier - $strlen_storageprefix = strlen($env->storage_prefix); + + foreach($history_revs as $filename) { preg_match("/[0-9]+$/", $filename, $revid); error_log("raw revid | ".var_export($revid, true)); if(count($revid) === 0) continue; $revid = intval($revid[0]); - + $pagename = filepath_to_pagename($filename); - $filepath_stripped = substr($filename, $strlen_storageprefix); - + $filepath_stripped = normalize_filename($filename); + if(!isset($pageindex->$pagename->history)) $pageindex->$pagename->history = []; - + if(isset($pageindex->$pagename->history[$revid])) continue; - + error_log("pagename: $pagename, revid: $revid, pageindex entry: ".var_export($pageindex->$pagename, true)); $newsize = filesize($filename); $prevsize = 0; if($revid > 0 && isset($pageindex->$pagename->history[$revid - 1])) { $prevsize = filesize(end($pageindex->$pagename->history)->filename); } + + // Let's attempt to salvage the editor for this revision from the old pageindex + // For that we walk through history of edits from old pageindex to find what editor was set for this specific file + $revision_editor = "unknown"; + if ($old_pageindex->$pagename->history) { + foreach ($old_pageindex->$pagename->history as $revision) + if ($revision->filename == $filepath_stripped && isset($revision->editor)) + $revision_editor = $revision->editor; + } + + // save the revision into history $pageindex->$pagename->history[$revid] = (object) [ "type" => "edit", "rid" => $revid, @@ -142,13 +177,30 @@ if(!file_exists($paths->pageindex)) "filename" => $filepath_stripped, "newsize" => $newsize, "sizediff" => $newsize - $prevsize, - "editor" => "unknown" + "editor" => $revision_editor ]; } } - + save_pageindex(); unset($existingpages); + + + if($output && !is_cli()) { + echo("data: Done! \n\n"); + flush(); + } + + +} + +/* + * Sort out the pageindex. Create it if it doesn't exist, and load + parse it + * if it does. + */ +if(!file_exists($paths->pageindex)) +{ + pageindex_rebuild(false); } else { diff --git a/modules/feature-guiconfig.php b/modules/feature-guiconfig.php index 648f06a..daf15d3 100644 --- a/modules/feature-guiconfig.php +++ b/modules/feature-guiconfig.php @@ -47,10 +47,10 @@ register_module([ $content .= "<p>You're currently running Pepperminty Wiki $version+" . substr($commit, 0, 7) . ".</p>\n"; $content .= "<h2>Actions</h2>"; + // rebuild search index button $content .= "<button class='action-invindex-rebuild' title='Rebuilds the index that is consulted when searching the wiki. Hit this button if some pages are not showing up.'>Rebuild Search Index</button>\n"; $content .= "<progress class='action-invindex-rebuild-progress' min='0' max='100' value='0' style='display: none;'></progress><br />\n"; $content .= "<output class='action-invindex-rebuild-latestmessage'></output><br />\n"; - $invindex_rebuild_script = <<<SCRIPT window.addEventListener("load", function(event) { document.querySelector(".action-invindex-rebuild").addEventListener("click", function(event) { @@ -76,8 +76,38 @@ window.addEventListener("load", function(event) { }); }); SCRIPT; - page_renderer::add_js_snippet($invindex_rebuild_script); + + // rebuild page index button + $content .= "<button class='action-pageindex-rebuild' title='Rebuilds the page index that contains information (tags, author, dates, filename) about all wiki pages. Hit this button if MD files were changed externally.'>Rebuild Page Index</button>\n"; + $content .= "<progress class='action-pageindex-rebuild-progress' min='0' max='100' value='0' style='display: none;'></progress><br />\n"; + $content .= "<output class='action-pageindex-rebuild-latestmessage'></output><br />\n"; + $pageindex_rebuild_script = <<<SCRIPT +window.addEventListener("load", function(event) { + document.querySelector(".action-pageindex-rebuild").addEventListener("click", function(event) { + var rebuildActionEvents = new EventSource("?action=pageindex-rebuild"); + var latestMessageElement = document.querySelector(".action-pageindex-rebuild-latestmessage"); + var progressElement = document.querySelector(".action-pageindex-rebuild-progress"); + rebuildActionEvents.addEventListener("message", function(event) { + console.log(event); + let message = event.data; + latestMessageElement.value = event.data; + let parts = message.match(/^\[\s*(\d+)\s+\/\s+(\d+)\s*\]/); + if(parts != null) { + progressElement.style.display = ""; + progressElement.min = 0; + progressElement.max = parseInt(parts[2]); + progressElement.value = parseInt(parts[1]); + } + if(message.startsWith("Done!")) + rebuildActionEvents.close(); + }); + // Close the connection on error & don't try again + rebuildActionEvents.addEventListener("error", (_event) => rebuildActionEvents.close()); + }); +}); +SCRIPT; + page_renderer::add_js_snippet($pageindex_rebuild_script); $content .= "<h2>Settings</h2>"; $content .= "<p>Mouse over the name of each setting to see a description of what it does.</p>\n"; @@ -207,6 +237,41 @@ SCRIPT; $content .= "</textarea>\n"; exit(page_renderer::render_main("Master Settings Updated - $settings->sitename", $content)); }); + + /** + * @api {get} ?action=pageindex-rebuild[&format=json] Rebuilds the page index + * @apiName UserList + * @apiGroup Utility + * @apiPermission Anonymous + */ + + + /* _ _ _ _ _ _ + * (_) | | | | (_) | | | | + * _ __ __ _ __ _ ___ _ _ __ __| | ___ __ __ ______ _ __ ___ | |__ _ _ _ | | __| | + * | '_ \ / _` | / _` | / _ \ | | | '_ \ / _` | / _ \ \ \/ / |______| | '__| / _ \ | '_ \ | | | | | | | | / _` | + * | |_) | | (_| | | (_| | | __/ | | | | | | | (_| | | __/ > < | | | __/ | |_) | | |_| | | | | | | (_| | + * | .__/ \__,_| \__, | \___| |_| |_| |_| \__,_| \___| /_/\_\ |_| \___| |_.__/ \__,_| |_| |_| \__,_| + * | | __/ | + * |_| |___/ + * + */ + add_action("pageindex-rebuild", function() { + global $env, $settings; + if($env->is_admin || + ( + !empty($_POST["secret"]) && + $_POST["secret"] === $settings->secret + ) + ) + pageindex_rebuild(); + else + { + http_response_code(401); + exit(page_renderer::render_main("Error - Page index regenerator - $settings->sitename", "<p>Error: You aren't allowed to regenerate the page index. Try logging in as an admin, or setting the <code>secret</code> POST parameter to $settings->sitename's secret - which can be found in $settings->sitename's <code>peppermint.json</code> file.</p>")); + } + }); + add_help_section("800-raw-page-content", "Viewing Raw Page Content", "<p>Although you can use the edit page to view a page's source, you can also ask $settings->sitename to send you the raw page source and nothing else. This feature is intended for those who want to automate their interaction with $settings->sitename.</p> <p>To use this feature, navigate to the page for which you want to see the source, and then alter the <code>action</code> parameter in the url's query string to be <code>raw</code>. If the <code>action</code> parameter doesn't exist, add it. Note that when used on an file's page this action will return the source of the description and not the file itself.</p>"); diff --git a/themes/default/theme.css b/themes/default/theme.css index 5ef9cdd..e66c733 100644 --- a/themes/default/theme.css +++ b/themes/default/theme.css @@ -174,6 +174,7 @@ blockquote { padding-left: 1em; border-left: 0.2em solid var(--accent-a3); borde pre { white-space: pre-wrap; padding: 0.3em 0.5em; background: var(--bg-page-inset); border-radius: 0.25em; box-shadow: inset 0 0 0.5em var(--shadow); } code { font-size: 1.1em; } +code:not(:has(> pre)) { background: var(--bg-page-inset); } a { cursor: pointer; } a:focus { outline-width: 0.1em; }