artabro/wire/core/ProcessWire.php
2024-08-27 11:35:37 +02:00

1283 lines
38 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
require_once(__DIR__ . '/boot.php');
/**
* ProcessWire API Bootstrap
*
* #pw-summary Represents an instance of ProcessWire connected with a set of API variables.
* #pw-summary-instances Methods for managing ProcessWire instances. Note that most of these methods are static.
* #pw-use-constants
* #pw-use-constructor
* #pw-body =
* This class boots a ProcessWire instance. The current ProcessWire instance is represented by the `$wire` API variable.
* ~~~~~
* // To create a new ProcessWire instance
* $wire = new ProcessWire('/server/path/', 'https://hostname/url/');
* ~~~~~
* #pw-body
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* Default API vars (A-Z)
* ======================
* @property AdminTheme|AdminThemeFramework|null $adminTheme
* @property WireCache $cache
* @property WireClassLoader $classLoader
* @property Config $config
* @property WireDatabasePDO $database
* @property WireDateTime $datetime
* @property Fieldgroups $fieldgroups
* @property Fields $fields
* @property Fieldtypes $fieldtypes
* @property WireFileTools $files
* @property Fuel $fuel
* @property WireHooks $hooks
* @property WireInput $input
* @property Languages $languages (present only if LanguageSupport installed)
* @property WireLog $log
* @property WireMailTools $mail
* @property Modules $modules
* @property Notices $notices
* @property Page $page
* @property Pages $pages
* @property Permissions $permissions
* @property Process|ProcessPageView $process
* @property WireProfilerInterface $profiler
* @property Roles $roles
* @property Sanitizer $sanitizer
* @property Session $session
* @property Templates $templates
* @property Paths $urls
* @property User $user
* @property Users $users
* @property ProcessWire $wire
* @property WireShutdown $shutdown
*
* @method init()
* @method ready()
* @method finished(array $data)
*
*
*/
class ProcessWire extends Wire {
/**
* Major version number
*
*/
const versionMajor = 3;
/**
* Minor version number
*
*/
const versionMinor = 0;
/**
* Reversion revision number
*
*/
const versionRevision = 229;
/**
* Version suffix string (when applicable)
*
*/
const versionSuffix = '';
/**
* Minimum required index.php version, represented by the PROCESSWIRE define
*
*/
const indexVersion = 300;
/**
* Minimum required .htaccess file version
*
*/
const htaccessVersion = 301;
/**
* Status prior to boot (no API variables available)
*
*/
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;
/**
* Status when system, $page and all API variables are ready
*
* All API variables available
*
*/
const statusReady = 4;
/**
* Status when the current $pages template file is being rendered, set right before render
*
* All API variables available
*
*/
const statusRender = 8;
/**
* 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 statusDownload = 32;
/**
* Status when the request has been fully delivered (but before a redirect)
*
* All API variables available
*
*/
const statusFinished = 128;
/**
* Status when the request has finished abnormally (like a manual exit)
*
* @since 3.0.180
*
*/
const statusExited = 256;
/**
* Status when the request failed due to an Exception or 404
*
* API variables should be checked for availability before using.
*
*/
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::statusExited => 'exited',
self::statusFailed => 'failed',
);
/**
* Whether debug mode is on or off
*
* @var bool
*
*/
protected $debug = false;
/**
* Fuel manages ProcessWire API variables
*
* @var Fuel|null
*
*/
protected $fuel = null;
/**
* Saved path, for includeFile() method
*
* @var string
*
*/
protected $pathSave = '';
/**
* Saved file, for includeFile() method
*
* @var string
*
*/
protected $fileSave = '';
/**
* @var SystemUpdater|null
*
*/
protected $updater = null;
/**
* ID for this instance of ProcessWire
*
* @var int
*
*/
protected $instanceID = 0;
/**
* @var WireShutdown
*
*/
protected $shutdown = null;
/**
* Create a new ProcessWire instance
*
* ~~~~~
* // A. Current directory assumed to be root of installation
* $wire = new ProcessWire();
*
* // B: Specify a Config object as returned by ProcessWire::buildConfig()
* $wire = new ProcessWire($config);
*
* // C: Specify where installation root is
* $wire = new ProcessWire('/server/path/');
*
* // D: Specify installation root path and URL
* $wire = new ProcessWire('/server/path/', '/url/');
*
* // E: Specify installation root path, scheme, hostname, URL
* $wire = new ProcessWire('/server/path/', 'https://hostname/url/');
* ~~~~~
*
* @param Config|string|null $config May be any of the following:
* - A Config object as returned from ProcessWire::buildConfig().
* - A string path to PW installation.
* - You may optionally omit this argument if current dir is root of PW installation.
* @param string $rootURL URL or scheme+host to installation.
* - This is only used if $config is omitted or a path string.
* - May also include scheme & hostname, i.e. "http://hostname.com/url" to force use of scheme+host.
* - If omitted, it is determined automatically.
* @throws WireException if given invalid arguments
*
*/
public function __construct($config = null, $rootURL = '/') {
parent::__construct();
if(empty($config)) $config = getcwd();
if(is_string($config)) $config = self::buildConfig($config, $rootURL);
if(!$config instanceof Config) throw new WireException("No configuration information available");
// this is reset in the $this->setConfig() method based on current debug mode
ini_set('display_errors', true);
error_reporting(E_ALL | E_STRICT);
$config->setWire($this);
$this->debug = $this->setConfigDebug($config);
if($this->debug) Debug::timer('all');
$this->instanceID = self::addInstance($this);
$this->setWire($this);
$this->fuel = new Fuel();
$this->fuel->set('wire', $this, true);
/** @var WireClassLoader $classLoader */
$classLoader = $this->wire('classLoader', new WireClassLoader($this), true);
$classLoader->addNamespace((strlen(__NAMESPACE__) ? __NAMESPACE__ : "\\"), PROCESSWIRE_CORE_PATH);
if($config->usePageClasses) {
$classLoader->addSuffix('Page', $config->paths->classes);
}
$this->wire('hooks', new WireHooks($this, $config), true);
$this->setConfig($config);
$this->shutdown = $this->wire(new WireShutdown($config));
$this->setStatus(self::statusBoot);
$this->load($config);
if(self::getNumInstances() > 1) {
// this instance is not handling the request and needs a mock $page API var and pageview
/** @var ProcessPageView $view */
$view = $this->fuel->get('modules')->get('ProcessPageView');
$view->execute(false);
}
}
/**
* Destruct
*
*/
public function __destruct() {
if($this->status < self::statusFinished) {
// call finished hook if it wasnt already
$this->status = self::statusExited;
$this->finished(array(
'prevStatus' => $this->status,
'exited' => true,
));
}
}
public function __toString() {
$str = $this->className() . " ";
$str .= self::versionMajor . "." . self::versionMinor . "." . self::versionRevision;
if(self::versionSuffix) $str .= " " . self::versionSuffix;
if(self::getNumInstances() > 1) $str .= " #$this->instanceID";
return $str;
}
/**
* Populate ProcessWire's configuration with runtime and optional variables
*
* @param Config $config
*
*/
protected function setConfig(Config $config) {
$this->wire('config', $config, true);
$this->wire($config->paths);
$this->wire($config->urls);
ini_set('date.timezone', $config->timezone);
ini_set('default_charset','utf-8');
if(!$config->templateExtension) $config->templateExtension = 'php';
if(!$config->httpHost) $config->httpHost = $this->getHttpHost($config);
if($config->https === null) {
$config->https = (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on')
|| (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'); // AWS LOAD BALANCER
}
$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;
$config->versionName = trim($version . " " . self::versionSuffix);
$config->moduleServiceKey .= str_replace('.', '', $version);
if($config->useFunctionsAPI && !function_exists("\\ProcessWire\\pages")) {
$file = $config->paths->core . 'FunctionsAPI.php';
/** @noinspection PhpIncludeInspection */
include_once($file);
}
if($config->installed >= 1547136020) {
// installations Jan 10, 2019 and onwards:
// make the __('text') translation function return entity encoded text, whether translated or not
__(true, 'entityEncode', true);
}
// check if noHTTPS option is using conditional hostname
if($config->noHTTPS && $config->noHTTPS !== true) {
$noHTTPS = $config->noHTTPS;
$httpHost = $config->httpHost;
if(is_string($noHTTPS)) $noHTTPS = array($noHTTPS);
if(is_array($noHTTPS)) {
$config->noHTTPS = false;
foreach($noHTTPS as $host) {
if($host === $httpHost) {
$config->noHTTPS = true;
break;
}
}
}
}
}
/**
* Determine whether debug mode should be enabled
*
* @param Config $config
* @return bool|int Returns determined debug mode value
* @since 3.0.212
*
*/
protected function setConfigDebug(Config $config) {
$debug = $config->debug;
if($debug) {
// use as-is
} else {
$debugIf = $config->debugIf;
if(empty($debugIf)) {
// no processing needed
} else if(is_callable($debugIf)) {
// callable function
$debug = $debugIf();
} else {
$ip = $config->sessionForceIP;
if(empty($ip)) $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
if(is_string($debugIf) && strlen($debugIf) && !empty($ip)) {
// match exact IP address or regex matching IP address(es)
$debugIf = trim($debugIf);
if(strpos($debugIf, '/') === 0) {
$debug = (bool) @preg_match($debugIf, $ip); // regex IPs
} else {
$debug = $debugIf === $ip; // exact IP match
}
} else if(is_array($debugIf) && !empty($ip)) {
// match IP address in array
$debug = in_array($ip, $debugIf);
}
}
if($debug) $config->debug = $debug;
}
if($debug) {
// If debug mode is on then echo all errors
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
} else {
// disable all error reporting
error_reporting(0);
ini_set('display_errors', 0);
}
return $debug;
}
/**
* Safely determine the HTTP host
*
* @param Config $config
* @return string
*
*/
protected function getHttpHost(Config $config) {
$httpHosts = $config->httpHosts;
$port = isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80;
$port = ($port === 80 || $port === 443 ? "" : ":$port");
$host = '';
if(is_array($httpHosts) && count($httpHosts)) {
// validate from an allowed whitelist of http hosts
$key = false;
if(isset($_SERVER['SERVER_NAME'])) {
$key = array_search(strtolower($_SERVER['SERVER_NAME']) . $port, $httpHosts, true);
}
if($key === false && isset($_SERVER['HTTP_HOST'])) {
$key = array_search(strtolower($_SERVER['HTTP_HOST']), $httpHosts, true);
}
if($key === false) {
// no valid host found, default to first in whitelist
$host = reset($httpHosts);
} else {
// found a valid host
$host = $httpHosts[$key];
}
} else {
// pull from server_name or http_host and sanitize
if(isset($_SERVER['SERVER_NAME']) && $host = $_SERVER['SERVER_NAME']) {
// no whitelist available, so defer to server_name
$host .= $port;
} else if(isset($_SERVER['HTTP_HOST'])) {
// fallback to sanitized http_host if server_name not available
// note that http_host already includes port if not 80
$host = $_SERVER['HTTP_HOST'];
}
// sanitize since it did not come from a whitelist
if(!preg_match('/^[-a-zA-Z0-9.:]+$/D', $host)) $host = '';
}
return $host;
}
/**
* Loads ProcessWire using the supplied Config and populates all API fuel
*
* #pw-internal
*
* @param Config $config
* @throws WireDatabaseException|WireException on fatal error
*
*/
public function load(Config $config) {
if($this->debug) {
Debug::timer('boot');
Debug::timer('boot.load');
}
$notices = new Notices();
$this->wire('notices', $notices, true); // first so any API var can send notices
$this->wire('urls', $config->urls); // shortcut API var
$this->wire('log', new WireLog(), true);
$this->wire('sanitizer', new Sanitizer());
$this->wire('datetime', new WireDateTime());
$this->wire('files', new WireFileTools());
$this->wire('mail', new WireMailTools());
try {
/** @noinspection PhpUnusedLocalVariableInspection */
$database = $this->wire('database', WireDatabasePDO::getInstance($config), true);
/** @noinspection PhpUnusedLocalVariableInspection */
$db = $this->wire('db', new DatabaseMysqli($config), true);
} catch(\Exception $e) {
// catch and re-throw to prevent DB connect info from ever appearing in debug backtrace
$this->trackException($e, true, 'Unable to load WireDatabasePDO');
throw new WireDatabaseException($e->getMessage());
}
$this->wire('cache', new WireCache(), true);
$modules = null;
try {
if($this->debug) Debug::timer('boot.load.modules');
$modules = $this->wire('modules', new Modules($config->paths->modules), true);
$modules->addPath($config->paths->siteModules);
$modules->setSubstitutes($config->substituteModules);
$modules->init();
if($this->debug) Debug::saveTimer('boot.load.modules');
} catch(\Exception $e) {
$this->trackException($e, true, 'Unable to load Modules');
if(!$modules) throw new WireException($e->getMessage());
$this->error($e->getMessage());
}
$this->updater = $modules->get('SystemUpdater');
if(!$this->updater) {
$modules->refresh();
$this->updater = $modules->get('SystemUpdater');
}
$fieldtypes = $this->wire('fieldtypes', new Fieldtypes(), true);
$fields = $this->wire('fields', new Fields(), true);
$fieldgroups = $this->wire('fieldgroups', new Fieldgroups(), true);
$templates = $this->wire('templates', new Templates($fieldgroups), true);
$pages = $this->wire('pages', new Pages($this), true);
$this->initVar('fieldtypes', $fieldtypes);
if($this->debug) Debug::timer('init.fields.templates.fieldgroups');
$this->initVar('fields', $fields);
$this->initVar('fieldgroups', $fieldgroups);
$this->initVar('templates', $templates);
if($this->debug) Debug::saveTimer('init.fields.templates.fieldgroups');
$this->initVar('pages', $pages);
if($this->debug) Debug::timer('boot.load.permissions');
if(!$t = $templates->get('permission')) throw new WireException("Missing system template: 'permission'");
/** @noinspection PhpUnusedLocalVariableInspection */
$permissions = $this->wire('permissions', new Permissions($this, $t, $config->permissionsPageID), true);
if($this->debug) Debug::saveTimer('boot.load.permissions');
if($this->debug) Debug::timer('boot.load.roles');
if(!$t = $templates->get('role')) throw new WireException("Missing system template: 'role'");
/** @noinspection PhpUnusedLocalVariableInspection */
$roles = $this->wire('roles', new Roles($this, $t, $config->rolesPageID), true);
if($this->debug) Debug::saveTimer('boot.load.roles');
if($this->debug) Debug::timer('boot.load.users');
$users = $this->wire('users', new Users($this, $config->userTemplateIDs, $config->usersPageIDs), true);
if($this->debug) Debug::saveTimer('boot.load.users');
// the current user can only be determined after the session has been initiated
$session = $this->wire('session', new Session($this), true);
$this->initVar('session', $session);
$this->wire('user', $users->getCurrentUser());
$input = $this->wire('input', new WireInput(), true);
if($config->wireInputLazy) $input->setLazy(true);
// populate admin URL before modules init()
$config->urls->admin = $config->urls->root . ltrim($pages->getPath($config->adminRootPageID), '/');
$notices->init();
if($this->debug) Debug::saveTimer('boot.load', 'includes all boot.load timers');
$this->setStatus(self::statusInit);
}
/**
* Initialize the given API var
*
* @param string $name
* @param Fieldtypes|Fields|Fieldgroups|Templates|Pages|Session $value
*
*/
protected function initVar($name, $value) {
if($this->debug) Debug::timer("boot.load.$name");
if($name != 'session') $value->init();
if($this->debug) Debug::saveTimer("boot.load.$name");
}
/**
* Set the system status to one of the ProcessWire::status* constants
*
* This also triggers init/ready functions for modules, when applicable.
*
* @param int $status
* @param array $data Associative array of any extra data to pass along to include files as locally scoped vars (3.0.142+)
*
*/
public function setStatus($status, array $data = array()) {
/** @var Config $config */
$config = $this->fuel->get('config');
// dont 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
$prevStatus = $this->status;
$this->status = $status;
$config->status = $status;
// call any relevant internal methods
if($status == self::statusInit) {
$this->init();
} else if($status == self::statusReady) {
$config->admin = $this->isAdmin();
$this->ready();
if($this->debug) Debug::saveTimer('boot', 'includes all boot timers');
}
// 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
$data['prevStatus'] = $prevStatus;
$data['maintenance'] = true;
$data['exited'] = false;
$this->finished($data);
} 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 \Throwable $e Exception or Error
* @param string $reason
* @param null $page
* @param string $url
* @since 3.0.142
*
*/
public function setStatusFailed($e, $reason = '', $page = null, $url = '') {
static $lastThrowable = null;
if($lastThrowable === $e) return;
$isException = $e instanceof \Exception;
if(!$page instanceof Page) $page = new NullPage();
$this->setStatus(ProcessWire::statusFailed, array(
'throwable' => $e,
'exception' => $isException ? $e : null,
'error' => $isException ? null : $e,
'failPage' => $page,
'reason' => $reason,
'url' => $url,
));
$lastThrowable = $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() {
/** @var Config $config */
$config = $this->fuel->get('config');
$admin = $config->admin;
if(is_bool($admin)) return $admin;
$admin = 0;
/** @var Page $page */
$page = $this->fuel->get('page');
if(!$page || !$page->id) return 0;
if(in_array($page->template->name, $config->adminTemplates)) {
/** @var User $user */
$user = $this->fuel->get('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
*
* #pw-hooker
*
*/
protected function ___init() {
if($this->debug) Debug::timer('boot.modules.autoload.init');
$this->fuel->get('modules')->triggerInit();
if($this->debug) Debug::saveTimer('boot.modules.autoload.init');
}
/**
* Hookable ready for anyone that wants to hook immediately before any autoload modules ready or after all modules ready
*
* #pw-hooker
*
*/
protected function ___ready() {
if($this->debug) Debug::timer('boot.modules.autoload.ready');
$this->fuel->get('modules')->triggerReady();
$this->updater->ready();
unset($this->updater);
if($this->debug) Debug::saveTimer('boot.modules.autoload.ready');
}
/**
* Hookable ready for anyone that wants to hook when the request is finished
*
* @param array $data Additional data for hooks (3.0.147+ only):
* - `maintenance` (bool): Allow maintenance to run? (default=true)
* - `prevStatus` (int): Previous status before finished status (render, download or failed).
* - `exited` (bool): True if request was exited before finished (ProcessWire instance destructed before expected). 3.0.180+
* - `redirectUrl` (string): Contains redirect URL only if request ending with redirect (not present otherwise).
* - `redirectType` (int): Contains redirect type 301 or 302, only if requestUrl property is also present.
*
* #pw-hooker
*
*/
protected function ___finished(array $data = array()) {
$config = $this->fuel->get('config'); /** @var Config $config */
$session = $this->fuel->get('session'); /** @var Session $session */
$cache = $this->fuel->get('cache'); /** @var WireCache $cache */
$profiler = $this->fuel->get('profiler'); /** @var WireProfilerInterface $profiler */
$exited = !empty($data['exited']);
if($data) {} // data for hooks
// if a hook cancelled maintenance then exit early
if(isset($data['maintenance']) && $data['maintenance'] === false) return;
if($session && !$exited) $session->maintenance();
if($cache && !$exited) $cache->maintenance();
if($profiler) $profiler->maintenance();
if($config && !$exited) {
if($config->templateCompile) {
$compiler = new FileCompiler($config->paths->templates);
$this->wire($compiler);
$compiler->maintenance();
}
if($config->moduleCompile) {
$compiler = new FileCompiler($config->paths->siteModules);
$this->wire($compiler);
$compiler->maintenance();
}
}
}
/**
* Set a new API variable
*
* Alias of $this->wire(), but for setting only, for syntactic convenience.
* i.e. $this->wire()->set($key, $value);
*
* @param string $key API variable name to set
* @param Wire|mixed $value Value of API variable
* @param bool $lock Whether to lock the value from being overwritten
* @return $this
*/
public function set($key, $value, $lock = false) {
$this->wire($key, $value, $lock);
return $this;
}
/**
* Get API var directly
*
* @param string $name
* @return mixed
*
*/
public function __get($name) {
if($name === 'fuel') return $this->fuel;
if($name === 'shutdown') return $this->shutdown;
if($name === 'instanceID') return $this->instanceID;
$value = $this->fuel->get($name);
if($value !== null) return $value;
return parent::__get($name);
}
/**
* Include a PHP file, giving it all PW API varibles in scope
*
* 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, array $data = array()) {
if(!file_exists($file)) return false;
$this->fileSave = $file; // to prevent any possibility of extract() vars from overwriting
$config = $this->fuel->get('config'); /** @var Config $config */
if($this->status > self::statusBoot && $config->templateCompile) {
$files = $this->fuel->get('files'); /** @var WireFileTools $files */
if($files) $this->fileSave = $files->compile($file, array('skipIfNamespace' => true));
}
$this->pathSave = getcwd();
chdir(dirname($this->fileSave));
if(count($data)) extract($data);
$fuel = $this->fuel->getArray();
extract($fuel);
/** @noinspection PhpIncludeInspection */
include($this->fileSave);
chdir($this->pathSave);
$this->fileSave = '';
return true;
}
/**
* Call method
*
* @param string $method
* @param array $arguments
* @return mixed
* @throws WireException
*
*/
public function __call($method, $arguments) {
if(method_exists($this, "___$method")) return parent::__call($method, $arguments);
$value = $this->__get($method);
if(is_object($value)) return call_user_func_array(array($value, '__invoke'), $arguments);
return parent::__call($method, $arguments);
}
/**
* Get an API variable
*
* #pw-internal
*
* @param string $name Optional API variable name
* @return mixed|null|Fuel
*
*/
public function fuel($name = '') {
if(empty($name)) return $this->fuel;
return $this->fuel->$name;
}
/**
* Called if any Wire-derived object makes API calls before being wired
*
* This is for debugging purposes only and is not called unless `ProcessWire::objectNotWired` is hooked.
* It is called only once per non-wired object. Uncomment code within to use.
*
* #pw-internal
*
* @param Wire $obj Object that accessed API var without being assigned ProcessWire instance
* @param string|Wire The $name argument that was passed to $obj->wire($name, $value)
* @param mixed $value The $value argument passed to $object->wire($name, $value)
* @since 3.0.158
*
*/
public function _objectNotWired(Wire $obj, $name, $value) {
// Uncomment code below to enable (use in admin)
/*
if(is_string($name) && $this->wire($name)) {
$msg = $obj->className() . " accessed API var \$$name before being wired";
$this->warning("$msg\n" . Debug::backtrace(array(
'limit' => 2,
'getString' => true,
'getCnt' => false,
'getFile' => 'basename',
)));
}
*/
}
/*** MULTI-INSTANCE *************************************************************************************/
/**
* Instances of ProcessWire
*
* @var array
*
*/
static protected $instances = array();
/**
* Current ProcessWire instance
*
* @var null
*
*/
static protected $currentInstance = null;
/**
* Instance ID of this ProcessWire instance
*
* #pw-group-instances
*
* @return int
*
*/
public function getProcessWireInstanceID() {
return $this->instanceID;
}
/**
* Add a ProcessWire instance and return the instance ID
*
* #pw-group-instances
*
* @param ProcessWire $wire
* @return int
*
*/
protected static function addInstance(ProcessWire $wire) {
$id = 0;
while(isset(self::$instances[$id])) $id++;
self::$instances[$id] = $wire;
return $id;
}
/**
* Get all ProcessWire instances
*
* #pw-group-instances
*
* @return array
*
*/
public static function getInstances() {
return self::$instances;
}
/**
* Return number of instances
*
* #pw-group-instances
*
* @return int
*
*/
public static function getNumInstances() {
return count(self::$instances);
}
/**
* Get a ProcessWire instance by ID
*
* #pw-group-instances
*
* @param int|null $instanceID Omit this argument to return the current instance
* @return null|ProcessWire
*
*/
public static function getInstance($instanceID = null) {
if(is_null($instanceID)) return self::getCurrentInstance();
return isset(self::$instances[$instanceID]) ? self::$instances[$instanceID] : null;
}
/**
* Get the current ProcessWire instance
*
* #pw-group-instances
*
* @return ProcessWire|null
*
*/
public static function getCurrentInstance() {
if(is_null(self::$currentInstance)) {
$wire = reset(self::$instances);
if($wire) self::setCurrentInstance($wire);
}
return self::$currentInstance;
}
/**
* Set the current ProcessWire instance
*
* #pw-group-instances
*
* @param ProcessWire $wire
*
*/
public static function setCurrentInstance(ProcessWire $wire) {
self::$currentInstance = $wire;
}
/**
* Remove a ProcessWire instance
*
* #pw-group-instances
*
* @param ProcessWire $wire
*
*/
public static function removeInstance(ProcessWire $wire) {
foreach(self::$instances as $key => $instance) {
if($instance === $wire) {
unset(self::$instances[$key]);
if(self::$currentInstance === $wire) self::$currentInstance = null;
break;
}
}
}
/**
* 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 well just modify as needed
* …or specify boolean true to get absolute root path, which disregards any symbolic links to core.
* @return string
*
*/
public static function getRootPath($rootPath = '') {
if($rootPath !== true && 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) || $rootPath === true) {
// 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
*
* @param string $rootPath Path to root of installation where ProcessWire's index.php file is located.
* @param string $rootURL Should be specified only for secondary ProcessWire instances.
* May also include scheme & hostname, i.e. "http://hostname.com/url" to force use of scheme+host.
* @param array $options Options to modify default behaviors (experimental):
* - `siteDir` (string): Name of "site" directory in $rootPath that contains site's config.php, no slashes (default="site").
* @return Config
*
*/
public static function buildConfig($rootPath = '', $rootURL = null, array $options = array()) {
$rootPath = self::getRootPath($rootPath);
$httpHost = '';
$scheme = '';
$siteDir = isset($options['siteDir']) ? $options['siteDir'] : 'site';
$cfg = array('dbName' => '');
if($rootURL && strpos($rootURL, '://')) {
// rootURL is specifying scheme and hostname
list($scheme, $httpHost) = explode('://', $rootURL);
if(strpos($httpHost, '/')) {
list($httpHost, $rootURL) = explode('/', $httpHost, 2);
$rootURL = "/$rootURL";
} else {
$rootURL = '/';
}
$scheme = strtolower($scheme);
$httpHost = strtolower($httpHost);
}
$rootPath = rtrim($rootPath, '/');
$_rootURL = $rootURL;
if(is_null($rootURL)) $rootURL = '/';
// check what rootPath is referring to
if(strpos($rootPath, "/$siteDir")) {
$parts = explode('/', $rootPath);
$testDir = array_pop($parts);
if(($testDir === $siteDir || strpos($testDir, 'site-') === 0) && is_file("$rootPath/config.php")) {
// rootPath was given as a /site/ directory rather than root directory
$rootPath = implode('/', $parts); // remove siteDir from rootPath
$siteDir = $testDir; // set proper siteDir
}
}
if(isset($_SERVER['HTTP_HOST'])) {
$host = $httpHost ? $httpHost : strtolower($_SERVER['HTTP_HOST']);
// when serving pages from a web server
if(is_null($_rootURL)) $rootURL = rtrim(dirname($_SERVER['SCRIPT_NAME']), "/\\") . '/';
$realScriptFile = empty($_SERVER['SCRIPT_FILENAME']) ? '' : realpath($_SERVER['SCRIPT_FILENAME']);
$realIndexFile = realpath($rootPath . "/index.php");
// check if we're being included from another script and adjust the rootPath accordingly
$sf = empty($realScriptFile) ? '' : dirname($realScriptFile);
$f = dirname($realIndexFile);
if($sf && $sf != $f && strpos($sf, $f) === 0) {
$x = rtrim(substr($sf, strlen($f)), '/');
if(is_null($_rootURL)) $rootURL = substr($rootURL, 0, strlen($rootURL) - strlen($x));
}
unset($sf, $f, $x);
// when internal is true, we are not being called by an external script
$cfg['internal'] = strtolower($realIndexFile) == strtolower($realScriptFile);
} else {
// when included from another app or command line script
$cfg['internal'] = false;
$host = '';
}
// Allow for an optional /index.config.php file that can point to a different site configuration per domain/host.
$indexConfigFile = $rootPath . "/index.config.php";
if(is_file($indexConfigFile)
&& !function_exists("\\ProcessWire\\ProcessWireHostSiteConfig")
&& !function_exists("\\ProcessWireHostSiteConfig")) {
// optional config file is present in root
$hostConfig = array();
/** @noinspection PhpIncludeInspection */
@include($indexConfigFile);
if(function_exists("\\ProcessWire\\ProcessWireHostSiteConfig")) {
$hostConfig = ProcessWireHostSiteConfig();
} else if(function_exists("\\ProcessWireHostSiteConfig")) {
$hostConfig = \ProcessWireHostSiteConfig();
}
if($host && isset($hostConfig[$host])) {
$siteDir = $hostConfig[$host];
} else if(isset($hostConfig['*'])) {
$siteDir = $hostConfig['*']; // default override
}
}
// other default directories
$sitePath = $rootPath . "/$siteDir/";
$wireDir = "wire";
$coreDir = "$wireDir/core";
$assetsDir = "$siteDir/assets";
$adminTplDir = 'templates-admin';
// create new Config instance
$cfg['urls'] = new Paths($rootURL);
$cfg['urls']->data(array(
'wire' => "$wireDir/",
'site' => "$siteDir/",
'modules' => "$wireDir/modules/",
'siteModules' => "$siteDir/modules/",
'core' => "$coreDir/",
'assets' => "$assetsDir/",
'cache' => "$assetsDir/cache/",
'logs' => "$assetsDir/logs/",
'files' => "$assetsDir/files/",
'tmp' => "$assetsDir/tmp/",
'templates' => "$siteDir/templates/",
'fieldTemplates' => "$siteDir/templates/fields/",
'adminTemplates' => "$wireDir/$adminTplDir/",
), true);
$cfg['paths'] = clone $cfg['urls'];
$cfg['paths']->set('root', $rootPath . '/');
$cfg['paths']->data('sessions', $cfg['paths']->assets . "sessions/");
$cfg['paths']->data('classes', $cfg['paths']->site . "classes/");
// Styles and scripts are CSS and JS files, as used by the admin application.
// But reserved here if needed by other apps and templates.
$cfg['styles'] = new FilenameArray();
$cfg['scripts'] = new FilenameArray();
$config = new Config();
$config->setTrackChanges(false);
$config->data($cfg, true);
// Include system config defaults
/** @noinspection PhpIncludeInspection */
require("$rootPath/$wireDir/config.php");
// Include site-specific config settings
$configFile = $sitePath . "config.php";
$configFileDev = $sitePath . "config-dev.php";
if(is_file($configFileDev)) {
/** @noinspection PhpIncludeInspection */
@require($configFileDev);
} else if(is_file($configFile)) {
/** @noinspection PhpIncludeInspection */
@require($configFile);
}
if($httpHost) {
$config->httpHost = $httpHost;
if(!in_array($httpHost, $config->httpHosts)) $config->httpHosts[] = $httpHost;
}
if($scheme) $config->https = ($scheme === 'https');
return $config;
}
}