sterzycom/kirby/kirby.php

785 lines
No EOL
20 KiB
PHP

<?php
use Kirby\Component;
use Kirby\Pipe;
use Kirby\Registry;
use Kirby\Request;
use Kirby\Roots;
use Kirby\Urls;
class Kirby {
static public $version = '2.3.0';
static public $instance;
static public $hooks = array();
static public $triggered = array();
public $roots;
public $urls;
public $cache;
public $path;
public $options = array();
public $routes;
public $router;
public $route;
public $site;
public $page;
public $plugins;
public $response;
public $request;
public $components = [];
public $registry;
static public function instance($class = null) {
if(!is_null(static::$instance)) return static::$instance;
return static::$instance = $class ? new $class : new static;
}
static public function version() {
return static::$version;
}
public function __construct($options = array()) {
$this->roots = new Roots(dirname(__DIR__));
$this->urls = new Urls();
$this->registry = new Registry($this);
$this->options = array_merge($this->defaults(), $options);
$this->path = implode('/', (array)url::fragments(detect::path()));
// make sure the instance is stored / overwritten
static::$instance = $this;
}
public function defaults() {
$defaults = array(
'url' => false,
'timezone' => 'UTC',
'license' => null,
'rewrite' => true,
'error' => 'error',
'home' => 'home',
'locale' => 'en_US.UTF8',
'routes' => array(),
'headers' => array(),
'languages' => array(),
'roles' => array(),
'cache' => false,
'debug' => 'env',
'ssl' => false,
'cache.driver' => 'file',
'cache.options' => array(),
'cache.ignore' => array(),
'cache.autoupdate' => true,
'date.handler' => 'date',
'kirbytext.video.class' => 'video',
'kirbytext.video.width' => false,
'kirbytext.video.height' => false,
'kirbytext.video.youtube.options' => array(),
'kirbytext.video.vimeo.options' => array(),
'kirbytext.image.figure' => true,
'content.file.extension' => 'txt',
'content.file.ignore' => array(),
'content.file.normalize' => false,
'email.service' => 'mail',
'email.to' => null,
'email.replyTo' => null,
'email.subject' => null,
'email.body' => null,
'email.options' => array(),
);
return $defaults;
}
public function roots() {
return $this->roots;
}
public function urls() {
return $this->urls;
}
public function registry() {
return $this->registry;
}
public function url() {
return $this->urls->index();
}
public function options() {
return $this->options;
}
public function option($key, $default = null) {
return a::get($this->options, $key, $default);
}
public function path() {
return $this->path;
}
public function page() {
return $this->page;
}
public function response() {
return $this->response;
}
/**
* Install a new entry in the registry
*/
public function set() {
return call_user_func_array([$this->registry, 'set'], func_get_args());
}
/**
* Retrieve an entry from the registry
*/
public function get() {
return call_user_func_array([$this->registry, 'get'], func_get_args());
}
public function configure() {
// load all available config files
$root = $this->roots()->config();
$configs = array(
'main' => 'config.php',
'host' => 'config.' . server::get('SERVER_NAME') . '.php',
'addr' => 'config.' . server::get('SERVER_ADDR') . '.php',
);
$allowed = array_filter(dir::read($root), function($file) {
return substr($file, 0, 7) === 'config.' and substr($file, -4) === '.php';
});
foreach($configs as $config) {
$file = $root . DS . $config;
if(in_array($config, $allowed, true) and file_exists($file)) include_once($file);
}
// apply the options
$this->options = array_merge($this->options, c::$data);
// overwrite the autodetected url
if($this->options['url']) {
$this->urls->index = $this->options['url'];
}
// connect the url class with its handlers
url::$home = $this->urls()->index();
url::$to = $this->option('url.to', function($url = '', $lang = null) {
if(url::isAbsolute($url)) return $url;
$start = substr($url, 0, 1);
switch($start) {
case '#':
return $url;
break;
case '.':
return page()->url() . '/' . $url;
break;
default:
if($page = page($url)) {
// use the "official" page url
return $page->url($lang);
} else {
// don't convert absolute urls
return url::makeAbsolute($url);
}
break;
}
});
// setup the pagination redirect to the error page
pagination::$defaults['redirect'] = $this->option('error');
// setting up the email class
email::$defaults['service'] = $this->option('email.service');
email::$defaults['from'] = $this->option('email.from');
email::$defaults['to'] = $this->option('email.to');
email::$defaults['replyTo'] = $this->option('email.replyTo');
email::$defaults['subject'] = $this->option('email.subject');
email::$defaults['body'] = $this->option('email.body');
email::$defaults['options'] = $this->option('email.options');
// simple error handling
if($this->options['debug'] === true) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else if($this->options['debug'] === false) {
error_reporting(0);
ini_set('display_errors', 0);
}
}
/**
* Registers all routes
*
* @param array $routes New routes
* @return array
*/
public function routes($routes = array()) {
// extend the existing routes
if(!empty($routes) and is_array($routes)) {
return $this->options['routes'] = array_merge($this->options['routes'], $routes);
}
$routes = $this->options['routes'];
$kirby = $this;
$site = $this->site();
if($site->multilang()) {
foreach($site->languages() as $lang) {
$routes[] = array(
'pattern' => ltrim($lang->url . '/(:all?)', '/'),
'method' => 'ALL',
'lang' => $lang,
'action' => function($path = null) use($kirby, $site) {
return $site->visit($path, $kirby->route->lang->code());
}
);
}
// fallback for the homepage
$routes[] = array(
'pattern' => '/',
'method' => 'ALL',
'action' => function() use($kirby, $site) {
// check if the language detector is activated
if($kirby->option('language.detect')) {
if(s::get('language') and $language = $kirby->site()->sessionLanguage()) {
// $language is already set but the user wants to
// select the default language
$referer = r::referer();
if(!empty($referer) && str::startsWith($referer, $this->urls()->index())) {
$language = $kirby->site()->defaultLanguage();
}
} else {
// detect the user language
$language = $kirby->site()->detectedLanguage();
}
} else {
// always use the default language if the detector is disabled
$language = $kirby->site()->defaultLanguage();
}
// redirect to the language homepage if necessary
if($language->url != '/' and $language->url != '') {
go($language->url());
}
// plain home pages
return $site->visit('/', $language->code());
}
);
}
// tinyurl handling
$routes['tinyurl'] = $this->component('tinyurl')->route();
// home redirect
$routes['homeRedirect'] = array(
'pattern' => $this->options['home'],
'action' => function() {
redirect::send(page('home')->url(), 307);
}
);
// plugin assets
$routes['pluginAssets'] = array(
'pattern' => 'assets/plugins/(:any)/(:all)',
'method' => 'GET',
'action' => function($plugin, $path) use($kirby) {
$root = $kirby->roots()->plugins() . DS . $plugin . DS . 'assets' . DS . $path;
$file = new Media($root);
if($file->exists()) {
return new Response(f::read($root), f::extension($root));
} else {
return new Response('The file could not be found', f::extension($path), 404);
}
}
);
// all other urls
$routes['others'] = array(
'pattern' => '(:all)',
'method' => 'ALL',
'action' => function($path = null) use($site, $kirby) {
// visit the currently active page
$page = $site->visit($path);
// react on errors for invalid URLs
if($page->isErrorPage() and $page->uri() != $path) {
// get the filename
$filename = rawurldecode(basename($path));
$pagepath = dirname($path);
// check if there's a page for the parent path
if($page = $site->find($pagepath)) {
// check if there's a file for the last element of the path
if($file = $page->file($filename)) {
go($file->url());
}
}
// return the error page if there's no such page
return $site->errorPage();
}
return $page;
}
);
return $routes;
}
/**
* Loads all available plugins for the site
*
* @return array
*/
public function plugins() {
// check for a cached plugins array
if(!is_null($this->plugins)) return $this->plugins;
// get the plugins root
$root = $this->roots->plugins();
// start the plugin registry
$this->plugins = array();
// check for an existing plugins dir
if(!is_dir($root)) return $this->plugins;
foreach(array_diff(scandir($root), array('.', '..')) as $file) {
if(is_dir($root . DS . $file)) {
$this->plugin($file, 'dir');
} else if(f::extension($file) == 'php') {
$this->plugin(f::name($file), 'file');
}
}
return $this->plugins;
}
/**
* Loads a single plugin
*
* @param string $name
* @param string $mode
* @return mixed
*/
public function plugin($name, $mode = 'dir') {
if(isset($this->plugins[$name])) return $this->plugins[$name];
if($mode == 'dir') {
$file = $this->roots->plugins() . DS . $name . DS . $name . '.php';
} else {
$file = $this->roots->plugins() . DS . $name . '.php';
}
// make the kirby variable available in plugin files
$kirby = $this;
if(file_exists($file)) return $this->plugins[$name] = include_once($file);
return false;
}
/**
* Load all default extensions
*/
public function extensions() {
// load all kirby tags and field methods
include_once(__DIR__ . DS . 'extensions' . DS . 'tags.php');
include_once(__DIR__ . DS . 'extensions' . DS . 'methods.php');
// install additional kirby tags
kirbytext::install($this->roots->tags());
}
/**
* Autoloads all page models
*/
public function models() {
if(!is_dir($this->roots()->models())) return false;
$root = $this->roots()->models();
$files = dir::read($root);
$load = array();
foreach($files as $file) {
if(f::extension($file) != 'php') continue;
$name = f::name($file);
$classname = str_replace(array('.', '-', '_'), '', $name . 'page');
$load[$classname] = $root . DS . $file;
// register the model
page::$models[$name] = $classname;
}
// start the autoloader
if(!empty($load)) {
load($load);
}
}
public function localize() {
$site = $this->site();
if($site->multilang() and !$site->language()) {
$site->language = $site->languages()->findDefault();
}
// set the local for the specific language
if(is_array($site->locale())) {
foreach($site->locale() as $key => $value) {
setlocale($key, $value);
}
} else {
setlocale(LC_ALL, $site->locale());
}
// additional language variables for multilang sites
if($site->multilang()) {
// path for the language file
$file = $this->roots()->languages() . DS . $site->language()->code() . '.php';
// load the file if it exists
if(file_exists($file)) include_once($file);
}
}
/**
* Returns the branch file
*
* @return string
*/
public function branch() {
// which branch?
$branch = count($this->options['languages']) > 0 ? 'multilang' : 'default';
// build the path for the branch file
return __DIR__ . DS . 'branches' . DS . $branch . '.php';
}
/**
* Initializes and returns the site object
* depending on the appropriate branch
*
* @return Site
*/
public function site() {
// check for a cached version of the site object
if(!is_null($this->site)) return $this->site;
// load all options
$this->configure();
// setup the cache
$this->cache();
// load the main branch file
include_once($this->branch());
// create the site object
return $this->site = new Site($this);
}
/**
* Cache setup
*
* @return Cache
*/
public function cache() {
if(!is_null($this->cache)) return $this->cache;
// cache setup
if($this->options['cache']) {
if($this->options['cache.driver'] == 'file' and empty($this->options['cache.options'])) {
$this->options['cache.options'] = array(
'root' => $this->roots()->cache()
);
}
return $this->cache = cache::setup($this->options['cache.driver'], $this->options['cache.options']);
} else {
return $this->cache = cache::setup('mock');
}
}
/**
* Renders the HTML for the page or fetches it from the cache
*
* @param Page $page
* @param boolean $headers
* @return string
*/
public function render(Page $page, $data = array(), $headers = true) {
// register the currently rendered page
$this->page = $page;
// send all headers for the page
if($headers) $page->headers();
// configure pagination urls
$query = (string)$this->request()->query();
$params = (string)$this->request()->params() . r($query, '?') . $query;
pagination::$defaults['url'] = $page->url() . r($params, '/') . $params;
// cache the result if possible
if($this->options['cache'] and $page->isCachable()) {
// try to read the cache by cid (cache id)
$cacheId = md5(url::current());
// check for modified content within the content folder
// and auto-expire the page cache in such a case
if($this->options['cache.autoupdate'] and $this->cache()->exists($cacheId)) {
// get the creation date of the cache file
$created = $this->cache()->created($cacheId);
// make sure to kill the cache if the site has been modified
if($this->site->wasModifiedAfter($created)) {
$this->cache()->remove($cacheId);
}
}
// try to fetch the template from cache
$template = $this->cache()->get($cacheId);
// fetch fresh content if the cache is empty
if(empty($template)) {
$template = $this->template($page, $data);
// store the result for the next round
$this->cache()->set($cacheId, $template);
}
return $template;
}
// return a fresh template
return $this->template($page, $data);
}
/**
* Template configuration
*
* @param Page $page
* @param array $data
* @return string
*/
public function template(Page $page, $data = array()) {
return $this->component('template')->render($page, $data);
}
public function request() {
if(!is_null($this->request)) return $this->request;
return $this->request = new Request($this);
}
public function router() {
return $this->router;
}
public function route() {
return $this->route;
}
/**
* Starts the router, renders the page and returns the response
*
* @return mixed
*/
public function launch() {
// this will trigger the configuration
$site = $this->site();
// force secure connections if enabled
if($this->option('ssl') and !r::secure()) {
// rebuild the current url with https
go(url::build(array('scheme' => 'https')));
}
// set the timezone for all date functions
date_default_timezone_set($this->options['timezone']);
// load all extensions
$this->extensions();
// load all plugins
$this->plugins();
// load all models
$this->models();
// start the router
$this->router = new Router($this->routes());
$this->route = $this->router->run($this->path());
// check for a valid route
if(is_null($this->route)) {
header::status('500');
header::type('json');
die(json_encode(array(
'status' => 'error',
'message' => 'Invalid route or request method'
)));
}
// call the router action with all arguments from the pattern
$response = call($this->route->action(), $this->route->arguments());
// load all language variables
// this can only be loaded once the router action has been called
// otherwise the current language is not yet available
$this->localize();
// build the response
$this->response = $this->component('response')->make($response);
// store the current language in the session
if($this->site()->multilang() && $language = $this->site()->language()) {
s::set('language', $language->code());
}
return $this->response;
}
/**
* Register a new hook
*
* @param string $hook The name of the hook
* @param closure $callback
*/
public function hook($hook, $callback) {
if(isset(static::$hooks[$hook]) and is_array(static::$hooks[$hook])) {
static::$hooks[$hook][] = $callback;
} else {
static::$hooks[$hook] = array($callback);
}
}
/**
* Trigger a hook
*
* @param string $hook The name of the hook
* @param mixed $args Additional arguments for the hook
* @return mixed
*/
public function trigger($hook, $args = null) {
if(isset(static::$hooks[$hook]) and is_array(static::$hooks[$hook])) {
foreach(static::$hooks[$hook] as $key => $callback) {
if(array_key_exists($hook, static::$triggered) && in_array($key, static::$triggered[$hook])) continue;
static::$triggered[$hook] = $key;
try {
call($callback, $args);
} catch(Exception $e) {
// caught callback error
}
}
}
}
static public function start() {
return kirby()->launch();
}
/**
* Register and fetch core components
*/
public function component($name, $component = null) {
if(is_null($component)) {
if(!isset($this->components[$name])) {
// load the default component if it exists
if(file_exists(__DIR__ . DS . 'kirby' . DS . 'component' . DS . strtolower($name) . '.php')) {
$this->component($name, 'Kirby\\Component\\' . $name);
} else {
throw new Exception('The component "' . $name . '" does not exist');
}
}
return $this->components[$name];
} else {
if(!is_string($component)) {
throw new Exception('Please provide a valid component name');
}
// init the component
$object = new $component($this);
if(!is_a($object, 'Kirby\\Component')) {
throw new Exception('The component "' . $name . '" must be an instance of the Kirby\\Component class');
}
if(!is_a($object, 'Kirby\\Component\\' . $name)) {
throw new Exception('The component "' . $name . '" must be an instance of the Kirby\\Component\\' . ucfirst($name) . ' class');
}
// add the component defaults
$this->options = array_merge($object->defaults(), $this->options);
// configure the component
$object->configure();
// register the component
$this->components[$name] = $object;
}
}
}