* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://getkirby.com/license */ class App { use AppCaches; use AppErrors; use AppPlugins; use AppTranslations; use AppUsers; use Properties; public const CLASS_ALIAS = 'kirby'; protected static $instance; protected static $version; public $data = []; protected $api; protected $collections; protected $core; protected $defaultLanguage; protected $environment; protected $language; protected $languages; protected $locks; protected $multilang; protected $nonce; protected $options; protected $path; protected $request; protected $response; protected $roles; protected $roots; protected $routes; protected $router; protected $sessionHandler; protected $site; protected $system; protected $urls; protected $user; protected $users; protected $visitor; /** * Creates a new App instance * * @param array $props * @param bool $setInstance If false, the instance won't be set globally */ public function __construct(array $props = [], bool $setInstance = true) { $this->core = new Core($this); // register all roots to be able to load stuff afterwards $this->bakeRoots($props['roots'] ?? []); try { // stuff from config and additional options $this->optionsFromConfig(); $this->optionsFromProps($props['options'] ?? []); $this->optionsFromEnvironment($props); } finally { // register the Whoops error handler inside of a // try-finally block to ensure it's still registered // even if there is a problem loading the configurations $this->handleErrors(); } // a custom request setup must come before defining the path $this->setRequest($props['request'] ?? null); // set the path to make it available for the url bakery $this->setPath($props['path'] ?? null); // create all urls after the config, so possible // options can be taken into account $this->bakeUrls($props['urls'] ?? []); // configurable properties $this->setOptionalProperties($props, [ 'languages', 'roles', 'site', 'user', 'users' ]); // set the singleton if (static::$instance === null || $setInstance === true) { Model::$kirby = static::$instance = $this; } // setup the I18n class with the translation loader $this->i18n(); // load all extensions $this->extensionsFromSystem(); $this->extensionsFromProps($props); $this->extensionsFromPlugins(); $this->extensionsFromOptions(); $this->extensionsFromFolders(); // trigger hook for use in plugins $this->trigger('system.loadPlugins:after'); // execute a ready callback from the config $this->optionsFromReadyCallback(); // bake config $this->bakeOptions(); } /** * Improved `var_dump` output * * @return array */ public function __debugInfo(): array { return [ 'languages' => $this->languages(), 'options' => $this->options(), 'request' => $this->request(), 'roots' => $this->roots(), 'site' => $this->site(), 'urls' => $this->urls(), 'version' => $this->version(), ]; } /** * Returns the Api instance * * @internal * @return \Kirby\Cms\Api */ public function api() { if ($this->api !== null) { return $this->api; } $root = $this->root('kirby') . '/config/api'; $extensions = $this->extensions['api'] ?? []; $routes = (include $root . '/routes.php')($this); $api = [ 'debug' => $this->option('debug', false), 'authentication' => $extensions['authentication'] ?? include $root . '/authentication.php', 'data' => $extensions['data'] ?? [], 'collections' => array_merge($extensions['collections'] ?? [], include $root . '/collections.php'), 'models' => array_merge($extensions['models'] ?? [], include $root . '/models.php'), 'routes' => array_merge($routes, $extensions['routes'] ?? []), 'kirby' => $this, ]; return $this->api = new Api($api); } /** * Applies a hook to the given value * * @internal * @param string $name Full event name * @param array $args Associative array of named event arguments * @param string $modify Key in $args that is modified by the hooks * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) * @return mixed Resulting value as modified by the hooks */ public function apply(string $name, array $args, string $modify, ?Event $originalEvent = null) { $event = $originalEvent ?? new Event($name, $args); if ($functions = $this->extension('hooks', $name)) { foreach ($functions as $function) { // bind the App object to the hook $newValue = $event->call($this, $function); // update value if one was returned if ($newValue !== null) { $event->updateArgument($modify, $newValue); } } } // apply wildcard hooks if available $nameWildcards = $event->nameWildcards(); if ($originalEvent === null && count($nameWildcards) > 0) { foreach ($nameWildcards as $nameWildcard) { // the $event object is passed by reference // and will be modified down the chain $this->apply($nameWildcard, $event->arguments(), $modify, $event); } } return $event->argument($modify); } /** * Normalizes and globally sets the configured options * * @return $this */ protected function bakeOptions() { // convert the old plugin option syntax to the new one foreach ($this->options as $key => $value) { // detect option keys with the `vendor.plugin.option` format if (preg_match('/^([a-z0-9-]+\.[a-z0-9-]+)\.(.*)$/i', $key, $matches) === 1) { list(, $plugin, $option) = $matches; // verify that it's really a plugin option if (isset(static::$plugins[str_replace('.', '/', $plugin)]) !== true) { continue; } // ensure that the target option array exists // (which it will if the plugin has any options) if (isset($this->options[$plugin]) !== true) { $this->options[$plugin] = []; // @codeCoverageIgnore } // move the option to the plugin option array // don't overwrite nested arrays completely but merge them $this->options[$plugin] = array_replace_recursive($this->options[$plugin], [$option => $value]); unset($this->options[$key]); } } Config::$data = $this->options; return $this; } /** * Sets the directory structure * * @param array|null $roots * @return $this */ protected function bakeRoots(array $roots = null) { $roots = array_merge($this->core->roots(), (array)$roots); $this->roots = Ingredients::bake($roots); return $this; } /** * Sets the Url structure * * @param array|null $urls * @return $this */ protected function bakeUrls(array $urls = null) { $urls = array_merge($this->core->urls(), (array)$urls); $this->urls = Ingredients::bake($urls); return $this; } /** * Returns all available blueprints for this installation * * @param string $type * @return array */ public function blueprints(string $type = 'pages'): array { $blueprints = []; foreach ($this->extensions('blueprints') as $name => $blueprint) { if (dirname($name) === $type) { $name = basename($name); $blueprints[$name] = $name; } } foreach (glob($this->root('blueprints') . '/' . $type . '/*.yml') as $blueprint) { $name = F::name($blueprint); $blueprints[$name] = $name; } ksort($blueprints); return array_values($blueprints); } /** * Calls any Kirby route * * @param string|null $path * @param string|null $method * @return mixed */ public function call(string $path = null, string $method = null) { $path ??= $this->path(); $method ??= $this->request()->method(); return $this->router()->call($path, $method); } /** * Creates an instance with the same * initial properties * * @param array $props * @param bool $setInstance If false, the instance won't be set globally * @return static */ public function clone(array $props = [], bool $setInstance = true) { $props = array_replace_recursive($this->propertyData, $props); $clone = new static($props, $setInstance); $clone->data = $this->data; return $clone; } /** * Returns a specific user-defined collection * by name. All relevant dependencies are * automatically injected * * @param string $name * @return \Kirby\Cms\Collection|null */ public function collection(string $name) { return $this->collections()->get($name, [ 'kirby' => $this, 'site' => $this->site(), 'pages' => $this->site()->children(), 'users' => $this->users() ]); } /** * Returns all user-defined collections * * @return \Kirby\Cms\Collections */ public function collections() { return $this->collections = $this->collections ?? new Collections(); } /** * Returns a core component * * @internal * @param string $name * @return mixed */ public function component($name) { return $this->extensions['components'][$name] ?? null; } /** * Returns the content extension * * @internal * @return string */ public function contentExtension(): string { return $this->options['content']['extension'] ?? 'txt'; } /** * Returns files that should be ignored when scanning folders * * @internal * @return array */ public function contentIgnore(): array { return $this->options['content']['ignore'] ?? Dir::$ignore; } /** * Generates a non-guessable token based on model * data and a configured salt * * @param mixed $model Object to pass to the salt callback if configured * @param string $value Model data to include in the generated token * @return string */ public function contentToken($model, string $value): string { if (method_exists($model, 'root') === true) { $default = $model->root(); } else { $default = $this->root('content'); } $salt = $this->option('content.salt', $default); if (is_a($salt, 'Closure') === true) { $salt = $salt($model); } return hash_hmac('sha1', $value, $salt); } /** * Calls a page controller by name * and with the given arguments * * @internal * @param string $name * @param array $arguments * @param string $contentType * @return array */ public function controller(string $name, array $arguments = [], string $contentType = 'html'): array { $name = basename(strtolower($name)); if ($controller = $this->controllerLookup($name, $contentType)) { return (array)$controller->call($this, $arguments); } if ($contentType !== 'html') { // no luck for a specific representation controller? // let's try the html controller instead if ($controller = $this->controllerLookup($name)) { return (array)$controller->call($this, $arguments); } } // still no luck? Let's take the site controller if ($controller = $this->controllerLookup('site')) { return (array)$controller->call($this, $arguments); } return []; } /** * Try to find a controller by name * * @param string $name * @param string $contentType * @return \Kirby\Toolkit\Controller|null */ protected function controllerLookup(string $name, string $contentType = 'html') { if ($contentType !== null && $contentType !== 'html') { $name .= '.' . $contentType; } // controller on disk if ($controller = Controller::load($this->root('controllers') . '/' . $name . '.php')) { return $controller; } // registry controller if ($controller = $this->extension('controllers', $name)) { return is_a($controller, 'Kirby\Toolkit\Controller') ? $controller : new Controller($controller); } return null; } /** * Get access to object that lists * all parts of Kirby core * * @return \Kirby\Cms\Core */ public function core() { return $this->core; } /** * Checks/returns a CSRF token * @since 3.7.0 * * @param string|null $check Pass a token here to compare it to the one in the session * @return string|bool Either the token or a boolean check result */ public function csrf(?string $check = null) { $session = $this->session(); // no arguments, generate/return a token // (check explicitly if there have been no arguments at all; // checking for null introduces a security issue because null could come // from user input or bugs in the calling code!) if (func_num_args() === 0) { $token = $session->get('kirby.csrf'); if (is_string($token) !== true) { $token = bin2hex(random_bytes(32)); $session->set('kirby.csrf', $token); } return $token; } // argument has been passed, check the token if ( is_string($check) === true && is_string($session->get('kirby.csrf')) === true ) { return hash_equals($session->get('kirby.csrf'), $check) === true; } return false; } /** * Returns the default language object * * @return \Kirby\Cms\Language|null */ public function defaultLanguage() { return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default(); } /** * Destroy the instance singleton and * purge other static props * * @internal */ public static function destroy(): void { static::$plugins = []; static::$instance = null; } /** * Detect the preferred language from the visitor object * * @return \Kirby\Cms\Language */ public function detectedLanguage() { $languages = $this->languages(); $visitor = $this->visitor(); foreach ($visitor->acceptedLanguages() as $lang) { if ($language = $languages->findBy('locale', $lang->locale(LC_ALL))) { return $language; } } foreach ($visitor->acceptedLanguages() as $lang) { if ($language = $languages->findBy('code', $lang->code())) { return $language; } } return $this->defaultLanguage(); } /** * Returns the Email singleton * * @param mixed $preset * @param array $props * @return \Kirby\Email\Email */ public function email($preset = [], array $props = []) { $debug = $props['debug'] ?? false; $props = (new Email($preset, $props))->toArray(); return ($this->component('email'))($this, $props, $debug); } /** * Returns the environment object with access * to the detected host, base url and dedicated options * * @return \Kirby\Http\Environment */ public function environment() { return $this->environment ?? new Environment(); } /** * Finds any file in the content directory * * @param string $path * @param mixed $parent * @param bool $drafts * @return \Kirby\Cms\File|null */ public function file(string $path, $parent = null, bool $drafts = true) { $parent = $parent ?? $this->site(); $id = dirname($path); $filename = basename($path); if (is_a($parent, 'Kirby\Cms\User') === true) { return $parent->file($filename); } if (is_a($parent, 'Kirby\Cms\File') === true) { $parent = $parent->parent(); } if ($id === '.') { if ($file = $parent->file($filename)) { return $file; } if ($file = $this->site()->file($filename)) { return $file; } return null; } if ($page = $this->page($id, $parent, $drafts)) { return $page->file($filename); } if ($page = $this->page($id, null, $drafts)) { return $page->file($filename); } return null; } /** * Return an image from any page * specified by the path * * Example: * * * @param string|null $path * @return \Kirby\Cms\File|null * * @todo merge with App::file() */ public function image(?string $path = null) { if ($path === null) { return $this->site()->page()->image(); } $uri = dirname($path); $filename = basename($path); if ($uri === '.') { $uri = null; } switch ($uri) { case '/': $parent = $this->site(); break; case null: $parent = $this->site()->page(); break; default: $parent = $this->site()->page($uri); break; } if ($parent) { return $parent->image($filename); } return null; } /** * Returns the current App instance * * @param \Kirby\Cms\App|null $instance * @param bool $lazy If `true`, the instance is only returned if already existing * @return static|null */ public static function instance(self $instance = null, bool $lazy = false) { if ($instance === null) { if ($lazy === true) { return static::$instance; } else { return static::$instance ?? new static(); } } return static::$instance = $instance; } /** * Takes almost any kind of input and * tries to convert it into a valid response * * @internal * @param mixed $input * @return \Kirby\Http\Response */ public function io($input) { // use the current response configuration $response = $this->response(); // any direct exception will be turned into an error page if (is_a($input, 'Throwable') === true) { if (is_a($input, 'Kirby\Exception\Exception') === true) { $code = $input->getHttpCode(); } else { $code = $input->getCode(); } $message = $input->getMessage(); if ($code < 400 || $code > 599) { $code = 500; } if ($errorPage = $this->site()->errorPage()) { return $response->code($code)->send($errorPage->render([ 'errorCode' => $code, 'errorMessage' => $message, 'errorType' => get_class($input) ])); } return $response ->code($code) ->type('text/html') ->send($message); } // Empty input if (empty($input) === true) { return $this->io(new NotFoundException()); } // (Modified) global response configuration, e.g. in routes if (is_a($input, 'Kirby\Cms\Responder') === true) { // return the passed object unmodified (without injecting headers // from the global object) to allow a complete response override // https://github.com/getkirby/kirby/pull/4144#issuecomment-1034766726 return $input->send(); } // Responses if (is_a($input, 'Kirby\Http\Response') === true) { $data = $input->toArray(); // inject headers from the global response configuration // lazily (only if they are not already set); // the case-insensitive nature of headers will be // handled by PHP's `header()` function $data['headers'] = array_merge($response->headers(), $data['headers']); return new Response($data); } // Pages if (is_a($input, 'Kirby\Cms\Page')) { try { $html = $input->render(); } catch (ErrorPageException $e) { return $this->io($e); } if ($input->isErrorPage() === true) { if ($response->code() === null) { $response->code(404); } } return $response->send($html); } // Files if (is_a($input, 'Kirby\Cms\File')) { return $response->redirect($input->mediaUrl(), 307)->send(); } // Simple HTML response if (is_string($input) === true) { return $response->send($input); } // array to json conversion if (is_array($input) === true) { return $response->json($input)->send(); } throw new InvalidArgumentException('Unexpected input'); } /** * Renders a single KirbyTag with the given attributes * * @internal * @param string|array $type Tag type or array with all tag arguments * (the key of the first element becomes the type) * @param string|null $value * @param array $attr * @param array $data * @return string */ public function kirbytag($type, ?string $value = null, array $attr = [], array $data = []): string { if (is_array($type) === true) { $kirbytag = $type; $type = key($kirbytag); $value = current($kirbytag); $attr = $kirbytag; // check data attribute and separate from attr data if exists if (isset($attr['data']) === true) { $data = $attr['data']; unset($attr['data']); } } $data['kirby'] = $data['kirby'] ?? $this; $data['site'] = $data['site'] ?? $data['kirby']->site(); $data['parent'] = $data['parent'] ?? $data['site']->page(); return (new KirbyTag($type, $value, $attr, $data, $this->options))->render(); } /** * KirbyTags Parser * * @internal * @param string|null $text * @param array $data * @return string */ public function kirbytags(string $text = null, array $data = []): string { $data['kirby'] ??= $this; $data['site'] ??= $data['kirby']->site(); $data['parent'] ??= $data['site']->page(); $options = $this->options; $text = $this->apply('kirbytags:before', compact('text', 'data', 'options'), 'text'); $text = KirbyTags::parse($text, $data, $options); $text = $this->apply('kirbytags:after', compact('text', 'data', 'options'), 'text'); return $text; } /** * Parses KirbyTags first and Markdown afterwards * * @internal * @param string|null $text * @param array $options * @param bool $inline (deprecated: use $options['markdown']['inline'] instead) * @return string * @todo remove $inline parameter in in 3.8.0 */ public function kirbytext(string $text = null, array $options = [], bool $inline = false): string { // warning for deprecated fourth parameter // @codeCoverageIgnoreStart if (func_num_args() === 3) { Helpers::deprecated('Cms\App::kirbytext(): the $inline parameter is deprecated and will be removed in Kirby 3.8.0. Use $options[\'markdown\'][\'inline\'] instead.'); } // @codeCoverageIgnoreEnd $options['markdown']['inline'] ??= $inline; $text = $this->apply('kirbytext:before', compact('text'), 'text'); $text = $this->kirbytags($text, $options); $text = $this->markdown($text, $options['markdown']); if ($this->option('smartypants', false) !== false) { $text = $this->smartypants($text); } $text = $this->apply('kirbytext:after', compact('text'), 'text'); return $text; } /** * Returns the current language * * @param string|null $code * @return \Kirby\Cms\Language|null */ public function language(string $code = null) { if ($this->multilang() === false) { return null; } if ($code === 'default') { return $this->languages()->default(); } if ($code !== null) { return $this->languages()->find($code); } return $this->language = $this->language ?? $this->languages()->default(); } /** * Returns the current language code * * @internal * @param string|null $languageCode * @return string|null */ public function languageCode(string $languageCode = null): ?string { if ($language = $this->language($languageCode)) { return $language->code(); } return null; } /** * Returns all available site languages * * @param bool * @return \Kirby\Cms\Languages */ public function languages(bool $clone = true) { if ($this->languages !== null) { return $clone === true ? clone $this->languages : $this->languages; } return $this->languages = Languages::load(); } /** * Access Kirby's part loader * * @return \Kirby\Cms\Loader */ public function load() { return new Loader($this); } /** * Returns the app's locks object * * @return \Kirby\Cms\ContentLocks */ public function locks(): ContentLocks { if ($this->locks !== null) { return $this->locks; } return $this->locks = new ContentLocks(); } /** * Parses Markdown * * @internal * @param string|null $text * @param bool|array $options Boolean inline value is deprecated, use `['inline' => true]` instead * @return string * @todo remove boolean $options in in 3.8.0 */ public function markdown(string $text = null, $options = null): string { // support for the old syntax to enable inline mode as second argument // @codeCoverageIgnoreStart if (is_bool($options) === true) { Helpers::deprecated('Cms\App::markdown(): Passing a boolean as second parameter has been deprecated and won\'t be supported anymore in Kirby 3.8.0. Instead pass array with the key "inline" set to true or false.'); $options = [ 'inline' => $options ]; } // @codeCoverageIgnoreEnd // merge global options with local options $options = array_merge( $this->options['markdown'] ?? [], (array)$options ); // TODO: remove passing the $inline parameter in 3.8.0 // $options['inline'] is set to `false` to avoid the deprecation // warning in the component; this can also be removed in 3.8.0 $inline = $options['inline'] ??= false; return ($this->component('markdown'))($this, $text, $options, $inline); } /** * Check for a multilang setup * * @return bool */ public function multilang(): bool { if ($this->multilang !== null) { return $this->multilang; } return $this->multilang = $this->languages()->count() !== 0; } /** * Returns the nonce, which is used * in the panel for inline scripts * @since 3.3.0 * * @return string */ public function nonce(): string { return $this->nonce = $this->nonce ?? base64_encode(random_bytes(20)); } /** * Load a specific configuration option * * @param string $key * @param mixed $default * @return mixed */ public function option(string $key, $default = null) { return A::get($this->options, $key, $default); } /** * Returns all configuration options * * @return array */ public function options(): array { return $this->options; } /** * Load all options from files in site/config * * @return array */ protected function optionsFromConfig(): array { // create an empty config container Config::$data = []; // load the main config options $root = $this->root('config'); $options = F::load($root . '/config.php', []); // merge into one clean options array return $this->options = array_replace_recursive(Config::$data, $options); } /** * Load all options for the current * server environment * * @param array $props * @return array */ protected function optionsFromEnvironment(array $props = []): array { $globalUrl = $this->options['url'] ?? null; // create the environment based on the URL setup $this->environment = new Environment([ 'allowed' => $globalUrl, 'cli' => $props['cli'] ?? null, ], $props['server'] ?? null); // merge into one clean options array $options = $this->environment()->options($this->root('config')); $this->options = array_replace_recursive($this->options, $options); // reload the environment if the environment config has overridden // the `url` option; this ensures that the base URL is correct $envUrl = $this->options['url'] ?? null; if ($envUrl !== $globalUrl) { $this->environment->detect([ 'allowed' => $envUrl, 'cli' => $props['cli'] ?? null ], $props['server'] ?? null); } return $this->options; } /** * Inject options from Kirby instance props * * @param array $options * @return array */ protected function optionsFromProps(array $options = []): array { return $this->options = array_replace_recursive( $this->options, $options ); } /** * Merge last-minute options from ready callback * * @return array */ protected function optionsFromReadyCallback(): array { if (isset($this->options['ready']) === true && is_callable($this->options['ready']) === true) { // fetch last-minute options from the callback $options = (array)$this->options['ready']($this); // inject all last-minute options recursively $this->options = array_replace_recursive($this->options, $options); // update the system with changed options if ( isset($options['debug']) === true || isset($options['whoops']) === true || isset($options['editor']) === true ) { $this->handleErrors(); } if (isset($options['debug']) === true) { $this->api = null; } if (isset($options['home']) === true || isset($options['error']) === true) { $this->site = null; } // checks custom language definition for slugs if ($slugsOption = $this->option('slugs')) { // slugs option must be set to string or "slugs" => ["language" => "de"] as array if (is_string($slugsOption) === true || isset($slugsOption['language']) === true) { $this->i18n(); } } } return $this->options; } /** * Returns any page from the content folder * * @param string|null $id * @param \Kirby\Cms\Page|\Kirby\Cms\Site|null $parent * @param bool $drafts * @return \Kirby\Cms\Page|null */ public function page(?string $id = null, $parent = null, bool $drafts = true) { if ($id === null) { return null; } $parent = $parent ?? $this->site(); if ($page = $parent->find($id)) { /** * We passed a single $id, we can be sure that the result is * @var \Kirby\Cms\Page $page */ return $page; } if ($drafts === true && $draft = $parent->draft($id)) { return $draft; } return null; } /** * Returns the request path * * @return string */ public function path(): string { if (is_string($this->path) === true) { return $this->path; } $current = $this->request()->path()->toString(); $index = $this->environment()->baseUri()->path()->toString(); $path = Str::afterStart($current, $index); return $this->setPath($path)->path; } /** * Returns the Response object for the * current request * * @param string|null $path * @param string|null $method * @return \Kirby\Http\Response */ public function render(string $path = null, string $method = null) { return $this->io($this->call($path, $method)); } /** * Returns the Request singleton * * @return \Kirby\Http\Request */ public function request() { if ($this->request !== null) { return $this->request; } $env = $this->environment(); return $this->request = new Request([ 'cli' => $env->cli(), 'url' => $env->requestUri() ]); } /** * Path resolver for the router * * @internal * @param string|null $path * @param string|null $language * @return mixed * @throws \Kirby\Exception\NotFoundException if the home page cannot be found */ public function resolve(string $path = null, string $language = null) { // set the current translation $this->setCurrentTranslation($language); // set the current locale $this->setCurrentLanguage($language); // the site is needed a couple times here $site = $this->site(); // use the home page if ($path === null) { if ($homePage = $site->homePage()) { return $homePage; } throw new NotFoundException('The home page does not exist'); } // search for the page by path $page = $site->find($path); // search for a draft if the page cannot be found if (!$page && $draft = $site->draft($path)) { if ($this->user() || $draft->isVerified($this->request()->get('token'))) { $page = $draft; } } // try to resolve content representations if the path has an extension $extension = F::extension($path); // no content representation? then return the page if (empty($extension) === true) { return $page; } // only try to return a representation // when the page has been found if ($page) { try { $response = $this->response(); $output = $page->render([], $extension); // attach a MIME type based on the representation // only if no custom MIME type was set if ($response->type() === null) { $response->type($extension); } return $response->body($output); } catch (NotFoundException $e) { return null; } } $id = dirname($path); $filename = basename($path); // try to resolve image urls for pages and drafts if ($page = $site->findPageOrDraft($id)) { return $page->file($filename); } // try to resolve site files at least return $site->file($filename); } /** * Response configuration * * @return \Kirby\Cms\Responder */ public function response() { return $this->response = $this->response ?? new Responder(); } /** * Returns all user roles * * @return \Kirby\Cms\Roles */ public function roles() { return $this->roles = $this->roles ?? Roles::load($this->root('roles')); } /** * Returns a system root * * @param string $type * @return string|null */ public function root(string $type = 'index'): ?string { return $this->roots->__get($type); } /** * Returns the directory structure * * @return \Kirby\Cms\Ingredients */ public function roots() { return $this->roots; } /** * Returns the currently active route * * @return \Kirby\Http\Route|null */ public function route() { return $this->router()->route(); } /** * Returns the Router singleton * * @internal * @return \Kirby\Http\Router */ public function router() { $routes = $this->routes(); if ($this->multilang() === true) { foreach ($routes as $index => $route) { if (empty($route['language']) === false) { unset($routes[$index]); } } } $hooks = [ 'beforeEach' => function ($route, $path, $method) { $this->trigger('route:before', compact('route', 'path', 'method')); }, 'afterEach' => function ($route, $path, $method, $result, $final) { return $this->apply('route:after', compact('route', 'path', 'method', 'result', 'final'), 'result'); } ]; return $this->router ??= new Router($routes, $hooks); } /** * Returns all defined routes * * @internal * @return array */ public function routes(): array { if (is_array($this->routes) === true) { return $this->routes; } $registry = $this->extensions('routes'); $system = $this->core->routes(); $routes = array_merge($system['before'], $registry, $system['after']); return $this->routes = $routes; } /** * Returns the current session object * * @param array $options Additional options, see the session component * @return \Kirby\Session\Session */ public function session(array $options = []) { $session = $this->sessionHandler()->get($options); // disable caching for sessions that use the `Authorization` header; // cookie sessions are already covered by the `Cookie` class if ($session->mode() === 'manual') { $this->response()->cache(false); $this->response()->header('Cache-Control', 'no-store, private', true); } return $session; } /** * Returns the session handler * * @return \Kirby\Session\AutoSession */ public function sessionHandler() { $this->sessionHandler = $this->sessionHandler ?? new AutoSession($this->root('sessions'), $this->option('session', [])); return $this->sessionHandler; } /** * Create your own set of languages * * @param array|null $languages * @return $this */ protected function setLanguages(array $languages = null) { if ($languages !== null) { $objects = []; foreach ($languages as $props) { $objects[] = new Language($props); } $this->languages = new Languages($objects); } return $this; } /** * Sets the request path that is * used for the router * * @param string|null $path * @return $this */ protected function setPath(string $path = null) { $this->path = $path !== null ? trim($path, '/') : null; return $this; } /** * Sets the request * * @param array|null $request * @return $this */ protected function setRequest(array $request = null) { if ($request !== null) { $this->request = new Request($request); } return $this; } /** * Create your own set of roles * * @param array|null $roles * @return $this */ protected function setRoles(array $roles = null) { if ($roles !== null) { $this->roles = Roles::factory($roles, [ 'kirby' => $this ]); } return $this; } /** * Sets a custom Site object * * @param \Kirby\Cms\Site|array|null $site * @return $this */ protected function setSite($site = null) { if (is_array($site) === true) { $site = new Site($site + [ 'kirby' => $this ]); } $this->site = $site; return $this; } /** * Returns the Environment object * @deprecated 3.7.0 Use `$kirby->environment()` instead * * @return \Kirby\Http\Environment * @todo Start throwing deprecation warnings in 3.8.0 * @todo Remove in 3.9.0 * @codeCoverageIgnore */ public function server() { return $this->environment(); } /** * Initializes and returns the Site object * * @return \Kirby\Cms\Site */ public function site() { return $this->site = $this->site ?? new Site([ 'errorPageId' => $this->options['error'] ?? 'error', 'homePageId' => $this->options['home'] ?? 'home', 'kirby' => $this, 'url' => $this->url('index'), ]); } /** * Applies the smartypants rule on the text * * @internal * @param string|null $text * @return string */ public function smartypants(string $text = null): string { $options = $this->option('smartypants', []); if ($options === false) { return $text; } elseif (is_array($options) === false) { $options = []; } if ($this->multilang() === true) { $languageSmartypants = $this->language()->smartypants() ?? []; if (empty($languageSmartypants) === false) { $options = array_merge($options, $languageSmartypants); } } return ($this->component('smartypants'))($this, $text, $options); } /** * Uses the snippet component to create * and return a template snippet * * @internal * @param mixed $name * @param array|object $data Variables or an object that becomes `$item` * @param bool $return On `false`, directly echo the snippet * @return string|null */ public function snippet($name, $data = [], bool $return = true): ?string { if (is_object($data) === true) { $data = ['item' => $data]; } $snippet = ($this->component('snippet'))($this, $name, array_merge($this->data, $data)); if ($return === true) { return $snippet; } echo $snippet; return null; } /** * System check class * * @return \Kirby\Cms\System */ public function system() { return $this->system = $this->system ?? new System($this); } /** * Uses the template component to initialize * and return the Template object * * @internal * @return \Kirby\Cms\Template * @param string $name * @param string $type * @param string $defaultType */ public function template(string $name, string $type = 'html', string $defaultType = 'html') { return ($this->component('template'))($this, $name, $type, $defaultType); } /** * Thumbnail creator * * @param string $src * @param string $dst * @param array $options * @return string */ public function thumb(string $src, string $dst, array $options = []): string { return ($this->component('thumb'))($this, $src, $dst, $options); } /** * Trigger a hook by name * * @internal * @param string $name Full event name * @param array $args Associative array of named event arguments * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) * @return void */ public function trigger(string $name, array $args = [], ?Event $originalEvent = null) { $event = $originalEvent ?? new Event($name, $args); if ($functions = $this->extension('hooks', $name)) { static $level = 0; static $triggered = []; $level++; foreach ($functions as $index => $function) { if (in_array($function, $triggered[$name] ?? []) === true) { continue; } // mark the hook as triggered, to avoid endless loops $triggered[$name][] = $function; // bind the App object to the hook $event->call($this, $function); } $level--; if ($level === 0) { $triggered = []; } } // trigger wildcard hooks if available $nameWildcards = $event->nameWildcards(); if ($originalEvent === null && count($nameWildcards) > 0) { foreach ($nameWildcards as $nameWildcard) { $this->trigger($nameWildcard, $args, $event); } } } /** * Returns a system url * * @param string $type * @param bool $object If set to `true`, the URL is converted to an object * @return string|\Kirby\Http\Uri|null */ public function url(string $type = 'index', bool $object = false) { $url = $this->urls->__get($type); if ($object === true) { if (Url::isAbsolute($url)) { return Url::toObject($url); } // index URL was configured without host, use the current host return Uri::current([ 'path' => $url, 'query' => null ]); } return $url; } /** * Returns the url structure * * @return \Kirby\Cms\Ingredients */ public function urls() { return $this->urls; } /** * Returns the current version number from * the composer.json (Keep that up to date! :)) * * @return string|null * @throws \Kirby\Exception\LogicException if the Kirby version cannot be detected */ public static function version(): ?string { try { return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null; } catch (Throwable $e) { throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.'); } } /** * Creates a hash of the version number * * @return string */ public static function versionHash(): string { return md5(static::version()); } /** * Returns the visitor object * * @return \Kirby\Http\Visitor */ public function visitor() { return $this->visitor = $this->visitor ?? new Visitor(); } }