From b75ad067eb67d72341b159094e5da320c57fb1d8 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Thu, 24 Oct 2019 20:13:03 +0100
Subject: [PATCH] Add file & substitution_pipe support.

Also use nomnoml in SVG mode instead of nomnoml-cli, because the latter
is a pain to build.
---
 module_index.json            |  2 +-
 modules/parser-parsedown.php | 95 +++++++++++++++++++++++++++---------
 peppermint.guiconfig.json    | 21 +++++---
 3 files changed, 88 insertions(+), 30 deletions(-)

diff --git a/module_index.json b/module_index.json
index 6695b98..17c1ebf 100755
--- a/module_index.json
+++ b/module_index.json
@@ -337,7 +337,7 @@
         "version": "0.10",
         "author": "Emanuil Rusev & Starbeamrainbowlabs",
         "description": "An upgraded (now default!) parser based on Emanuil Rusev's Parsedown Extra PHP library (https:\/\/github.com\/erusev\/parsedown-extra), which is licensed MIT. Please be careful, as this module adds some weight to your installation.",
-        "lastupdate": 1571614692,
+        "lastupdate": 1571944258,
         "optional": false,
         "extra_data": {
             "Parsedown.php": "https:\/\/raw.githubusercontent.com\/erusev\/parsedown\/fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955\/Parsedown.php",
diff --git a/modules/parser-parsedown.php b/modules/parser-parsedown.php
index 6a9e147..16fd9dd 100644
--- a/modules/parser-parsedown.php
+++ b/modules/parser-parsedown.php
@@ -110,36 +110,85 @@ register_module([
 				exit();
 			}
 			
-			$source_handle = tmpfile();
-			fwrite($source_handle, $source);
-			fseek($source_handle, 0);
-			
 			// Create the cache directory if doesn't exist already
 			if(!file_exists(dirname($cache_file_location)))
 				mkdir(dirname($cache_file_location), 0750, true);
 			
-			$dest_handle = fopen($cache_file_location, "wb+");
+			$cli_to_execute = $renderer->cli;
 			
-			$error_text_handle = tmpfile();
+			$descriptors = [
+				0 => null,		// stdin
+				1 => null,		// stdout
+				2 => tmpfile()	// stderr
+			];
+			
+			switch ($renderer->cli_mode) {
+				case "pipe":
+					// Fill stdin with the input text
+					$descriptors[0] = tmpfile();
+					fwrite($descriptors[0], $source);
+					fseek($descriptors[0], 0);
+					
+					// Pipe the output to be the cache file
+					$descriptors[1] = fopen($cache_file_location, "wb+");
+					break;
+					
+				case "substitution_pipe":
+					// Update the command that we're going to execute
+					$cli_to_execute = str_replace(
+						"{input_text}",
+						escapeshellarg($source),
+						$cli_to_execute
+					);
+					
+					// Set the descriptors
+					$descriptors[0] = tmpfile();
+					$descriptors[1] = fopen($cache_file_location, "wb+");
+					break;
+				
+				case "file":
+					$descriptors[0] = tmpfile();
+					fwrite($descriptors[0], $source);
+					$descriptors[1] = tmpfile();
+					
+					$cli_to_execute = str_replace(
+						[ "{input_file}", "{output_file}" ],
+						[
+							escapeshellarg(stream_get_meta_data($descriptors[0])["uri"]),
+							escapeshellarg($cache_file_location)
+						],
+						$cli_to_execute
+					);
+					break;
+				
+				default:
+					http_response_code(503);
+					header("cache-control: no-cache, no-store, must-revalidate");
+					header("content-type: image/png");
+					imagepng(errorimage("Error: Unknown external renderer mode '$renderer->cli_mode'.\nPlease contact $settings->admindetails_name, $settings->sitename's administrator."));
+					exit();
+					break;
+			}
+			
+			if('\\' !== DIRECTORY_SEPARATOR) {
+				// We're not on Windows, so we can use timeout to force-kill if it takes too long
+				$cli_to_execute = "timeout {$settings->parser_ext_time_limit} $cli_to_execute";
+			}
 			
 			$start_time = microtime(true);
 			$process_handle = proc_open(
-				$renderer->cli,
-				[
-					0 => $source_handle,
-					1 => $dest_handle,
-					2 => $error_text_handle
-				],
+				$cli_to_execute,
+				$descriptors,
 				$pipes,
 				null, // working directory
 				null // environment variables
 			);
 			if(!is_resource($process_handle)) {
-				fclose($dest_handle);
-				fclose($source_handle);
-				fclose($error_text_handle);
+				fclose($descriptors[0]);
+				fclose($descriptors[1]);
+				fclose($descriptors[2]);
 				
-				unlink($cache_file_location);
+				if(file_exists($cache_file_location)) unlink($cache_file_location);
 				
 				http_response_code(503);
 				header("cache-control: no-cache, no-store, must-revalidate");
@@ -150,22 +199,22 @@ register_module([
 			// Wait for it to exit
 			$exit_code = proc_close($process_handle);
 			
-			fclose($dest_handle);
-			fclose($source_handle);
+			fclose($descriptors[0]);
+			fclose($descriptors[1]);
 			
 			$time_taken = round((microtime(true) - $start_time) * 1000, 2);
 			
-			if($exit_code !== 0) {
-				fseek($error_text_handle, 0);
-				$error_details = stream_get_contents($error_text_handle);
+			if($exit_code !== 0 || !file_exists($cache_file_location)) {
+				fseek($descriptors[2], 0);
+				$error_details = stream_get_contents($descriptors[2]);
 				// Delete the cache file, which is guaranteed to exist because
 				// we pre-emptively create it above
-				unlink($cache_file_location);
+				if(file_exists($cache_file_location)) unlink($cache_file_location);
 				
 				http_response_code(503);
 				header("content-type: image/png");
 				imagepng(errorimage(
-					"Error: The external renderer ($renderer->name)\nexited with code $exit_code.\nDetails:\n" . wordwrap($error_details)
+					"Error: The external renderer ($renderer->name)\nexited with code $exit_code,\nor potentially did not create the output file.\nDetails:\n" . wordwrap($error_details)
 				));
 				exit();
 			}
diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json
index 6416830..733e185 100644
--- a/peppermint.guiconfig.json
+++ b/peppermint.guiconfig.json
@@ -22,14 +22,14 @@
 	"parser_cache": { "type": "checkbox", "description": "Whether parser output should be cached to speed things up. The cache directory is <code>._cache</code> in the data directory - delete it if you experience issues (unlikely).", "default": true },
 	"parser_cache_min_size": { "type": "number", "description": "The minimum size a source string must be (in bytes) before it's considered eligible for caching.", "default": 1024 },
 	"parser_ext_renderers_enabled": { "type": "checkbox", "description": "Whether to enable external diagram renderer support, which is part of the parsedown parser. See the <code>parser_ext_renderers</code> setting below for more information.", "default": true },
-	"parser_ext_renderers": { "type": "parserext", "description": "Used by the parsedown parser as an object mapping fenced code block languages to their respective external renderers. Should be in the form <code>language_code</code> → <code>external renderer definition</code>. See the default for examples on how to define an external renderer.", "default": {
+	"parser_ext_renderers": { "type": "parserext", "description": "Used by the parsedown parser as an object mapping fenced code block languages to their respective external renderers. Should be in the form <code>language_code</code> → <code>external renderer definition</code>. See the default for examples on how to define an external renderer. Warning: On Windows, the enforcement of strict time limits is not possible. Beware of DoS attacks!", "default": {
 		"nomnoml": {
 			"name": "nomnoml",
-			"description": "The nomnoml UML diagram renderer. Requires the nomnoml-cli npm package to be globally installed.",
+			"description": "The nomnoml UML diagram renderer. Requires the nomnoml npm package to be globally installed.",
 			"url": "http://nomnoml.com/",
-			"cli": "nomnoml",
-			"cli_mode": "pipe",
-			"output_format": "image/png"
+			"cli": "nomnoml {input_file} {output_file} 0",
+			"cli_mode": "file",
+			"output_format": "image/svg+xml"
 		},
 		"plantuml": {
 			"name": "PlantUML",
@@ -41,13 +41,22 @@
 		},
 		"lilypond": {
 			"name": "LilyPond",
-			"description": "A music notation typesetter. Requires lilypond to be installed.",
+			"description": "A music notation typesetter. Requires lilypond to be installed. WARNING: Don't enable lilypond if you allow anonymous edits. While every effort has been made to close loopholes, a DoS attack may still be possible against your server.",
 			"url": "http://lilypond.org/manuals.html",
 			"cli": "lilypondzzzzzz --silent -ddelete-intermediate-files -dbackend=eps -dresolution=600 --output {output_file} {input_file}",
 			"cli_mode": "file",
 			"output_format": "image/svg+xml"
+		},
+		"latexserver": {
+			"name": "Server-Side MathJax",
+			"description": "Client-side Mathjax via the 'enable_math_rendering' setting not your thing? Try it server-side instead! Requires the 'mathjax-node-cli' npm package to be globally installed.",
+			"url": "https://github.com/mathjax/mathjax-node-cli/blob/master/package.json#L41",
+			"cli": "tex2svg -- {input_text} >{output_file}",
+			"cli_mode": "substitution_pipe",
+			"output_format": "image/svg+xml"
 		}
 	} },
+	"parser_ext_time_limit": { "type": "number", "description": "The number of seconds external renderers are allowed to run for. Has no effect if external renderers are turned off. Also currently has no effect on Windows.", "default": 5 },
 	"interwiki_index_location": { "type": "text", "description": "The location to find the interwiki wiki definition file, which contains a list of wikis along with their names, prefixes, and root urls. May be a URL, or simply a file path - as it's passed to file_get_contents(). If left blank, interwiki link parsing is disabled.", "default": null },
 	"clean_raw_html": { "type": "checkbox", "description": "Whether page sources should be cleaned of HTML before rendering. It is STRONGLY recommended that you keep this option turned on.", "default": true },
 	"all_untrusted": { "type": "checkbox", "description": "Whether to treat both page sources and comment text as untrusted input. Untrusted input has additional restrictions to protect against XSS attacks etc. Turn on if your wiki allows anonymous edits.", "default": false},
