From fa81f0df25d06ca1105953eea3633f648da2311d Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Tue, 10 Mar 2020 01:47:40 +0000
Subject: [PATCH] Implement basic Pepperminty Wiki CLI & shell :D

The BkTree tester gave me the idea.

No longer will you have to hope that search indexing will complete in
time and adjust the maximum execution time for larger wikis..... when
that's implemented.
---
 build.php                 |   3 +
 core/05-functions.php     |  10 +++
 core/10-login.php         |   2 +-
 core/100-run.php          |   6 ++
 core/30-consistency.php   |   2 +-
 core/50-security.php      |   4 +-
 modules/feature-cli.php   | 177 ++++++++++++++++++++++++++++++++++++++
 peppermint.guiconfig.json |   4 +-
 8 files changed, 204 insertions(+), 4 deletions(-)
 create mode 100644 modules/feature-cli.php

diff --git a/build.php b/build.php
index e6dc5ad..84ea513 100644
--- a/build.php
+++ b/build.php
@@ -1,5 +1,8 @@
 <?php
 
+// This is the Pepperminty Wiki build environment
+define("PEPPERMINTY_WIKI_BUILD", true);
+
 echo("*** Preparing environment ***\n");
 
 ini_set("user_agent", "Pepperminty-Wiki-Downloader PHP/" . phpversion() . "; +https://github.com/sbrl/Pepperminty-Wiki/ Pepperminty-Wiki/" . file_get_contents("version"));
diff --git a/core/05-functions.php b/core/05-functions.php
index b17069b..406a2b3 100644
--- a/core/05-functions.php
+++ b/core/05-functions.php
@@ -823,3 +823,13 @@ function crypto_id(int $length) : string {
 		[ "=" => "", "+" => "-", "/" => "_"]
 	), 0, $length);
 }
+
+/**
+ * Returns whether we are both on the cli AND the cli is enabled.
+ * @return boolean
+ */
+function is_cli() {
+	global $settings;
+	return php_sapi_name() == "cli" &&
+		$settings->cli_enabled;
+}
diff --git a/core/10-login.php b/core/10-login.php
index 4fe00a1..c440db0 100644
--- a/core/10-login.php
+++ b/core/10-login.php
@@ -1,6 +1,6 @@
 <?php
 
-session_start();
+if(!is_cli()) session_start();
 // Make sure that the login cookie lasts beyond the end of the user's session
 setcookie(session_name(), session_id(), time() + $settings->sessionlifetime, "", "", false, true);
 ///////// Login System /////////
diff --git a/core/100-run.php b/core/100-run.php
index 4d35b7f..100e8b7 100644
--- a/core/100-run.php
+++ b/core/100-run.php
@@ -11,6 +11,12 @@ if(!isset($actions->credits))
 	exit(page_renderer::render_main("Error - $settings->$sitename", "<p>No credits page detected. The credits page is a required module!</p>"));
 }
 
+// If we're on the CLI, then start it
+if(!defined("PEPPERMINTY_WIKI_BUILD") &&
+	module_exists("feature-cli") &&
+	$settings->cli_enabled &&
+	php_sapi_name() == "cli")
+	cli();
 
 //////////////////////////////////
 /// Final Consistency Measures ///
diff --git a/core/30-consistency.php b/core/30-consistency.php
index 5dc4cca..9047b3a 100644
--- a/core/30-consistency.php
+++ b/core/30-consistency.php
@@ -5,7 +5,7 @@
 
 // Work around an Opera + Syntaxtic bug where there is no margin at the left
 // hand side if there isn't a query string when accessing a .php file.
-if(!isset($_GET["action"]) and !isset($_GET["page"]) and basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) == "index.php")
+if(!is_cli() && !isset($_GET["action"]) && !isset($_GET["page"]) && basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) == "index.php")
 {
 	http_response_code(302);
 	header("location: " . dirname(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)));
diff --git a/core/50-security.php b/core/50-security.php
index 79e2110..2fcef66 100644
--- a/core/50-security.php
+++ b/core/50-security.php
@@ -11,7 +11,9 @@
 //  - A login is required to view this wiki
 //  - The user isn't already requesting the login page
 // Note we use $_GET here because $env->action isn't populated at this point
-if($settings->require_login_view === true && // If this site requires a login in order to view pages
+if(
+    !is_cli() &&
+    $settings->require_login_view === true && // If this site requires a login in order to view pages
    !$env->is_logged_in && // And the user isn't logged in
    !in_array($_GET["action"], [ "login", "checklogin", "opensearch-description", "invindex-rebuild", "stats-update" ])) // And the user isn't trying to login, or get the opensearch description, or access actions that apply their own access rules
 {
diff --git a/modules/feature-cli.php b/modules/feature-cli.php
new file mode 100644
index 0000000..4a3bcf2
--- /dev/null
+++ b/modules/feature-cli.php
@@ -0,0 +1,177 @@
+<?php
+register_module([
+	"name" => "Command-line interface",
+	"version" => "0.1",
+	"author" => "Starbeamrainbowlabs",
+	"description" => "Allows interaction with Pepperminty Wiki on the command line.",
+	"id" => "feature-cli",
+	"code" => function() {
+		global $settings;
+		
+		cli_register("version", "Shows the current version of Pepperminty Wiki", function(array $_args) : int {
+			echo("$version-".substr($commit, 0, 7)."\n");
+			return 0;
+		});
+		
+		cli_register("help", "Displays this message", function(array $_args) : int {
+			global $version, $commit, $cli_commands;
+			echo("***** Pepperminty Wiki CLI *****
+$version-".substr($commit, 0, 7)."
+
+This is the command-line interface for Pepperminty Wiki.
+
+Commands:
+");
+			
+			foreach($cli_commands as $name => $data) {
+				echo("    $name    {$data->description}\n");
+			}
+			
+			return 0;
+		});
+		
+		cli_register("shell", "Starts the Pepperminty Wiki shell", function(array $_args) : int {
+			cli_shell();
+			return 0;
+		});
+		
+		cli_register("exit", "Exits the Pepperminty Wiki shell", function(array $args) {
+			$exit_code = 0;
+			if(!empty($args)) $exit_code = intval($args[0]);
+			exit($exit_code);
+		});
+		
+		add_help_section("999-cli", "Command Line Interface", "<p>System administrators can interact with $settings->sitename via a command-line interface if they have console or terminal-level access to the server that $settings->sitename runs on.</p>
+		<p>To do this, system administrators can display the CLI-specific help by changing directory (with the <code>cd</code> command) to be next to <code>index.php</code>, and executing the following:</p>
+		<pre><code>php index.php</code></pre>");
+	}
+]);
+
+/**
+ * Ensures that the current execution environment is the command-line interface.
+ * This function will not return if thisthe current execution environment is not the CLI.
+ * @return void
+ */
+function ensure_cli() {
+	global $settings;
+	if(php_sapi_name() == "cli") return true;
+	
+	header("content-type: text/plain");
+	exit("Oops! Somewhere along the way Pepperminty Wiki's command-line interface was invoked by accident.
+This is unfortunately an unrecoverable fatal error. Please get in touch with $settings->admindetails_name, $settings->sitename's administrator (their email address si $settings->admindetails_email).
+");
+}
+
+/**
+ * Parses $_SERVER["argv"] and provides a command-line interface.
+ * This function kill the process if the current execution environment is not the CLI.
+ * @return void
+ */
+function cli() {
+	global $version, $commit;
+	ensure_cli();
+	
+	$args = array_slice($_SERVER["argv"], 1);
+	
+	switch($args[0] ?? "") {
+		case "version":
+		case "shell":
+			exit(cli_exec($args[0]));
+		
+		case "exec":
+			file_put_contents("php://stderr", "Executing {$args[1]}\n");
+			exit(cli_exec($args[1]) ? 0 : 1);
+			break;
+		
+		case "help":
+		default:
+			echo("***** Pepperminty Wiki CLI *****
+$version-".substr($commit, 0, 7)."
+
+This is the command-line interface for Pepperminty Wiki.
+
+Usage:
+php ./index.php {subcommand}
+
+Commands:
+    help                  Displays this message
+    version               Shows the current version of Pepperminty Wiki
+    shell                 Starts the Pepperminty Wiki shell
+    exec \"{command}\"    Executes a Pepperminty Wiki shell command
+");
+			break;
+	}
+	
+	exit(0);
+}
+
+/**
+ * Starts the Pepperminty Wiki CLI Shell.
+ * This function kill the process if the current execution environment is not the CLI.
+ * @return [type] [description]
+ */
+function cli_shell() {
+	global $settings;
+	ensure_cli();
+	
+	echo(wordwrap("Welcome to the Pepperminty Wiki CLI shell!
+Type \"help\" (without quotes) to get help.
+Be warned that you are effectively the superuser for your wiki right now, with completely unrestricted access!
+"));
+	
+	while(true) {
+		$next_line = readline($settings->cli_prompt);
+		if($next_line == false) { echo("\nexit\n"); exit(0); }
+		if(strlen($next_line) == 0) continue;
+		
+		$exit_code = cli_exec($next_line);
+		echo("<<<< $exit_code <<<<\n");
+	}
+}
+
+/**
+ * Executes a given Pepperminty Wiki shell command.
+ * This function kill the process if the current execution environment is not the CLI.
+ * The returned exit code functions as a normal shell process exit code does.
+ * @param	string	$string		The shell command to execute.
+ * @return	int		The exit code of the command executed.
+ */
+function cli_exec(string $string) : int {
+	global $settings, $cli_commands;
+	ensure_cli();
+	
+	$parts = preg_split("/\s+/", $string);
+	
+	if(!isset($cli_commands->{$parts[0]})) {
+		echo("Error: The command with the name {$parts[0]} could not be found (try the help command instead).\n");
+		return 1;
+	}
+	
+	// Apparently you still have to assign a callable to a variable in order to call it dynamically like this. Ref: core/100-run.php
+	$method = $cli_commands->{$parts[0]}->code;
+	return $method(array_slice($parts, 1));
+}
+
+$cli_commands = new stdClass();
+
+/**
+ * Registers a new CLI command.
+ * Throws an error if a CLI command with the specified name already exists.
+ * @param  string   $name        The name of command.
+ * @param  string   $description The description of the command.
+ * @param  callable $function    The function to execute when this command is executed. An array is passed as the first and only argument containing the arguments passed when the command was invoked.
+ * @return void
+ */
+function cli_register(string $name, string $description, callable $function) {
+	global $cli_commands;
+	
+	if(isset($cli_commands->$name))
+		throw new Exception("Error: A CLI command with the name $name has already been registered (description: {$cli_commands->$name->description})");
+	
+	$cli_commands->$name = (object) [
+		"description" => $description,
+		"code" => $function
+	];
+}
+
+?>
diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json
index 5606032..b1139e5 100644
--- a/peppermint.guiconfig.json
+++ b/peppermint.guiconfig.json
@@ -251,5 +251,7 @@
 	"css_theme_gallery_index_url": { "type": "text", "description": "A url that points  to an index file that contains a list of themes. Used to populate the gallary. Multiple urls are allowed - separate them with a space.", "default": "https://starbeamrainbowlabs.com/labs/peppermint/themes/themeindex.json" },
 	"css_theme_gallery_selected_id": { "type": "text", "description": "The id of the currently selected theme. Defaults to the internal default theme.", "default": "default" },
 	"css": { "type": "textarea", "description": "A string of css to include. Will be included in the &lt;head&gt; of every page inside a &lt;style&gt; tag. This may also be an absolute url - urls will be referenced via a &lt;link rel='stylesheet' /&gt; tag. If the theme gallery is installed and automatic updates enabled, then the value of this property is managed by the theme gallery and changes may be overwritten (try the css_custom setting instead).", "default": "auto" },
-	"css_custom": { "type": "textarea", "description": "A string of custom CSS to include on top of the base theme css. Allows for theme customisations while still enabling automatic updates :D Just like the css setting, this one can also be a url.", "default": "/* Enter your custom css here. */" }
+	"css_custom": { "type": "textarea", "description": "A string of custom CSS to include on top of the base theme css. Allows for theme customisations while still enabling automatic updates :D Just like the css setting, this one can also be a url.", "default": "/* Enter your custom css here. */" },
+	"cli_enabled": { "type": "text", "description": "Whether the Pepperminty Wiki CLI is enabled or not.", "default": true },
+	"cli_prompt": { "type": "text", "description": "The string to use as the prompt in the CLI shell.", "default": "# " }
 }
