diff --git a/wire/config.php b/wire/config.php index f419db07..b8b74ea5 100644 --- a/wire/config.php +++ b/wire/config.php @@ -401,6 +401,7 @@ $config->loginDisabledRoles = array( * * Set to false do disable the option for compiled template files. * When set to true, it will be used unless a given template's 'compile' option is set to 0. + * This setting also covers system status files like /site/ready.php, /site/init.php, etc. (3.0.142+) * * @var bool * @@ -1417,6 +1418,132 @@ $config->lazyPageChunkSize = 250; * */ +/** + * statusFiles: Optional automatic include files when ProcessWire reaches each status/state + * + * **Using status/state files:** + * + * - These files must be located in your /site/ directory, i.e. /site/ready.php. + * - If a file does not exist, PW will see that and ignore it. + * - For any state/status files that you don’t need, it is preferable to make them + * blank or remove them, so that PW does not have to check if the file exists. + * - It is also preferable that included files have the ProcessWire namespace, and it is + * required that a `boot` file (if used) have the Processwire namespace. + * - The `init` and `ready` status files are called *after* autoload modules have had their + * corresponding methods (init or ready) called. Use `_init` or `_ready` (with leading + * underscore) as the keys to specify files that should instead be called *before* the state. + * - While php files in /site/ are blocked from direct access by the .htaccess file, it’s + * also a good idea to add `if(!defined("PROCESSWIRE")) die();` at the top of them. + * + * **Status files and available API variables:** + * + * - Included files receive current available ProcessWire API variables, locally scoped. + * - In the `boot` state, only $wire, $hooks, $config, $classLoader API variables are available. + * - In the `init` state, all core API variables are available, except for $page. + * - In the `ready`, `render`, `download` and `finished` states, all API variables are available. + * - In the `failed` state, an unknown set of API variables is available, so use isset() to check. + * + * **Description of statuses/states:** + * + * The statuses occur in this order: + * + * 1. The `boot` status occurs in the ProcessWire class constructor, after PW has initialized its + * class loader, loaded its config files, and initialized its hooks system. One use for this + * state is to add static hooks to methods that might be executed between boot and init, which + * would be missed by the time the init state is reached. + * + * 2. The `init` status occurs after ProcessWire has loaded all of its core API variables, except + * that the $page API variable has not yet been determined. At this point, all of the autoload + * modules have had their init() methods called as well. + * + * - If you want to target the state right before modules.init() methods are called, (rather + * than after), you can use `initBefore`. + * + * 3. The `ready` status is similar to the init status except that the current Page is now known, + * and the $page API variable is known and available. The ready file is included after autoload + * modules have had their ready() methods called. + * + * - If you want to limit your ready file to just be called for front-end (site) requests, + * you can use `readySite`. + * + * - If you want to limit your ready file to just be called for back-end (admin) requests with + * a logged-in user, you can use `readyAdmin`. + * + * - If you want to target the state right before modules.ready() methods are called, (rather + * than after), you can use `readyBefore`. This is the same as the init state, except that + * the current $page is available. + * + * 4. The `render` status occurs when a page is about to be rendered and the status is retained + * until the page has finished rendering. If using a status file for this, in addition to API + * variables, it will also receive a `$contentType` variable that contains the matched content- + * type header, or it may be blank for text/html content-type, or if not yet known. If externally + * bootstrapped it will contain the value “external”. + * + * 5. The `download` status occurs when a file is about to be sent as a download to the user. + * It occurs *instead* of a render status (rather than in addition to). If using an include file + * for this, in addition to API vars, it will also receive a `$downloadFile` variable containing + * the filename requested to be downloaded (string). + * + * 6. The `finished` status occurs after the request has been delivered and output sent. ProcessWire + * performs various maintenance tasks during this state. + * + * 7. The `failed` status occurs when the request has failed due an Exception being thrown. + * In addition to available API vars, it also receives these variables: + * + * - `$exception` (\Exception): The Exception that triggered the failed status, this is most + * commonly going to be a Wire404Exception, WirePermissionException or WireException. + * - `$reason` (string): Additional text info about error, beyond $exception->getMessage(). + * - `$failPage` (Page|NullPage): Page where the error occurred + * + * **Defining status files:** + * + * You can define all of the status files at once using an array like the one this documentation + * is for, but chances are you want to set one or two rather than all of them. You can do so like + * this, after creating /site/boot.php and site/failed.php files (as examples): + * ~~~~~ + * $config->statusFiles('boot', 'boot.php'); + * $config->statusFiles('failed', 'failed.php'); + * ~~~~~ + * + * @since 3.0.142 + * @var array + * + * #property string boot File to include for 'boot' state. + * #property string init File to include for 'init' state. + * #property string initBefore File to include right before 'init' state, before modules.init(). + * #property string ready File to include for API 'ready' state. + * #property string readyBefore File to include right before 'ready'state, before modules.ready(). + * #property string readySite File to include for 'ready' state on front-end/site only. + * #property string readyAdmin File to include for 'ready' state on back-end/admin only. + * #property string download File to include for API 'download' state (sending file to user). + * #property string render File to include for the 'render' state (always called before). + * #property string finished File to include for the 'finished' state. + * #property string failed File to include for the 'failed' state. + * + */ +$config->statusFiles = array( + 'boot' => '', + 'initBefore' => '', + 'init' => 'init.php', + 'readyBefore' => '', + 'ready' => 'ready.php', + 'readySite' => '', + 'readyAdmin' => '', + 'render' => '', + 'download' => '', + 'finished' => 'finished.php', + 'failed' => '', +); + +/** + * adminTemplates: Names of templates that ProcessWire should consider exclusive to the admin + * + * @since 3.0.142 + * @var array + * + */ +$config->adminTemplates = array('admin'); + /*** 10. RUNTIME ******************************************************************************** * @@ -1448,6 +1575,20 @@ $config->modal = false; */ $config->external = false; +/** + * status: Current runtime status (corresponding to ProcessWire::status* constants) + * + */ +$config->status = 0; + +/** + * admin: TRUE when current request is for a logged-in user in the admin, FALSE when not, 0 when not yet known + * + * @since 3.0.142 + * + */ +$config->admin = 0; + /** * cli: This is automatically set to TRUE when PW is booted as a command line (non HTTP) script. * @@ -1460,7 +1601,6 @@ $config->cli = false; */ $config->version = ''; - /** * versionName: This is automatically populated with the current PW version name (i.e. 2.5.0 dev) * diff --git a/wire/core/Config.php b/wire/core/Config.php index 7455cd43..cb8cb3f3 100644 --- a/wire/core/Config.php +++ b/wire/core/Config.php @@ -22,6 +22,7 @@ * * @property bool $ajax If the current request is an ajax (asynchronous javascript) request, this is set to true. #pw-group-runtime * @property bool|int $modal If the current request is in a modal window, this is set to a positive number. False if not. #pw-group-runtime + * @property bool|int $admin Is current request for logged-in user in admin? True, false, or 0 if not yet known. @since 3.0.142 #pw-group-runtime * @property string $httpHost Current HTTP host name. #pw-group-HTTP-and-input * @property bool $https If the current request is an HTTPS request, this is set to true. #pw-group-runtime * @property string $version Current ProcessWire version string (i.e. "2.2.3") #pw-group-system #pw-group-runtime @@ -77,6 +78,7 @@ * @property string $uploadBadExtensions Space separated list of file extensions that are always disallowed from uploads. #pw-group-files * * @property string $adminEmail Email address to send fatal error notifications to. #pw-group-system + * @property array $adminTemplates Names of templates that ProcessWire should consider exclusive to the admin. #pw-group-system @since 3.0.142 * * @property string $pageNameCharset Character set for page names, must be 'ascii' (default, lowercase) or 'UTF8' (uppercase). #pw-group-URLs * @property string $pageNameWhitelist Whitelist of characters allowed in UTF8 page names. #pw-group-URLs @@ -152,6 +154,8 @@ * @property bool $debugMarkupQA Set to true to make the MarkupQA class report verbose debugging messages (to superusers). #pw-internal * @property array $markupQA Optional settings for MarkupQA class used by FieldtypeTextarea module. #pw-group-modules * @property string|null $pagerHeadTags Populated at runtime to contain `` tags for document head, after pagination has been rendered by MarkupPagerNav module. #pw-group-runtime + * @property array $statusFiles File inclusions for ProcessWire’s runtime statuses/states. #pw-group-system @since 3.0.142 + * @property int $status Value of current system status/state corresponding to ProcessWire::status* constants. #pw-internal * * @property int $rootPageID Page ID of homepage (usually 1) #pw-group-system-IDs * @property int $adminRootPageID Page ID of admin root page #pw-group-system-IDs diff --git a/wire/core/PageimageVariations.php b/wire/core/PageimageVariations.php index 9aa9b599..124dc71a 100644 --- a/wire/core/PageimageVariations.php +++ b/wire/core/PageimageVariations.php @@ -596,6 +596,7 @@ class PageimageVariations extends Wire implements \IteratorAggregate, \Countable $files = $this->wire('files'); foreach($variations as $variation) { + /** @var Pageimage $variation */ $filename = $variation->filename; if(!is_file($filename)) continue; if($options['dryRun']) { diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index 38b3ebe0..58de9211 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -65,40 +65,91 @@ class ProcessWire extends Wire { const htaccessVersion = 301; /** - * Status when system is booting + * Status prior to boot (no API variables available) * */ - const statusBoot = 0; + const statusNone = 0; + + /** + * Status when system is booting + * + * API variables available: $wire, $hooks, $config, $classLoader + * + */ + const statusBoot = 1; /** * Status when system and modules are initializing * + * All API variables available except for $page + * */ const statusInit = 2; /** - * Systus when system, $page and API variables are ready + * Status when system, $page and all API variables are ready + * + * All API variables available * */ const statusReady = 4; /** - * Status when the current $page’s template file is being rendered + * Status when the current $page’s template file is being rendered, set right before render + * + * All API variables available * */ const statusRender = 8; /** - * Status when the request has been fully delivered + * Status when current request will send a file download to client and exit (rather than rendering a page template file) + * + * All API variables available * */ - const statusFinished = 16; + const statusDownload = 32; + + /** + * Status when the request has been fully delivered + * + * All API variables available + * + */ + const statusFinished = 128; /** * Status when the request failed due to an Exception or 404 * + * API variables should be checked for availability before using. + * */ - const statusFailed = 1024; + const statusFailed = 1024; + + /** + * Current status/state + * + * @var int + * + */ + protected $status = self::statusNone; + + /** + * Names for each of the system statuses + * + * @var array + * + */ + protected $statusNames = array( + self::statusNone => '', + self::statusBoot => 'boot', + self::statusInit => 'init', + self::statusReady => 'ready', + self::statusRender => 'render', + self::statusDownload => 'download', + self::statusFinished => 'finished', + self::statusFailed => 'failed', + ); /** * Whether debug mode is on or off @@ -127,6 +178,14 @@ class ProcessWire extends Wire { */ protected $pathSave = ''; + /** + * Saved file, for includeFile() method + * + * @var string + * + */ + protected $fileSave = ''; + /** * @var SystemUpdater|null * @@ -147,7 +206,6 @@ class ProcessWire extends Wire { */ protected $shutdown = null; - /** * Create a new ProcessWire instance * @@ -260,6 +318,7 @@ class ProcessWire extends Wire { $config->ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'); $config->cli = (!isset($_SERVER['SERVER_SOFTWARE']) && (php_sapi_name() == 'cli' || (isset($_SERVER['argc']) && $_SERVER['argc'] > 0 && is_numeric($_SERVER['argc'])))); $config->modal = empty($_GET['modal']) ? false : abs((int) $_GET['modal']); + $config->admin = 0; // 0=not known, determined during ready state $version = self::versionMajor . "." . self::versionMinor . "." . self::versionRevision; $config->version = $version; @@ -481,30 +540,128 @@ class ProcessWire extends Wire { * * This also triggers init/ready functions for modules, when applicable. * - * @param $status + * @param int $status + * @param array $data Associtaive array of any extra data to pass along to include files as locally scoped vars (3.0.142+) * */ - public function setStatus($status) { - $config = $this->wire('config'); - // don't re-trigger if this state has already been triggered - if($config->status >= $status) return; - $config->status = $status; - $sitePath = $this->wire('config')->paths->site; + public function setStatus($status, array $data = array()) { + /** @var Config $config */ + $config = $this->wire('config'); + + // don’t re-trigger if this state has already been triggered + // except that a failed status can be backtracked + if($this->status >= $status && $this->status != self::statusFailed) return; + + $name = isset($this->statusNames[$status]) ? $this->statusNames[$status] : 'unknown'; + $path = $config->paths->site; + $files = $config->statusFiles; + + if($status == self::statusReady || $status == self::statusInit) { + // before status include file, i.e. "readyBefore" or "initBefore" + $nameBefore = $name . 'Before'; + $file = empty($files[$nameBefore]) ? null : $path . basename($files[$nameBefore]); + if($file !== null) $this->includeFile($file, $data); + } + + // set status to config + $this->status = $status; + $config->status = $status; + + // call any relevant internal methods if($status == self::statusInit) { $this->init(); - $this->includeFile($sitePath . 'init.php'); - } else if($status == self::statusReady) { + $config->admin = $this->isAdmin(); $this->ready(); if($this->debug) Debug::saveTimer('boot', 'includes all boot timers'); - $this->includeFile($sitePath . 'ready.php'); - - } else if($status == self::statusFinished) { - $this->includeFile($sitePath . 'finished.php'); + } + + // after status include file, names like 'init', 'ready', etc. + $file = empty($files[$name]) ? null : $path . basename($files[$name]); + if($file !== null) $this->includeFile($file, $data); + + if($status == self::statusFinished) { + // internal finished always runs after any included finished file $this->finished(); + } else if($status == self::statusReady) { + // additional 'admin' or 'site' options for ready status + if(!empty($files['readyAdmin']) && $config->admin === true) { + $this->includeFile($path . basename($files['readyAdmin']), $data); + } else if(!empty($files['readySite']) && $config->admin === false) { + $this->includeFile($path . basename($files['readySite']), $data); + } } } + + /** + * Set internal runtime status to failed, with additional info + * + * #pw-internal + * + * @param \Exception $e + * @param string $reason + * @param null $page + * @param string $url + * @since 3.0.142 + * + */ + public function setStatusFailed(\Exception $e, $reason = '', $page = null, $url = '') { + static $lastException = null; + if($lastException === $e) return; + if(!$page instanceof Page) $page = new NullPage(); + $this->setStatus(ProcessWire::statusFailed, array( + 'exception' => $e, + 'failPage' => $page, + 'reason' => $reason, + 'url' => $url, + )); + $lastException = $e; + } + + /** + * Is the current request for a logged-in user within the admin control panel? + * + * #pw-internal + * + * @return bool|int Returns boolean true or false, or 0 if not yet able to tell + * @since 3.0.142 + * + */ + protected function isAdmin() { + + $config = $this->wire('config'); + $admin = $config->admin; + if(is_bool($admin)) return $admin; + $admin = 0; + + $page = $this->wire('page'); + if(!$page || !$page->id) return 0; + + if(in_array($page->template->name, $config->adminTemplates)) { + $user = $this->wire('user'); + if($user) $admin = $user->isLoggedin() ? true : false; + } else { + $admin = false; + } + + return $admin; + } + + /** + * Get the current runtime status/state + * + * #pw-internal + * + * @param bool $getName Get the name of the status rather than the integer value? (default=false) + * @return int|string + * @since 3.0.142 + * + */ + public function getStatus($getName = false) { + if(!$getName) return $this->status; + return isset($this->statusNames[$this->status]) ? $this->statusNames[$this->status] : 'unknown'; + } /** * Hookable init for anyone that wants to hook immediately before any autoload modules initialized or after all modules initialized @@ -558,7 +715,6 @@ class ProcessWire extends Wire { $compiler = new FileCompiler($this->wire('config')->paths->siteModules); $compiler->maintenance(); } - } /** @@ -589,19 +745,27 @@ class ProcessWire extends Wire { * File is executed in the directory where it exists. * * @param string $file Full path and filename + * @param array $data Associative array of any extra data to pass along to include file as locally scoped vars * @return bool True if file existed and was included, false if not. * */ - protected function includeFile($file) { + protected function includeFile($file, array $data = array()) { if(!file_exists($file)) return false; - $file = $this->wire('files')->compile($file, array('skipIfNamespace' => true)); + $this->fileSave = $file; // to prevent any possibility of extract() vars from overwriting + $config = $this->wire('config'); /** @var Config $config */ + if($this->status > self::statusBoot && $config->templateCompile) { + $files = $this->wire('files'); /** @var WireFileTools $files */ + if($files) $this->fileSave = $files->compile($file, array('skipIfNamespace' => true)); + } $this->pathSave = getcwd(); - chdir(dirname($file)); + chdir(dirname($this->fileSave)); + if(count($data)) extract($data); $fuel = $this->fuel->getArray(); extract($fuel); /** @noinspection PhpIncludeInspection */ - include($file); + include($this->fileSave); chdir($this->pathSave); + $this->fileSave = ''; return true; } diff --git a/wire/core/admin.php b/wire/core/admin.php index 5e36b0c8..509e6c70 100644 --- a/wire/core/admin.php +++ b/wire/core/admin.php @@ -154,9 +154,11 @@ if($page->process && $page->process != 'ProcessPageView') { if($process) {} // ignore } catch(Wire404Exception $e) { + $wire->setStatusFailed($e, "404 from $page->process", $page); $wire->error($e->getMessage()); } catch(WirePermissionException $e) { + $wire->setStatusFailed($e, "Permission error from $page->process", $page); if($controller && $controller->isAjax()) { $content = $controller->jsonMessage($e->getMessage(), true); @@ -170,6 +172,7 @@ if($page->process && $page->process != 'ProcessPageView') { } } catch(\Exception $e) { + $wire->setStatusFailed($e, "Error from $page->process", $page); $msg = $e->getMessage(); if($config->debug) { $msg = $sanitizer->entities($msg); diff --git a/wire/modules/Process/ProcessPageView.module b/wire/modules/Process/ProcessPageView.module index b2d6085d..10b23fc4 100644 --- a/wire/modules/Process/ProcessPageView.module +++ b/wire/modules/Process/ProcessPageView.module @@ -8,16 +8,16 @@ * For more details about how Process modules work, please see: * /wire/core/Process.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2019 by Ryan Cramer * https://processwire.com * * @method string execute($internal = true) * @method string executeExternal() - * @method ready() - * @method finished() - * @method failed(\Exception $e) + * @method ready(array $data = array()) + * @method finished(array $data = array()) + * @method failed(\Exception $e, $reason = '', $page = null, $url = '') * @method sendFile($page, $basename) - * @method string pageNotFound($page, $url, $triggerReady = false, $reason = '') + * @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null) * */ class ProcessPageView extends Process { @@ -180,11 +180,11 @@ class ProcessPageView extends Process { } try { - $this->wire()->setStatus(ProcessWire::statusRender); if($this->requestFile) { - + $this->responseType = self::responseTypeFile; + $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $this->requestFile)); $this->sendFile($page, $this->requestFile); } else { @@ -200,6 +200,8 @@ class ProcessPageView extends Process { } if($contentType) header("Content-Type: $contentType"); } + + $this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => $contentType)); if($config->ajax) { $this->responseType = self::responseTypeAjax; @@ -211,11 +213,16 @@ class ProcessPageView extends Process { } } catch(Wire404Exception $e) { - return $this->pageNotFound($page, $this->requestURL, false, '404 thrown during render'); + return $this->pageNotFound($page, $this->requestURL, false, '404 thrown during page render', $e); + + } catch(\Exception $e) { + $this->responseType = self::responseTypeError; + $this->failed($e, "Thrown during page render", $page); + throw $e; // re-throw non-404 Exceptions } } else { - return $this->pageNotFound(new NullPage(), $this->requestURL, true, 'requested page resolved to NullPage'); + return $this->pageNotFound(new NullPage(), $this->requestURL, true, 'Requested URL did not resolve to a Page'); } return ''; @@ -238,24 +245,28 @@ class ProcessPageView extends Process { } $this->wire('page', $page); $this->ready(); - $this->wire()->setStatus(ProcessWire::statusRender); + $this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => 'external')); return ''; } /** * Hook called when the $page API var is ready, and before the $page is rendered. + * + * @param array $data * */ - public function ___ready() { - $this->wire()->setStatus(ProcessWire::statusReady); + public function ___ready(array $data = array()) { + $this->wire()->setStatus(ProcessWire::statusReady, $data); } /** * Hook called with the pageview has been finished and output has been sent. Note this is called in /index.php. + * + * @param array $data * */ - public function ___finished() { - $this->wire()->setStatus(ProcessWire::statusFinished); + public function ___finished(array $data = array()) { + $this->wire()->setStatus(ProcessWire::statusFinished, $data); } /** @@ -264,11 +275,13 @@ class ProcessPageView extends Process { * Sends a copy of the exception that occurred. * * @param \Exception $e + * @param string $reason + * @param Page|null $page + * @param string $url * */ - public function ___failed(\Exception $e) { - if($e) {} - $this->wire()->setStatus(ProcessWire::statusFailed); + public function ___failed(\Exception $e, $reason = '', $page = null, $url = '') { + $this->wire()->setStatusFailed($e, $reason, $page, $url); } /** @@ -862,12 +875,15 @@ class ProcessPageView extends Process { * @param string $url The URL that the request originated from (like $_SERVER['REQUEST_URI'] but already sanitized) * @param bool $triggerReady Whether or not the ready() hook should be triggered (default=false) * @param string $reason Reason why 404 occurred, for debugging purposes (en text) + * @param WireException|Wire404Exception $exception Exception that was thrown or that indicates details of error * @throws WireException * @return string */ - protected function ___pageNotFound($page, $url, $triggerReady = false, $reason = '') { - if($page || $url || $reason) {} // variables provided for hooks only + protected function ___pageNotFound($page, $url, $triggerReady = false, $reason = '', $exception = null) { + + if(!$exception) $exception = new Wire404Exception($reason); // create but do not throw + $this->failed($exception, $reason, $page, $url); $this->responseType = self::responseTypeError; $config = $this->config; $this->header404();