artabro/wire/core/ProcessWire.php

1284 lines
38 KiB
PHP
Raw Permalink Normal View History

2024-08-27 11:35:37 +02:00
<?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;
}
}