diff --git a/wire/core/Page.php b/wire/core/Page.php index 316e3f8e..03296d55 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -1538,7 +1538,8 @@ class Page extends WireData implements \Countable, WireMatchable { */ public function getText($key, $oneLine = false, $entities = null) { $value = $this->getMarkup($key); - if(!strlen($value)) return ''; + $length = strlen($value); + if(!$length) return ''; $options = array( 'entities' => (is_null($entities) ? $this->outputFormatting() : (bool) $entities) ); @@ -1547,6 +1548,8 @@ class Page extends WireData implements \Countable, WireMatchable { } else { $value = $this->wire('sanitizer')->markupToText($value, $options); } + // if stripping tags from non-empty value made it empty, just indicate that it was markup and length + if(!strlen(trim($value))) $value = "markup($length)"; return $value; } diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index d9759d7b..f8573a31 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -204,8 +204,9 @@ class ProcessWire extends Wire { $this->wire('hooks', new WireHooks($this, $config), true); - $this->shutdown = $this->wire(new WireShutdown()); $this->setConfig($config); + $this->shutdown = $this->wire(new WireShutdown($config)); + $this->setStatus(self::statusBoot); $this->load($config); if(self::getNumInstances() > 1) { @@ -274,13 +275,6 @@ class ProcessWire extends Wire { $config->debug = $debugIf; } - // If script is being called externally, add an extra shutdown function - if(!$config->internal) register_shutdown_function(function() { - if(error_get_last()) return; - $process = isset($this) ? $this->wire('process') : wire('process'); - if($process == 'ProcessPageView') $process->finished(); - }); - if($config->useFunctionsAPI) { $file = $config->paths->core . 'FunctionsAPI.php'; /** @noinspection PhpIncludeInspection */ @@ -302,9 +296,6 @@ class ProcessWire extends Wire { } } } - - - $this->setStatus(self::statusBoot); } /** @@ -750,6 +741,41 @@ class ProcessWire extends Wire { } } + /** + * Get root path, check it, and optionally auto-detect it if not provided + * + * @param bool|string $rootPath Root path if already known, in which case we’ll just modify as needed + * @return string + * + */ + protected static function getRootPath($rootPath = '') { + + if(strpos($rootPath, '..') !== false) { + $rootPath = realpath($rootPath); + } + + if(empty($rootPath) && !empty($_SERVER['SCRIPT_FILENAME'])) { + // first try to determine from the script filename + $parts = explode(DIRECTORY_SEPARATOR, $_SERVER['SCRIPT_FILENAME']); + array_pop($parts); // most likely: index.php + $rootPath = implode('/', $parts) . '/'; + if(!file_exists($rootPath . 'wire/core/ProcessWire.php')) $rootPath = ''; + } + + if(empty($rootPath)) { + // if unable to determine from script filename, attempt to determine from current file + $parts = explode(DIRECTORY_SEPARATOR, __FILE__); + $parts = array_slice($parts, 0, -3); // removes "ProcessWire.php", "core" and "wire" + $rootPath = implode('/', $parts) . '/'; + } + + if(DIRECTORY_SEPARATOR != '/') { + $rootPath = str_replace(DIRECTORY_SEPARATOR, '/', $rootPath); + } + + return $rootPath; + } + /** * Static method to build a Config object for booting ProcessWire * @@ -762,17 +788,9 @@ class ProcessWire extends Wire { * @throws WireException * */ - public static function buildConfig($rootPath, $rootURL = null, array $options = array()) { - - if(strpos($rootPath, '..') !== false) { - $rootPath = realpath($rootPath); - if($rootPath === false) throw new WireException("Path not found"); - } - - if(DIRECTORY_SEPARATOR != '/') { - $rootPath = str_replace(DIRECTORY_SEPARATOR, '/', $rootPath); - } - + public static function buildConfig($rootPath = '', $rootURL = null, array $options = array()) { + + $rootPath = self::getRootPath($rootPath); $httpHost = ''; $scheme = ''; $siteDir = isset($options['siteDir']) ? $options['siteDir'] : 'site'; diff --git a/wire/core/WireInput.php b/wire/core/WireInput.php index d1d515e5..78dbfcc8 100644 --- a/wire/core/WireInput.php +++ b/wire/core/WireInput.php @@ -577,7 +577,7 @@ class WireInput extends Wire { // page not yet available, attempt to pull URL from request uri $info = parse_url($_SERVER['REQUEST_URI']); $parts = explode('/', $info['path']); - $charset = $config->pageNameCharset; + $charset = $config ? $config->pageNameCharset : ''; foreach($parts as $i => $part) { if($i > 0) $url .= "/"; $url .= ($charset === 'UTF8' ? $sanitizer->pageNameUTF8($part) : $sanitizer->pageName($part, false)); diff --git a/wire/core/WireShutdown.php b/wire/core/WireShutdown.php index 459c1d88..cfa82217 100644 --- a/wire/core/WireShutdown.php +++ b/wire/core/WireShutdown.php @@ -3,7 +3,7 @@ /** * ProcessWire shutdown handler * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * * Look for errors at shutdown and log them, plus echo the error if the page is editable * @@ -12,26 +12,80 @@ */ class WireShutdown extends Wire { - + + /** + * Associative array of [ PHP E_* constants (i.e. E_ERROR) => Translated description ] + * + * @var array + * + */ protected $types = array(); - protected $fatalTypes = array(); + + /** + * Regular array of PHP E_* constants that are considered fatal (i.e. E_ERROR) + * + * @var array + * + */ + protected $fatalTypes = array( + E_ERROR, + E_CORE_ERROR, + E_COMPILE_ERROR, + E_USER_ERROR, + E_PARSE, + E_RECOVERABLE_ERROR, + ); + + /** + * Associative array of phrase translations for this module + * + * @var array + * + */ protected $labels = array(); - - public function __construct() { + + /** + * @var Config + * + */ + protected $config; + + /** + * Contents of last error_get_last() call + * + * @var array + * + */ + protected $error = array(); + + /** + * Default HTML to use for error message + * + * Can be overridden with $config->fatalErrorHTML in /site/config.php + * + */ + const defaultFatalErrorHTML = '
{message}
{why}
$2
', $html);
+
+ // reference original file rather than compiled version, when applicable
+ $html = str_replace('assets/cache/FileCompiler/site/', '', $html);
+
+ // output the error message
+ echo "\n\n$html\n\n";
+ }
+
+ /**
+ * Send a 500 internal server error
+ *
+ * This is a public fatal error that doesn’t reveal anything specific.
+ *
+ * @param string $message Message to indicate who error was also sent to
+ * @param bool $useHTML Output for a web browser?
+ *
+ */
+ protected function sendError500($message, $useHTML) {
+
+ if($useHTML) {
+ header("HTTP/1.1 500 Internal Server Error");
+ $message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
+ // file that error message will be output in, when available
+ $file = $this->config->paths->templates . 'errors/500.html';
+ } else {
+ $file = '';
+ }
+
+ $this->sendExistingOutput();
+
+ if($file && is_file($file)) {
+ // use defined /site/templates/errors/500.html file
+ echo str_replace('{message}', $message, file_get_contents($file));
+ } else {
+ // use generic error message, since no 500.html available
+ echo "\n\n" . $this->labels['unable-complete'] . ($message ? " - $message" : "") . "\n\n";
+ }
+ }
+
+ /**
+ * Send any existing output while removing PHP’s error message from it (to avoid duplication)
+ *
+ */
+ protected function sendExistingOutput() {
+
+ $files = TemplateFile::getRenderStack();
+ if(!count($files)) return;
+
+ $out = ob_get_clean();
+ if(!strlen($out)) return;
+
+ // if error message isn't in existing output, then reutrn as-is
+ if(empty($this->error['message']) || strpos($out, $this->error['message']) === false) {
+ echo $out;
+ return;
}
- if(!$sendOutput) return true;
+ $token = '';
+ do {
+ $token .= 'xPW' . mt_rand() . 'SD';
+ } while(strpos($out, $token) !== false);
+
+ // replace error message with token
+ $out = str_replace($this->error['message'], $token, $out);
+
+ // replace anything else on the same line as the PHP error (error type, file, line-number)
+ $out = preg_replace('/([\r\n]|^)[^\r\n]+' . $token . '[^\r\n]*/', '', $out);
+
+ echo $out;
+ }
- // we populate $who to give an ambiguous indication where the full error message has been sent
- if($log) $who .= $this->labels['error-logged'] . ' ';
- if($config && $config->adminEmail) $who .= $this->labels['admin-notified'];
+ /**
+ * Shutdown function registered with PHP
+ *
+ * @return bool
+ *
+ */
+ public function shutdown() {
+
+ /** @var Config|null $config */
+ /** @var User|null $user */
+ /** @var Page|null $page */
+
+ $error = error_get_last();
+
+ if(!$error) return true;
+ if(!in_array($error['type'], $this->fatalTypes)) return true;
+
+ $this->error = $error;
+ $this->prepareLabels();
+ $config = $this->config;
+ $user = $this->wire('user'); // current user, if present
+ $useHTML = isset($_SERVER['HTTP_HOST']); // is this an HTTP request where we can output HTML?
+ $name = $user ? $user->name : '?'; // user name
+ $why = ''; // reason why error is being shown, when access allows
+ $who = ''; // who/where the error message has been sent
+ $message = $this->getErrorMessage($error);
+ $url = $this->getCurrentUrl();
+ $sendOutput = $config->allowExceptions !== true;
+
+ // use text-only output if an http request that is ajax
+ if($useHTML && $config->ajax) $useHTML = false;
+
+ // include IP address is user name if configured to do so
+ if($config->logIP && isset($_SERVER['REMOTE_ADDR'])) {
+ $ip = $this->wire('session') ? $this->wire('session')->getIP() : $_SERVER['REMOTE_ADDR'];
+ $name = "$name ($ip)";
+ }
+
+ // send error email if applicable
+ if($config->adminEmail && $sendOutput && $this->wire('mail')) {
+ $n = $this->wire('mail')->new()
+ ->to($config->adminEmail)
+ ->from($config->adminEmail)
+ ->subject($this->labels['email-subject'])
+ ->body("Page: $url\nUser: $name\n\n" . str_replace("\t", "\n", $message));
+ if($n) $who .= $this->labels['admin-notified'];
+ }
+
+ // save to errors.txt log file if applicable
+ if($config->paths->logs) {
+ $log = $this->wire(new FileLog($config->paths->logs . 'errors.txt'));
+ $log->setDelimeter("\t");
+ $log->save("$name\t$url\t" . str_replace("\n", " ", $message));
+ $who .= ($who ? ' ' : '') . $this->labels['error-logged'];
+ }
+
+ // if not allowed to send output, then do nothing further
+ if(!$sendOutput) return true;
// we populate $why if we're going to show error details for any of the following reasons:
// otherwise $why will NOT be populated with anything
- if($debug) $why = $this->labels['debug-mode'] . " (\$config->debug = true; => /site/config.php).";
- else if(!$http) $why = $this->labels['cli-mode'];
- else if($user && $user->isSuperuser()) $why = $this->labels['you-superuser'];
- else if($config && is_file($config->paths->root . "install.php")) $why = $this->labels['install-php'];
- else if($config && !is_file($config->paths->assets . "active.php")) {
+ if($config->debug) {
+ $why = $this->labels['debug-mode'] . " (\$config->debug = true; => /site/config.php).";
+ } else if(!$useHTML) {
+ $why = $this->labels['cli-mode'];
+ } else if($user && $user->isSuperuser()) {
+ $why = $this->labels['you-superuser'];
+ } else if($config && is_file($config->paths->root . "install.php")) {
+ $why = $this->labels['install-php'];
+ } else if($config && $config->paths->assets && !is_file($config->paths->assets . "active.php")) {
// no login has ever occurred or user hasn't logged in since upgrade before this check was in place
// check the date the site was installed to ensure we're not dealing with an upgrade
$installed = $config->paths->assets . "installed.php";
@@ -125,40 +363,26 @@ class WireShutdown extends Wire {
// site was installed within the last 6 hours, safe to assume it's a new install
$why = $this->labels['superuser-never'];
}
- }
-
+ }
+
if($why) {
- // when in debug mode, we can assume the message was already shown, so we just say why.
- // when not in debug mode, we display the full error message since error_reporting and display_errors are off.
$why = $this->labels['shown-because'] . " $why $who";
- $html = "{message}
{why}
$2
', $html);
- $html = str_replace('assets/cache/FileCompiler/site/', '', $html);
- echo "\n\n$html\n\n";
- } else {
- echo "\n\n$message\n\n$why\n\n";
- }
+ $this->sendErrorMessage($message, $why, $useHTML);
} else {
- // public fatal error that doesn't reveal anything specific
- if($http) header("HTTP/1.1 500 Internal Server Error");
- // file that error message will be output in, when available
- $file = $config && $http ? $config->paths->templates . 'errors/500.html' : '';
- if($file && is_file($file)) {
- // use defined /site/templates/errors/500.html file
- echo str_replace('{message}', $who, file_get_contents($file));
- } else {
- // use generic error message, since no 500.html available
- echo "\n\n" . $this->labels['unable-complete'] . ($who ? " - $who" : "") . "\n\n";
- }
+ $this->sendError500($who, $useHTML);
}
return true;
}
+
+ /**
+ * Secondary shutdown call when ProcessWire booted externally
+ *
+ */
+ public function shutdownExternal() {
+ if(error_get_last()) return;
+ $process = $this->wire('process');
+ if($process == 'ProcessPageView') $process->finished();
+ }
}