diff --git a/.gitmodules b/.gitmodules index d4d9a59..3ab1269 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "panel"] - path = panel - url = https://github.com/getkirby/panel.git -[submodule "kirby"] - path = kirby - url = https://github.com/getkirby/kirby.git [submodule "site/fields/markdown"] path = site/fields/markdown url = https://github.com/JonasDoebertin/kirby-visual-markdown.git diff --git a/README.md b/README.md index 4d1abad..0815586 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Sterzy.com This is the code for the http://sterzy.com website. It is based on the kirby cms (http://getkirby.com), which has it's own license agreement ([Kirby End User License Agreement](https://github.com/getkirby/starterkit/blob/master/license.md)). - The rest of the code is under this license: [License](https://svs.ankaa.uberspace.de/sterzy/sterzycom/blob/master/LICENSE "license"), [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/). # Installing diff --git a/index.php b/index.php index feef4bf..77ab730 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,5 @@ __DIR__ . DS . 'kirby.php', + 'kirby\\roots' => __DIR__ . DS . 'kirby' . DS . 'roots.php', + 'kirby\\urls' => __DIR__ . DS . 'kirby' . DS . 'urls.php', + 'kirby\\component' => __DIR__ . DS . 'kirby' . DS . 'component.php', + 'kirby\\registry' => __DIR__ . DS . 'kirby' . DS . 'registry.php', + 'kirby\\request' => __DIR__ . DS . 'kirby' . DS . 'request.php', + 'kirby\\request\\params' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'params.php', + 'kirby\\request\\query' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'query.php', + 'kirby\\request\\path' => __DIR__ . DS . 'kirby' . DS . 'request' . DS . 'path.php', + + // core components + 'kirby\\component\\template' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'template.php', + 'kirby\\component\\thumb' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'thumb.php', + 'kirby\\component\\markdown' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'markdown.php', + 'kirby\\component\\smartypants' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'smartypants.php', + 'kirby\\component\\snippet' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'snippet.php', + 'kirby\\component\\css' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'css.php', + 'kirby\\component\\js' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'js.php', + 'kirby\\component\\tinyurl' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'tinyurl.php', + 'kirby\\component\\response' => __DIR__ . DS . 'kirby' . DS . 'component' . DS . 'response.php', + + // traits + 'kirby\\traits\\image' => __DIR__ . DS . 'kirby' . DS . 'traits' . DS . 'image.php', + + // all core abstracts + 'assetabstract' => __DIR__ . DS . 'core' . DS . 'asset.php', + 'avatarabstract' => __DIR__ . DS . 'core' . DS . 'avatar.php', + 'pagesabstract' => __DIR__ . DS . 'core' . DS . 'pages.php', + 'childrenabstract' => __DIR__ . DS . 'core' . DS . 'children.php', + 'contentabstract' => __DIR__ . DS . 'core' . DS . 'content.php', + 'fieldabstract' => __DIR__ . DS . 'core' . DS . 'field.php', + 'fileabstract' => __DIR__ . DS . 'core' . DS . 'file.php', + 'filesabstract' => __DIR__ . DS . 'core' . DS . 'files.php', + 'kirbytextabstract' => __DIR__ . DS . 'core' . DS . 'kirbytext.php', + 'kirbytagabstract' => __DIR__ . DS . 'core' . DS . 'kirbytag.php', + 'pageabstract' => __DIR__ . DS . 'core' . DS . 'page.php', + 'roleabstract' => __DIR__ . DS . 'core' . DS . 'role.php', + 'rolesabstract' => __DIR__ . DS . 'core' . DS . 'roles.php', + 'siteabstract' => __DIR__ . DS . 'core' . DS . 'site.php', + 'usersabstract' => __DIR__ . DS . 'core' . DS . 'users.php', + 'userabstract' => __DIR__ . DS . 'core' . DS . 'user.php', + + // lib + 'pageextension' => __DIR__ . DS . 'lib' . DS . 'pageextension.php', + 'structure' => __DIR__ . DS . 'lib' . DS . 'structure.php', + + // parsedown + 'parsedown' => __DIR__ . DS . 'vendors' . DS . 'parsedown.php', + 'parsedownextra' => __DIR__ . DS . 'vendors' . DS . 'parsedownextra.php', + + // smartypants + 'smartypantstypographer_parser' => __DIR__ . DS . 'vendors' . DS . 'smartypants.php', + +)); + +// load all helper functions +include(__DIR__ . DS . 'helpers.php'); diff --git a/kirby/branches/default.php b/kirby/branches/default.php new file mode 100644 index 0000000..6f27318 --- /dev/null +++ b/kirby/branches/default.php @@ -0,0 +1,18 @@ + __DIR__ . DS . 'multilang' . DS . 'content.php', + 'field' => __DIR__ . DS . 'multilang' . DS . 'field.php', + 'file' => __DIR__ . DS . 'multilang' . DS . 'file.php', + 'language' => __DIR__ . DS . 'multilang' . DS . 'language.php', + 'languages' => __DIR__ . DS . 'multilang' . DS . 'languages.php', + 'page' => __DIR__ . DS . 'multilang' . DS . 'page.php', + 'site' => __DIR__ . DS . 'multilang' . DS . 'site.php', +)); \ No newline at end of file diff --git a/kirby/branches/multilang/content.php b/kirby/branches/multilang/content.php new file mode 100644 index 0000000..c80959c --- /dev/null +++ b/kirby/branches/multilang/content.php @@ -0,0 +1,41 @@ +name = f::name($this->name); + $this->language = $language; + + } + + public function realroot() { + return dirname($this->root()) . DS . $this->name() . '.' . $this->language . '.' . f::extension($this->root()); + } + + public function exists() { + return file_exists($this->realroot()); + } + + public function language() { + + if(!is_null($this->language)) return $this->language; + + $codes = $this->page->site()->languages()->codes(); + $code = f::extension(f::name($this->root)); + + return $this->language = in_array($code, $codes) ? $this->page->site()->languages()->find($code) : false; + + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/field.php b/kirby/branches/multilang/field.php new file mode 100644 index 0000000..d8dbeec --- /dev/null +++ b/kirby/branches/multilang/field.php @@ -0,0 +1,34 @@ +page->site(); + + // use current language if $lang not set + if(is_null($lang)) $lang = $site->language()->code(); + + // if language is default/fallback language + if($site->language($lang)->default()) return true; + + $current = $this->page->content($lang); + $default = $this->page->content($site->defaultLanguage->code); + + $field = $current->get($this->key); + $untranslated = $default->get($this->key)->value(); + + return $field->isNotEmpty() and $field->value() !== $untranslated; + + } + + +} diff --git a/kirby/branches/multilang/file.php b/kirby/branches/multilang/file.php new file mode 100644 index 0000000..eec16fb --- /dev/null +++ b/kirby/branches/multilang/file.php @@ -0,0 +1,170 @@ +page->textfile($this->filename(), $lang); + } + + /** + * Get the meta information + * + * @param string $lang optional language code + * @return Content + */ + public function meta($lang = null) { + + // get the content for the current language + if(is_null($lang)) { + + // the current language's content can be cached + if(isset($this->cache['meta'])) return $this->cache['meta']; + + // get the current content + $meta = $this->_meta($this->site->language->code); + + // get the fallback content + if($this->site->language->code != $this->site->defaultLanguage->code) { + + // fetch the default language content + $defaultMeta = $this->_meta($this->site->defaultLanguage->code); + + // replace all missing fields with values from the default content + foreach($defaultMeta->data as $key => $field) { + if(empty($meta->data[$key]->value)) { + $meta->data[$key] = $field; + } + } + + } + + // cache the meta for this language + return $this->cache['meta'] = $meta; + + // get the meta for another language + } else { + return $this->_meta($lang); + } + + } + + /** + * Private method to simplify meta fetching + * + * @return Content + */ + protected function _meta($lang) { + + // get the inventory + $inventory = $this->page->inventory(); + + // try to fetch the content for this language + $meta = isset($inventory['meta'][$this->filename][$lang]) ? $inventory['meta'][$this->filename][$lang] : null; + + // try to replace empty content with the default language content + if(empty($meta) and isset($inventory['meta'][$this->filename][$this->site->defaultLanguage->code])) { + $meta = $inventory['meta'][$this->filename][$this->site->defaultLanguage->code]; + } + + // find and cache the content for this language + return new Content($this->page, $this->page->root() . DS . $meta, $lang); + + } + + /** + * Renames the file and also its meta info txt + * + * @param string $filename + * @param boolean $safeName + */ + public function rename($name, $safeName = true) { + + $filename = $this->createNewFilename($name, $safeName); + $root = $this->dir() . DS . $filename; + + if(empty($name)) { + throw new Exception('The filename is missing'); + } + + if($root == $this->root()) return $filename; + + if(file_exists($root)) { + throw new Exception('A file with that name already exists'); + } + + if(!f::move($this->root(), $root)) { + throw new Exception('The file could not be renamed'); + } + + foreach($this->site->languages() as $lang) { + + // rename all meta files + $meta = $this->textfile($lang->code()); + + if(file_exists($meta)) { + f::move($meta, $this->page->textfile($filename, $lang->code())); + } + + } + + // reset the page cache + $this->page->reset(); + + // reset the basics + $this->root = $root; + $this->filename = $filename; + $this->name = $name; + $this->cache = array(); + + cache::flush(); + + return $filename; + + } + + public function update($data = array(), $lang = null) { + + $data = array_merge((array)$this->meta()->toArray(), $data); + + foreach($data as $k => $v) { + if(is_null($v)) unset($data[$k]); + } + + if(!data::write($this->textfile($lang), $data, 'kd')) { + throw new Exception('The file data could not be saved'); + } + + // reset the page cache + $this->page->reset(); + + // reset the file cache + $this->cache = array(); + + cache::flush(); + return true; + + } + + public function delete() { + + foreach($this->site->languages() as $lang) { + // delete the meta file for each language + f::remove($this->textfile($lang->code())); + } + + parent::delete(); + + return true; + + } + + +} \ No newline at end of file diff --git a/kirby/branches/multilang/language.php b/kirby/branches/multilang/language.php new file mode 100644 index 0000000..18c6368 --- /dev/null +++ b/kirby/branches/multilang/language.php @@ -0,0 +1,34 @@ +site = $site; + $this->code = $lang['code']; + $this->name = $lang['name']; + $this->locale = $lang['locale']; + $this->default = (isset($lang['default']) and $lang['default']); + $this->direction = (isset($lang['direction']) and $lang['direction'] == 'rtl') ? 'rtl' : 'ltr'; + $this->url = isset($lang['url']) ? $lang['url'] : $lang['code']; + + } + + public function url() { + return url::makeAbsolute($this->url, $this->site->url()); + } + + public function isDefault() { + return $this->default; + } + + public function __toString() { + return $this->code; + } + +} diff --git a/kirby/branches/multilang/languages.php b/kirby/branches/multilang/languages.php new file mode 100644 index 0000000..cb1cd43 --- /dev/null +++ b/kirby/branches/multilang/languages.php @@ -0,0 +1,28 @@ +site = $site; + } + + public function find($code) { + return isset($this->data[$code]) ? $this->data[$code] : null; + } + + public function codes() { + return $this->keys(); + } + + public function findDefault() { + return $this->site->defaultLanguage(); + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/page.php b/kirby/branches/multilang/page.php new file mode 100644 index 0000000..bd25bc5 --- /dev/null +++ b/kirby/branches/multilang/page.php @@ -0,0 +1,281 @@ +intendedTemplate(); + if(is_null($lang)) $lang = $this->site->language->code; + return textfile($this->diruri(), $template, $lang); + } + + /** + * Returns the translated URI + */ + public function uri($lang = null) { + // build the page's uri with the parent uri and the page's slug + return ltrim($this->parent->uri($lang) . '/' . $this->slug($lang), '/'); + } + + /** + * Returns the URL key from the content file + * if available and otherwise returns the page UID + * + * @param string $lang + * @return string + */ + public function urlKey($lang = null) { + + if($content = $this->content($lang)) { + // search for a translated url_key in that language + if($key = (string)a::get((array)$content->data(), 'url_key')) { + // if available, use the translated url key as slug + return $key; + } + } + + return $this->uid(); + + } + + /** + * Returns the slug for the page + * The slug is the last part of the URL path + * For multilang sites this can be translated with a URL-Key field + * in the text file for this page. + * + * @param string $lang Optional language code to get the translated slug + * @return string i.e. 01-projects returns projects + */ + public function slug($lang = null) { + + $default = $this->site->defaultLanguage->code; + $current = $this->site->language->code; + + // get the slug for the current language + if(is_null($lang)) { + + // the current language's slug can be cached + if(isset($this->cache['slug'])) return $this->cache['slug']; + + // if the current language is the default language + // simply return the uid + if($current == $default) { + return $this->cache['slug'] = $this->uid(); + } + + // get the translated url key + return $this->urlKey(); + + } else { + + // if the passed language code is the current language code + // we can simply return the slug method without a language code specified + if($lang == $current) { + return $this->slug(); + } + + // the slug for the default language is just the name of the folder + if($lang == $default) { + return $this->uid(); + } + + // get the translated url key + return $this->urlKey($lang); + + } + + } + + /** + * Returns the full url for the page + * + * @param string $lang Optional language code to get the URL for that specific language on multilang sites + * @return string + */ + public function url() { + + $args = func_get_args(); + $lang = array_shift($args); + + // for multi language sites every url needs + // to be treated specially to make sure each uid is translated properly + // and language codes are prepended if needed + if(is_null($lang)) { + // get the current language + $lang = $this->site->language->code; + } + + // Kirby is trying to remove the home folder name from the url + if($this->isHomePage()) { + return $this->site->url($lang); + } else if($this->parent->isHomePage()) { + return $this->site->url($lang) . '/' . $this->parent->slug($lang) . '/' . $this->slug($lang); + } else { + return $this->parent->url($lang) . '/' . $this->slug($lang); + } + + } + + /** + * Modified inventory fetcher + * + * @return array + */ + public function inventory() { + + $inventory = parent::inventory(); + $defaultLang = $this->site->defaultLanguage->code; + $expression = '!(.*?)(\.(' . implode('|', $this->site->languages->codes()) . ')|)\.' . $this->kirby->options['content.file.extension'] . '$!i'; + + foreach($inventory['meta'] as $key => $meta) { + $inventory['meta'][$key] = array($defaultLang => $meta); + } + + foreach($inventory['content'] as $key => $content) { + + preg_match($expression, $content, $match); + + $file = $match[1]; + $lang = isset($match[3]) ? $match[3] : null; + + if(in_array($file, $inventory['files'])) { + $inventory['meta'][$file][$lang] = $content; + } else { + + if(is_null($lang)) { + $lang = f::extension($file); + if(empty($lang)) $lang = $defaultLang; + } + + $inventory['content'][$lang] = $content; + } + + unset($inventory['content'][$key]); + + } + + // try to fill the default language with something else + if(!isset($inventory['content'][$defaultLang])) { + $inventory['content'][$defaultLang] = a::first($inventory['content']); + } + + return $inventory; + + } + + /** + * Returns the content object for this page + * + * @param string $lang optional language code + * @return Content + */ + public function content($lang = null) { + + // get the content for the current language + if(is_null($lang)) { + + // the current language's content can be cached + if(isset($this->cache['content'])) return $this->cache['content']; + + // get the current content + $content = $this->_content($this->site->language->code); + + // get the fallback content + if($this->site->language->code != $this->site->defaultLanguage->code) { + + // fetch the default language content + $defaultContent = $this->_content($this->site->defaultLanguage->code); + + // replace all missing fields with values from the default content + foreach($defaultContent->data as $key => $field) { + if(empty($content->data[$key]->value)) { + $content->data[$key] = $field; + } + } + + } + + // find and cache the content for this language + return $this->cache['content'] = $content; + + // get the content for another language + } else { + return $this->_content($lang); + } + + } + + /** + * Private method to simplify content fetching + * + * @return Content + */ + protected function _content($lang) { + + // get the inventory + $inventory = $this->inventory(); + + // try to fetch the content for this language + $content = isset($inventory['content'][$lang]) ? $inventory['content'][$lang] : null; + + // try to replace empty content with the default language content + if(empty($content) and isset($inventory['content'][$this->site->defaultLanguage->code])) { + $content = $inventory['content'][$this->site->defaultLanguage->code]; + } + + // find and cache the content for this language + return new Content($this, $this->root() . DS . $content, $lang); + + } + + /** + * Creates a new page object + * + * @param string $uri + * @param string $template + * @param array $data + */ + static public function create($uri, $template, $data = array()) { + return parent::create($uri, $template . '.' . site()->defaultLanguage->code, $data); + } + + /** + * Update the page with a new set of data + * + * @param array $data + */ + public function update($input = array(), $lang = null) { + + $data = a::update($this->content($lang)->toArray(), $input); + + if(!data::write($this->textfile(null, $lang), $data, 'kd')) { + throw new Exception('The page could not be updated'); + } + + $this->kirby->cache()->flush(); + $this->reset(); + $this->touch(); + return true; + + } + + /** + * Returns the name of the content text file / intended template + * So even if there's no such template it will return the intended name. + * + * @return string + */ + public function intendedTemplate() { + if(isset($this->cache['intendedTemplate'])) return $this->cache['intendedTemplate']; + return $this->cache['intendedTemplate'] = $this->content($this->site->defaultLanguage()->code())->exists() ? $this->content()->name() : 'default'; + } + +} \ No newline at end of file diff --git a/kirby/branches/multilang/site.php b/kirby/branches/multilang/site.php new file mode 100644 index 0000000..df465dd --- /dev/null +++ b/kirby/branches/multilang/site.php @@ -0,0 +1,193 @@ +languages = new Languages($this); + + foreach($kirby->options['languages'] as $lang) { + + $language = new Language($this, $lang); + + // store the default language + if($language->default) $this->defaultLanguage = $this->language = $language; + + // add the language to the collection + $this->languages->data[$language->code] = $language; + + } + + } + + /** + * Returns the translated URI + */ + public function uri($lang = null) { + return null; + } + + public function slug($lang = null) { + return null; + } + + /** + * Returns the url of the site + * + * @return string + */ + public function url($lang = false) { + if($lang) { + // return the specific language url + return $this->languages->find($lang)->url(); + } else { + return parent::url(); + } + } + + /** + * Marks the site as a multilanguage site + * + * @return boolean + */ + public function multilang() { + return true; + } + + /** + * Returns the Languages Collection + * + * @return Languages + */ + public function languages() { + return $this->languages; + } + + /** + * Returns the current language + * or any other language by language code + * + * @param string $code + * @return Language + */ + public function language($code = null) { + if(is_null($code)) return $this->language; + return $this->languages()->find($code); + } + + /** + * Returns the default language + * + * @return Language + */ + public function defaultLanguage() { + return $this->defaultLanguage; + } + + /** + * Tries to find the language for the current visitor + * + * @return Language + */ + public function visitorLanguage() { + return $this->languages()->find(visitor::acceptedLanguageCode()); + } + + /** + * Returns the detected language + * + * @return Language + */ + public function detectedLanguage() { + + if($language = $this->visitorLanguage()) { + return $language; + } else { + return $this->defaultLanguage(); + } + + } + + /** + * Returns the language which will be + * remembered for the next visit + * + * @return Language + */ + public function sessionLanguage() { + if($code = s::get('language') and $language = $this->languages()->find($code)) { + return $language; + } else { + return null; + } + } + + public function switchLanguage(Language $language) { + + s::set('language', $language->code()); + + if($this->language()->code() != $language->code()) { + go($this->page()->url($language->code())); + } + + } + + /** + * Sets the currently active page + * and returns its page object + * + * @param string $uri + * @return Page + */ + public function visit($uri = '', $lang = null) { + + // if the language code is missing or the code is invalid (TODO) + if(!in_array($lang, $this->languages()->keys())) { + $lang = $this->defaultLanguage->code; + } + + // set the current language + $this->language = $this->languages()->data[$lang]; + + // clean the uri + $uri = trim($uri, '/'); + + if(empty($uri)) { + return $this->page = $this->homePage(); + } else { + + if($lang == $this->defaultLanguage->code and $page = $this->children()->find($uri)) { + return $this->page = $page; + } else if($page = $this->children()->findByURI($uri)) { + return $this->page = $page; + } else { + return $this->page = $this->errorPage(); + } + } + + } + + /** + * Returns the locale for the site + * + * @return string + */ + public function locale() { + return $this->language->locale; + } + +} diff --git a/kirby/core/asset.php b/kirby/core/asset.php new file mode 100644 index 0000000..a92e457 --- /dev/null +++ b/kirby/core/asset.php @@ -0,0 +1,21 @@ +kirby = kirby::instance(); + if(is_a($path, 'Media')) { + parent::__construct($path->root(), $path->url()); + } else { + parent::__construct( + url::isAbsolute($path) ? null : $this->kirby->roots()->index() . DS . ltrim($path, DS), + url::makeAbsolute($path) + ); + } + } + +} \ No newline at end of file diff --git a/kirby/core/avatar.php b/kirby/core/avatar.php new file mode 100644 index 0000000..e17d7ef --- /dev/null +++ b/kirby/core/avatar.php @@ -0,0 +1,30 @@ +user = $user; + + // this should rather be coming from the user object + $this->kirby = kirby::instance(); + + // try to find the avatar + if($file = f::resolve($this->kirby->roots()->avatars() . DS . $user->username(), ['jpg', 'jpeg', 'gif', 'png'])) { + $filename = f::filename($file); + } else { + $filename = $user->username() . '.jpg'; + $file = $this->kirby->roots()->avatars() . DS . $filename; + } + + parent::__construct($file, $this->kirby->urls()->avatars() . '/' . $filename); + + } + +} \ No newline at end of file diff --git a/kirby/core/children.php b/kirby/core/children.php new file mode 100644 index 0000000..67ae247 --- /dev/null +++ b/kirby/core/children.php @@ -0,0 +1,207 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class ChildrenAbstract extends Pages { + + protected $page = null; + protected $cache = array(); + + /** + * Constructor + */ + public function __construct($page) { + $this->page = $page; + } + + /** + * Creates a new Page object and adds it to the collection + */ + public function add($dirname) { + $page = new Page($this->page, $dirname); + $this->data[$page->id()] = $page; + return $page; + } + + /** + * Creates a new subpage + * + * @param string $uid + * @param string $template + * @param array $data + */ + public function create($uid, $template, $data = array()) { + $page = page::create($this->page->id() . '/' . $uid, $template, $data); + $this->data[$page->id()] = $page; + return $page; + } + + /** + * Returns the parent page + * + * @return Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the Children of Children + * + * @return Children + */ + public function children() { + $grandChildren = new Children($this->page); + foreach($this->data as $page) { + foreach($page->children() as $subpage) { + $grandChildren->data[$subpage->id()] = $subpage; + } + } + return $grandChildren; + } + + /** + * Find a specific page by its uri + * + * @return Page or false + */ + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } else if (count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $collection = new Children($this->page); + foreach($args as $id) { + if($page = $this->find($id)) { + $collection->data[$page->id()] = $page; + } + } + return $collection; + } else { + + // get the first argument and remove slashes + $id = trim($args[0], '/'); + + // build the direct uri + $directId = trim($this->page()->id() . '/' . $id, '/'); + + // fast access to direct uris + if(isset($this->data[$directId])) return $this->data[$directId]; + + $path = explode('/', $id); + $obj = $this; + $page = false; + + foreach($path as $uid) { + + $id = ltrim($obj->page()->id() . '/' . $uid, '/'); + + if(!isset($obj->data[$id])) return false; + + $page = $obj->data[$id]; + $obj = $page->children(); + + } + + return $page; + + } + + } + + /** + * Finds pages by it's unique URI + * + * @param mixed $uri Either a single URI string or an array of URIs + * @param string $use The field, which should be used (uid or slug) + * @return mixed Either a Page object, a Pages object for multiple pages or null if nothing could be found + */ + public function findByURI() { + + $args = func_get_args(); + + if(count($args) == 0) { + return false; + } else if(count($args) > 1) { + $collection = new Children($this->page); + foreach($args as $uri) { + $page = $this->findByURI($uri); + if($page) $collection->data[$page->id()] = $page; + } + return $collection; + } else { + + // get the first argument and remove slashes + $uri = trim($args[0], '/'); + $array = str::split($uri, '/'); + $obj = $this; + $page = false; + + foreach($array as $p) { + + $next = $obj->findBy('slug', $p); + + if(!$next) break; + + $page = $next; + $obj = $next->children(); + + } + + return ($page and $page->slug() != a::last($array)) ? false : $page; + + } + + } + + /** + * Creates a clean one-level collection with all + * pages, subpages, subsubpages, etc. + * + * @param object Pages object for recursive indexing + * @return Children + */ + public function index(Children $obj = null) { + + if(is_null($obj)) { + if(isset($this->cache['index'])) return $this->cache['index']; + $this->cache['index'] = new Children($this->page); + $obj = $this; + } + + foreach($obj->data as $key => $page) { + $this->cache['index']->data[$page->uri()] = $page; + $this->index($page->children()); + } + + return $this->cache['index']; + + } + + /** + * Extended group method + * detaches children and converts them to + * a simple pages collection + * + * @param function $callback + * @return Pages + */ + public function group($callback) { + $collection = new Pages($this); + return $collection->group($callback); + } + +} \ No newline at end of file diff --git a/kirby/core/content.php b/kirby/core/content.php new file mode 100644 index 0000000..0a6bac6 --- /dev/null +++ b/kirby/core/content.php @@ -0,0 +1,151 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class ContentAbstract { + + public $page = null; + public $root = null; + public $raw = null; + public $data = array(); + public $fields = array(); + public $name = null; + + /** + * Constructor + */ + public function __construct($page, $root) { + + $this->page = $page; + $this->root = $root; + $this->name = pathinfo($root, PATHINFO_FILENAME); + + // stop at invalid files + if(empty($this->root) or !is_file($this->root) or !is_readable($this->root)) return; + + // read the content file and remove the BOM + $this->raw = str_replace(BOM, '', file_get_contents($this->root)); + + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $this->raw); + + // loop through all fields and add them to the content + foreach($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(array('-', ' '), '_', strtolower(trim(substr($field, 0, $pos)))); + + // Don't add fields with empty keys + if(empty($key)) continue; + + // add the key to the fields list + $this->fields[] = $key; + + // add the key object + $this->data[$key] = new Field($this->page, $key, trim(substr($field, $pos+1))); + } + + } + + /** + * Returns the root for the content file + */ + public function root() { + return $this->root; + } + + /** + * Returns the name of the content file + * without the extension. This is + * being used to determine the template for the page + * + * @return string + */ + public function name() { + return $this->name; + } + + /** + * Returns an array with all + * field names + * + * @return array3 + */ + public function fields() { + return $this->fields; + } + + /** + * Returns the raw content from the file + * + * @return string + */ + public function raw() { + return $this->raw; + } + + /** + * Returns the entire data array + * with all field objects + * + * @return array + */ + public function data() { + return $this->data; + } + + /** + * Checks if the content file exists + * + * @return boolean + */ + public function exists() { + return file_exists($this->root); + } + + /** + * Gets a field from the content + * + * @return Field + */ + public function get($key, $arguments = null) { + + // case-insensitive data fetching + $key = strtolower($key); + + if(isset($this->data[$key])) { + return $this->data[$key]; + } else { + // return an empty field as default + return new Field($this->page, $key); + } + + } + + /** + * Checks if a field exists + * + * @param string $key + * @return boolean + */ + public function has($key) { + return isset($this->data[strtolower($key)]); + } + + public function __call($method, $arguments = null) { + return $this->get($method, $arguments); + } + + public function toArray() { + return array_map(function($item) { + return $item->value; + }, $this->data); + } + +} \ No newline at end of file diff --git a/kirby/core/field.php b/kirby/core/field.php new file mode 100644 index 0000000..ba8127c --- /dev/null +++ b/kirby/core/field.php @@ -0,0 +1,55 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FieldAbstract { + + static public $methods = array(); + + public $page; + public $key; + public $value; + + public function __construct($page, $key, $value = '') { + $this->page = $page; + $this->key = $key; + $this->value = $value; + } + + public function page() { + return $this->page; + } + public function exists() { + return $this->page->content()->has($this->key); + } + public function key() { + return $this->key; + } + public function value() { + return $this->value; + } + public function isTranslated($lang = null) { + return true; + } + public function __toString() { + return (string)$this->value; + } + public function toString() { + return $this->value; + } + public function __call($method, $arguments = array()) { + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this; + } + } +} diff --git a/kirby/core/file.php b/kirby/core/file.php new file mode 100644 index 0000000..010f386 --- /dev/null +++ b/kirby/core/file.php @@ -0,0 +1,333 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FileAbstract extends Media { + + use Kirby\Traits\Image; + + static public $methods = array(); + + public $kirby; + public $site; + public $page; + public $files; + + /** + * Constructor + * + * @param Files The parent files collection + * @param string The filename + */ + public function __construct(Files $files, $filename) { + + $this->kirby = $files->kirby; + $this->site = $files->site; + $this->page = $files->page; + $this->files = $files; + $this->root = $this->files->page()->root() . DS . $filename; + + parent::__construct($this->root); + + } + + /** + * Returns the kirby object + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the parent site object + * + * @return Site + */ + public function site() { + return $this->site; + } + + /** + * Returns the parent page object + * + * @return Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the parent files collection + * + * @return Files + */ + public function files() { + return $this->files; + } + + /** + * Returns the full root for the content file + * + * @return string + */ + public function textfile() { + return $this->page->textfile($this->filename()); + } + + public function siblings() { + return $this->files->not($this->filename); + } + + public function next() { + $siblings = $this->files; + $index = $siblings->indexOf($this); + if($index === false) return false; + return $this->files->nth($index+1); + } + + public function hasNext() { + return $this->next(); + } + + public function prev() { + $siblings = $this->files; + $index = $siblings->indexOf($this); + if($index === false) return false; + return $this->files->nth($index-1); + } + + public function hasPrev() { + return $this->prev(); + } + + /** + * Returns the absolute URL for the file + * + * @return string + */ + public function url($raw = false) { + if($raw || empty($this->modifications)) { + return $this->page->contentUrl() . '/' . rawurlencode($this->filename); + } else { + return $this->kirby->component('thumb')->url($this); + } + } + + /** + * Returns the relative URI for the image + * + * @return string + */ + public function uri() { + return $this->page->uri() . '/' . rawurlencode($this->filename); + } + + /** + * Returns the full directory path starting from the content folder + * + * @return string + */ + public function diruri() { + return $this->page->diruri() . '/' . rawurlencode($this->filename); + } + + /** + * Get the meta information + * + * @return Content + */ + public function meta() { + + if(isset($this->cache['meta'])) { + return $this->cache['meta']; + } else { + + $inventory = $this->page->inventory(); + $file = isset($inventory['meta'][$this->filename]) ? $this->page->root() . DS . $inventory['meta'][$this->filename] : null; + + return $this->cache['meta'] = new Content($this->page, $file); + + } + + } + + /** + * Custom modified method for files + * + * @param string $format + * @return string + */ + public function modified($format = null, $handler = null) { + return parent::modified($format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * Magic getter for all meta fields + * + * @return Field + */ + public function __call($key, $arguments = null) { + if(isset(static::$methods[$key])) { + if(!$arguments) $arguments = array(); + array_unshift($arguments, clone $this); + return call(static::$methods[$key], $arguments); + } else { + return $this->meta()->get($key, $arguments); + } + } + + /** + * Generates a new filename for a given name + * and makes sure to handle badly given extensions correctly + * + * @param string $name + * @return string + */ + public function createNewFilename($name, $safeName = true) { + + $name = basename($safeName ? f::safeName($name) : $name); + $ext = f::extension($name); + + // remove possible extensions + if(in_array($ext, f::extensions())) { + $name = f::name($name); + } + + return trim($name . '.' . $this->extension(), '.'); + + } + + /** + * Renames the file and also its meta info txt + * + * @param string $filename + * @param boolean $safeName + */ + public function rename($name, $safeName = true) { + + $filename = $this->createNewFilename($name, $safeName); + $root = $this->dir() . DS . $filename; + + if(empty($name)) { + throw new Exception('The filename is missing'); + } + + if($root == $this->root()) return $filename; + + if(file_exists($root)) { + throw new Exception('A file with that name already exists'); + } + + if(!f::move($this->root(), $root)) { + throw new Exception('The file could not be renamed'); + } + + $meta = $this->textfile(); + + if(file_exists($meta)) { + f::move($meta, $this->page->textfile($filename)); + } + + // reset the page cache + $this->page->reset(); + + // reset the basics + $this->root = $root; + $this->filename = $filename; + $this->name = $name; + $this->cache = array(); + + cache::flush(); + + return $filename; + + } + + public function update($data = array()) { + + $data = array_merge((array)$this->meta()->toArray(), $data); + + foreach($data as $k => $v) { + if(is_null($v)) unset($data[$k]); + } + + if(!data::write($this->textfile(), $data, 'kd')) { + throw new Exception('The file data could not be saved'); + } + + // reset the page cache + $this->page->reset(); + + // reset the file cache + $this->cache = array(); + + cache::flush(); + return true; + + } + + public function delete() { + + // delete the meta file + f::remove($this->textfile()); + + if(!f::remove($this->root())) { + throw new Exception('The file could not be deleted'); + } + + cache::flush(); + return true; + + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return mixed + */ + public function date($format = null, $field = 'date') { + if($timestamp = strtotime($this->meta()->$field())) { + if(is_null($format)) { + return $timestamp; + } else { + return $this->kirby->options['date.handler']($format, $timestamp); + } + } else { + return false; + } + } + + /** + * Converts the entire file object into + * a plain PHP array + * + * @param closure $callback Filter callback + * @return array + */ + public function toArray($callback = null) { + + $data = parent::toArray(); + + // add the meta content + $data['meta'] = $this->meta()->toArray(); + + if(is_null($callback)) { + return $data; + } else { + return array_map($callback, $data); + } + + } + +} \ No newline at end of file diff --git a/kirby/core/files.php b/kirby/core/files.php new file mode 100644 index 0000000..ec92d8e --- /dev/null +++ b/kirby/core/files.php @@ -0,0 +1,137 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class FilesAbstract extends Collection { + + static public $methods = array(); + + public $kirby = null; + public $site = null; + public $page = null; + + public function __construct($page) { + $this->kirby = $page->kirby; + $this->site = $page->site; + $this->page = $page; + $inventory = $page->inventory(); + + foreach($inventory['files'] as $filename) { + $file = new File($this, $filename); + $this->data[strtolower($file->filename())] = $file; + } + } + + public function __call($method, $arguments) { + + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this->get($method); + } + + } + + public function kirby() { + return $this->kirby; + } + + public function site() { + return $this->site; + } + + public function page() { + return $this->page; + } + + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } + + if(count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $files = clone $this; + $files->data = array(); + foreach($args as $filename) { + $file = $this->find($filename); + if(!empty($file)) { + $files->data[$filename] = $file; + } + } + return $files; + } else { + $filename = strtolower($args[0]); + return isset($this->data[$filename]) ? $this->data[$filename] : null; + } + + } + + /** + * Returns a new collection of files without the given files + * + * @param args any number of filenames or file objects, passed as individual arguments + * @return object a new collection without the files + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $filename) { + if(is_array($filename) or $filename instanceof Traversable) { + foreach($filename as $f) { + $collection = $collection->not($f); + } + } else if(is_a($filename, 'Media')) { + // unset by Media object + unset($collection->data[strtolower($filename->filename())]); + } else { + unset($collection->data[strtolower($filename)]); + } + } + return $collection; + } + + /** + * Converts the files collection + * into a plain array + * + * @param closure $callback Filter callback for each item + * @return array + */ + public function toArray($callback = null) { + + $data = array(); + + foreach($this as $file) { + $data[] = $file->toArray($callback); + } + + return $data; + + } + + /** + * Converts the files collection + * into a json string + * + * @param closure $callback Filter callback for each item + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + +} \ No newline at end of file diff --git a/kirby/core/kirbytag.php b/kirby/core/kirbytag.php new file mode 100644 index 0000000..8ad00fa --- /dev/null +++ b/kirby/core/kirbytag.php @@ -0,0 +1,173 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class KirbytagAbstract { + + protected $page; + protected $kirbytext; + protected $name; + protected $html; + protected $attr = array(); + + public function __construct($kirbytext, $name, $tag) { + + if(is_null($kirbytext)) $kirbytext = new Kirbytext(''); + + $this->page = $kirbytext->field->page; + $this->kirbytext = $kirbytext; + $this->name = $name; + $this->html = kirbytext::$tags[$name]['html']; + + // get a list with all attributes + $attributes = isset(kirbytext::$tags[$name]['attr']) ? (array)kirbytext::$tags[$name]['attr'] : array(); + + // add the name as first attribute + array_unshift($attributes, $name); + + if(is_array($tag)) { + foreach($attributes as $key) { + if(isset($tag[$key])) $this->attr[$key] = $tag[$key]; + } + } else { + + // extract all attributes + $search = preg_split('!(' . implode('|', $attributes) . '):!i', $tag, false, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + $num = 0; + + foreach($search AS $key) { + + if(!isset($search[$num+1])) break; + + $key = trim($search[$num]); + $value = trim($search[$num+1]); + + $this->attr[$key] = $value; + $num = $num+2; + + } + + } + + } + + /** + * Returns the parent active page + * + * @return object Page + */ + public function page() { + return $this->page; + } + + /** + * Returns the parent kirbytext object + * + * @return object Kirbytext + */ + public function kirbytext() { + return $this->kirbytext; + } + + /** + * Returns the field object + * + * @return object Field + */ + public function field() { + return $this->kirbytext->field(); + } + + /** + * Tries to find all related files for the current page + * + * @return object Files + */ + public function files() { + return $this->page->files(); + } + + /** + * Tries to find a file for the given url/uri + * + * @param string $url a full path to a file or just a filename for files form the current active page + * @return object File + */ + public function file($url) { + + // if this is an absolute url cancel + if(preg_match('!(http|https)\:\/\/!i', $url)) return false; + + // skip urls without extensions + if(!preg_match('!\.[a-z0-9]+$!i',$url)) return false; + + // relative url + if(str::contains($url, '/')) { + + $path = dirname($url); + $filename = basename($url); + + if($page = page($path) and $file = $page->file($filename)) { + return $file; + } else { + return false; + } + + } + + // try to get all files for the current page + $files = $this->files(); + + // cancel if no files are available + if(!$files) return false; + + // try to find the file + return $files->find($url); + + } + + /** + * Returns a specific attribute by key or all attributes + * by passing no key at all. + * + * @param mixed $key + * @param mixed $default + * @return array + */ + public function attr($key = null, $default = null) { + if(is_null($key)) return $this->attr; + return isset($this->attr[$key]) ? $this->attr[$key] : $default; + } + + /** + * Smart getter for the applicable target attribute. + * This will watch for popup or target attributes and return + * a proper target value if available. + * + * @return string + */ + public function target() { + if(empty($this->attr['popup']) and empty($this->attr['target'])) return false; + return empty($this->attr['popup']) ? $this->attr['target'] : '_blank'; + } + + public function html() { + if(!is_callable($this->html)) { + return (string)$this->html; + } else { + return call_user_func_array($this->html, array($this)); + } + } + + public function __toString() { + return (string)$this->html(); + } + +} \ No newline at end of file diff --git a/kirby/core/kirbytext.php b/kirby/core/kirbytext.php new file mode 100644 index 0000000..b46d9b9 --- /dev/null +++ b/kirby/core/kirbytext.php @@ -0,0 +1,107 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class KirbytextAbstract { + + static public $tags = array(); + static public $pre = array(); + static public $post = array(); + + public $field; + + public function __construct($field) { + + if(is_a($field, 'Field')) { + $this->field = $field; + } else if(is_array($field)) { + throw new Exception('Kirbytext cannot handle arrays'); + } else if(empty($field) or is_string($field)) { + $this->field = new Field(page(), null, $field); + } + + } + + public function field() { + return $this->field; + } + + public function parse() { + + if(!$this->field) return ''; + + $text = $this->field->value; + + // pre filters + foreach(static::$pre as $filter) { + $text = call_user_func_array($filter, array($this, $text)); + } + + // tagsify + $text = preg_replace_callback('!(?=[^\]])\([a-z0-9_-]+:.*?\)!is', array($this, 'tag'), $text); + + // markdownify + $text = kirby::instance()->component('markdown')->parse($text); + + // smartypantsify + $text = kirby::instance()->component('smartypants')->parse($text); + + // post filters + foreach(static::$post as $filter) { + $text = call_user_func_array($filter, array($this, $text)); + } + + return $text; + + } + + public function tag($input) { + + // remove the brackets + $tag = trim(rtrim(ltrim($input[0], '('), ')')); + $name = trim(substr($tag, 0, strpos($tag, ':'))); + + // if the tag is not installed return the entire string + if(!isset(static::$tags[$name])) return $input[0]; + + try { + $tag = new Kirbytag($this, $name, $tag); + return $tag->html(); + } catch(Exception $e) { + // broken tags will be ignored + return $input[0]; + } + + } + + static public function install($root) { + + if(!is_dir($root)) return false; + + foreach(scandir($root) as $file) { + if(pathinfo($file, PATHINFO_EXTENSION) == 'php') { + $name = pathinfo($file, PATHINFO_FILENAME); + $tag = include($root . DS . $file); + if(is_array($tag)) Kirbytext::$tags[$name] = $tag; + } + } + + } + + public function __toString() { + try { + return $this->parse(); + } catch(Exception $e) { + // on massive render bugs the entire text will be returned + return $this->field->value; + } + } + +} \ No newline at end of file diff --git a/kirby/core/page.php b/kirby/core/page.php new file mode 100644 index 0000000..fff6cca --- /dev/null +++ b/kirby/core/page.php @@ -0,0 +1,1443 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class PageAbstract { + + static public $models = array(); + static public $methods = array(); + + public $kirby; + public $site; + public $parent; + + protected $id; + protected $dirname; + protected $root; + protected $depth; + protected $uid; + protected $num; + protected $uri; + protected $cache = array(); + + /** + * Constructor + * + * @param Page $parent + * @param string $dirname + */ + public function __construct($parent, $dirname) { + + $this->kirby = $parent->kirby; + $this->site = $parent->site; + $this->parent = $parent; + $this->dirname = $dirname; + $this->root = $parent->root() . DS . $dirname; + $this->depth = $parent->depth() + 1; + + // extract the uid and num of the directory + if(preg_match('/^([0-9]+)[\-](.*)$/', $this->dirname, $match)) { + $this->uid = $match[2]; + $this->num = $match[1]; + } else { + $this->num = null; + $this->uid = $this->dirname; + } + + // assign the uid + $this->id = $this->uri = ltrim($parent->id() . '/' . $this->uid, '/'); + + } + + /** + * Cleans the temporary page cache and + * the cache of all parent pages + */ + public function reset() { + $this->cache = array(); + $this->parent()->reset(); + } + + /** + * Mark the page as modified + */ + public function touch() { + return touch($this->root()); + } + + /** + * Returns the kirby object + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the site object + * + * @return Site + */ + public function site() { + return $this->site; + } + + /** + * Returns the parent page element + * + * @return Page + */ + public function parent() { + return $this->parent; + } + + /** + * Returns all parents + * + * @return Children + */ + public function parents() { + + if(isset($this->cache['parents'])) return $this->cache['parents']; + + $children = new Children($this->site); + $parents = array(); + $next = $this->parent(); + + while($next and $next->depth() > 0) { + $children->data[$next->id()] = $next; + $next = $next->parent(); + } + + return $this->cache['parents'] = $children; + + } + + /** + * Returns the full root of the page folder + * + * @return string + */ + public function root() { + return $this->root; + } + + /** + * Returns the name of the directory + * + * @return string + */ + public function dirname() { + return $this->dirname; + } + + /** + * Returns the relative URL for the directory. + * Relative to the base content directory + * + * @return string + */ + public function diruri() { + if(isset($this->cache['diruri'])) return $this->cache['diruri']; + return $this->cache['diruri'] = ltrim($this->parent()->diruri() . '/' . $this->dirname(), '/'); + } + + /** + * Returns the full url for the page + * + * @return string + */ + public function url() { + + if(isset($this->cache['url'])) return $this->cache['url']; + + // Kirby is trying to remove the home folder name from the url + if($this->isHomePage()) { + // return the base url + return $this->cache['url'] = $this->site->url(); + } else if($this->parent->isHomePage()) { + return $this->cache['url'] = $this->site->url() . '/' . $this->parent->uid . '/' . $this->uid; + } else { + $purl = $this->parent->url(); + return $this->cache['url'] = $purl == '/' ? '/' . $this->uid : $this->parent->url() . '/' . $this->uid; + } + + } + + /** + * Returns the full URL for the content folder + * + * @return string + */ + public function contentUrl() { + return $this->kirby()->urls()->content() . '/' . $this->diruri(); + } + + /** + * Builds and returns the short url for the current page + * + * @return string + */ + public function tinyurl() { + if(!$this->kirby->options['tinyurl.enabled']) { + return $this->url(); + } else { + return url($this->kirby->options['tinyurl.folder'] . '/' . $this->hash()); + } + } + + /** + * Returns a number indicating how deep the page + * is nested within the content folder + * + * @return int + */ + public function depth() { + return $this->depth; + } + + /** + * Returns the uri for the page + * which is being used for the url later + * + * @return string + */ + public function uri() { + return $this->uri; + } + + /** + * Returns the id, which is going to be used for + * Collection keys and things like that + * + * @return string + */ + public function id() { + return $this->id; + } + + /** + * Checks if the page can be cached + * + * @return boolean + */ + public function isCachable() { + + // The error page should not be cached + if($this->isErrorPage()) { + return false; + } + + foreach($this->kirby->option('cache.ignore') as $pattern) { + if(fnmatch($pattern, $this->uri()) === true) { + return false; + } + } + + return true; + + } + + /** + * Returns the page uid, which is the + * folder name without the sorting number + * + * @return string + */ + public function uid() { + return $this->uid; + } + + /** + * Alternative for $this->uid() + * + * @return string + */ + public function slug() { + return $this->uid; + } + + /** + * Returns the sorting number if it exists + * + * @return string + */ + public function num() { + return $this->num; + } + + /** + * Reads the directory and returns an inventory array + * + * @return array + */ + public function inventory() { + + if(isset($this->cache['inventory'])) return $this->cache['inventory']; + + // get all items within the directory + $ignore = array('.', '..', '.DS_Store', '.git', '.svn', 'Thumb.db'); + $items = array_diff(scandir($this->root), array_merge($ignore, (array)$this->kirby->option('content.file.ignore'))); + + // create the inventory + $this->cache['inventory'] = array( + 'children' => array(), + 'content' => array(), + 'meta' => array(), + 'thumbs' => array(), + 'files' => array(), + ); + + // normalize the filename if possible + if($this->kirby->option('content.file.normalize') && class_exists('Normalizer')) { + $items = array_map('Normalizer::normalize', $items); + } + + foreach($items as $item) { + + // skip any invisible files and folders + if(substr($item, 0, 1) === '.') continue; + + $root = $this->root . DS . $item; + + if(is_dir($root)) { + $this->cache['inventory']['children'][] = $item; + } else if(pathinfo($item, PATHINFO_EXTENSION) == $this->kirby->options['content.file.extension']) { + $this->cache['inventory']['content'][] = $item; + } else if(strpos($item, '.thumb.') !== false and preg_match('!\.thumb\.(jpg|jpeg|png|gif)$!i', $item)) { + // get the filename of the original image and use it as the array key + $image = str_replace('.thumb', '', $item); + // this makes it easier to find the corresponding image later + $this->cache['inventory']['thumbs'][$image] = $item; + } else { + $this->cache['inventory']['files'][] = $item; + } + + } + + foreach($this->cache['inventory']['thumbs'] as $key => $thumb) { + // remove invalid thumbs by looking for a matching image file and + if(!in_array($key, $this->cache['inventory']['files'])) { + $this->cache['inventory']['files'][] = $thumb; + unset($this->cache['inventory']['thumbs'][$key]); + } + } + + foreach($this->cache['inventory']['content'] as $key => $content) { + $file = pathinfo($content, PATHINFO_FILENAME); + if(in_array($file, $this->cache['inventory']['files'])) { + $this->cache['inventory']['meta'][$file] = $content; + unset($this->cache['inventory']['content'][$key]); + } + } + + // sort the children + natsort($this->cache['inventory']['children']); + + return $this->cache['inventory']; + + } + + /** + * Returns all children for this page + * + * @return Children + */ + public function children() { + + if(isset($this->cache['children'])) return $this->cache['children']; + + $this->cache['children'] = new Children($this); + + $inventory = $this->inventory(); + + // with page models + if(!empty(static::$models)) { + foreach($inventory['children'] as $dirname) { + $child = new Page($this, $dirname); + // let's create a model if one is defined + if(isset(static::$models[$child->intendedTemplate()])) { + $model = static::$models[$child->intendedTemplate()]; + $child = new $model($this, $dirname); + } + $this->cache['children']->data[$child->id()] = $child; + } + // without page models + } else { + foreach($inventory['children'] as $dirname) { + $child = new Page($this, $dirname); + $this->cache['children']->data[$child->id()] = $child; + } + } + + return $this->cache['children']; + + } + + /** + * Checks if the page has children + * + * @return boolean + */ + public function hasChildren() { + return $this->children()->count(); + } + + /** + * Checks if the page has visible children + * + * @return boolean + */ + public function hasVisibleChildren() { + return $this->children()->visible()->count(); + } + + /** + * Checks if the page has invisible children + * + * @return boolean + */ + public function hasInvisibleChildren() { + return $this->children()->invisible()->count(); + } + + /** + * Returns the grand children of this page + * + * @return Children + */ + public function grandChildren() { + return $this->children()->children(); + } + + /** + * Returns the siblings for this page, not including this page + * + * @param boolean $self + * @return Children + */ + public function siblings($self = true) { + return $self ? $this->parent->children() : $this->parent->children()->not($this); + } + + /** + * Internal method to return the next page + * + * @param object $siblings Children A collection of siblings to search in + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + protected function _next(Children $siblings, $sort = array(), $visibility = false) { + + if($sort) $siblings = call(array($siblings, 'sortBy'), $sort); + $index = $siblings->indexOf($this); + if($index === false) return null; + if($visibility) { + $siblings = $siblings->offset($index+1); + $siblings = $siblings->{$visibility}(); + return $siblings->first(); + } else { + return $siblings->nth($index + 1); + } + } + + /** + * Internal method to return the previous page + * + * @param object $siblings Children A collection of siblings to search in + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + protected function _prev(Children $siblings, $sort = array(), $visibility = false) { + if($sort) $siblings = call(array($siblings, 'sortBy'), $sort); + $index = $siblings->indexOf($this); + if($index === false or $index === 0) return null; + if($visibility) { + $siblings = $siblings->limit($index); + $siblings = $siblings->{$visibility}(); + return $siblings->last(); + } else { + return $siblings->nth($index - 1); + } + } + + /** + * Returns the next page element + * + * @return Page + */ + public function next() { + return $this->_next($this->parent->children(), func_get_args()); + } + + /** + * Checks if there's a next page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNext() { + return call(array($this, 'next'), func_get_args()) != null; + } + + /** + * Returns the next visible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function nextVisible() { + if(!$this->parent) { + return null; + } else { + return $this->_next($this->parent->children(), func_get_args(), 'visible'); + } + } + + /** + * Checks if there's a next visible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNextVisible() { + return call(array($this, 'nextVisible'), func_get_args()) != null; + } + + /** + * Returns the next invisible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function nextInvisible() { + if(!$this->parent) { + return null; + } else { + return $this->_next($this->parent->children(), func_get_args(), 'invisible'); + } + } + + /** + * Checks if there's a next invisible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasNextInvisible() { + return call(array($this, 'nextInvisible'), func_get_args()) != null; + } + + /** + * Returns the previous page element + * + * @return Page + */ + public function prev() { + return $this->_prev($this->parent->children(), func_get_args()); + } + + /** + * Checks if there's a previous page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrev() { + return call(array($this, 'prev'), func_get_args()) != null; + } + + /** + * Returns the previous visible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function prevVisible() { + if(!$this->parent) { + return null; + } else { + return $this->_prev($this->parent->children(), func_get_args(), 'visible'); + } + } + + /** + * Checks if there's a previous visible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrevVisible() { + return call(array($this, 'prevVisible'), func_get_args()) != null; + } + + /** + * Returns the previous invisible page in the current collection if available + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return mixed Page or null + */ + public function prevInvisible() { + if(!$this->parent) { + return null; + } else { + return $this->_prev($this->parent->children(), func_get_args(), 'invisible'); + } + } + + /** + * Checks if there's a previous invisible page + * + * @param string $sort An optional sort field for the siblings + * @param string $direction An optional sort direction + * @return boolean + */ + public function hasPrevInvisible() { + return call(array($this, 'prevInvisible'), func_get_args()) != null; + } + + /** + * Find any child or a set of children of this page + * + * @return Page | Children + */ + public function find() { + return call_user_func_array(array($this->children(), 'find'), func_get_args()); + } + + /** + * Find any file or a set of files for this page + * + * @return File | Files + */ + public function file() { + $args = func_get_args(); + if(empty($args)) return $this->files()->first(); + return call_user_func_array(array($this->files(), 'find'), $args); + } + + // file stuff + + /** + * Returns all files for this page + * + * @return Files + */ + public function files() { + if(isset($this->cache['files'])) return $this->cache['files']; + return $this->cache['files'] = new Files($this); + } + + /** + * Checks if this page has attached files + * + * @return boolean + */ + public function hasFiles() { + return $this->files()->count(); + } + + // File filters + public function images() { return $this->files()->filterBy('type', 'image'); } + public function videos() { return $this->files()->filterBy('type', 'video'); } + public function documents() { return $this->files()->filterBy('type', 'document'); } + public function audio() { return $this->files()->filterBy('type', 'audio'); } + public function code() { return $this->files()->filterBy('type', 'code'); } + public function archives() { return $this->files()->filterBy('type', 'archive'); } + + // File checkers + public function hasImages() { return $this->images()->count(); } + public function hasVideos() { return $this->videos()->count(); } + public function hasDocuments() { return $this->documents()->count(); } + public function hasAudio() { return $this->audio()->count(); } + public function hasCode() { return $this->code()->count(); } + public function hasArchives() { return $this->archives()->count(); } + + /** + * Returns a single image + * + * @return File + */ + public function image($filename = null) { + if(is_null($filename)) return $this->images()->first(); + return $this->images()->find($filename); + } + + /** + * Returns a single video + * + * @return File + */ + public function video($filename = null) { + if(is_null($filename)) return $this->videos()->first(); + return $this->videos()->find($filename); + } + + /** + * Returns a single document + * + * @return File + */ + public function document($filename = null) { + if(is_null($filename)) return $this->documents()->first(); + return $this->documents()->find($filename); + } + + + /** + * Returns the content object for this page + * + * @return Content + */ + public function content() { + + if(isset($this->cache['content'])) { + return $this->cache['content']; + } else { + $inventory = $this->inventory(); + return $this->cache['content'] = new Content($this, $this->root() . DS . array_shift($inventory['content'])); + } + + } + + /** + * Returns the title for this page and + * falls back to the uid if no title exists + * + * @return Field + */ + public function title() { + $title = $this->content()->get('title'); + if($title != '') { + return $title; + } else { + $title->value = $this->uid(); + return $title; + } + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return mixed + */ + public function date($format = null, $field = 'date') { + + if($timestamp = strtotime($this->content()->$field())) { + if(is_null($format)) { + return $timestamp; + } else { + return $this->kirby->options['date.handler']($format, $timestamp); + } + } else { + return false; + } + + } + + /** + * Returns a unique hashed version of the uri, + * which is used for the tinyurl for example + * + * @return string + */ + public function hash() { + if(isset($this->cache['hash'])) return $this->cache['hash']; + + // add a unique hash + $checksum = sprintf('%u', crc32($this->uri())); + return $this->cache['hash'] = base_convert($checksum, 10, 36); + } + + /** + * Magic getter for all content fields + * + * @return Field + */ + public function __call($key, $arguments = null) { + if(isset($this->$key)) { + return $this->$key; + } else if(isset(static::$methods[$key])) { + if(!$arguments) $arguments = array(); + array_unshift($arguments, clone $this); + return call(static::$methods[$key], $arguments); + } else { + return $this->content()->get($key, $arguments); + } + } + + /** + * Alternative for $this->equals() + */ + public function is(Page $page) { + return $this->id() == $page->id(); + } + + /** + * Alternative for $this->is() + */ + public function equals(Page $page) { + return $this->is($page); + } + + /** + * Checks if this page object is the main site + * + * @return boolean + */ + public function isSite() { + return false; + } + + /** + * Checks if this is the active page + * + * @return boolean + */ + public function isActive() { + return $this->site->page()->is($this); + } + + /** + * Checks if the page is open + * + * @return boolean + */ + public function isOpen() { + if($this->isActive()) return true; + return $this->site->page()->parents()->has($this); + } + + /** + * Checks if the page is visible + * + * @return boolean + */ + public function isVisible() { + return !is_null($this->num); + } + + /** + * Checks if the page is invisible + * + * @return boolean + */ + public function isInvisible() { + return !$this->isVisible(); + } + + /** + * Checks if this page is the home page + * You can define the uri of the homepage in your config + * file with the home option. By default it's assumed + * that the homepage folder has the name "home" + * + * @return boolean + */ + public function isHomePage() { + return $this->uri === $this->kirby->options['home']; + } + + /** + * Checks if this page is the error page + * You can define the uri of the error page in your config + * file with the error option. By default it's assumed + * that the error page folder has the name "error" + * + * @return boolean + */ + public function isErrorPage() { + return $this->uri === $this->kirby->options['error']; + } + + /** + * Checks if the page is a child of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isChildOf(Page $page) { + return $this->is($page) ? false : $this->parent->is($page); + } + + /** + * Checks if the page is the parent of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isParentOf(Page $page) { + return $this->is($page) ? false : $page->parent->is($this); + } + + /** + * Checks if the page is a descendant of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isDescendantOf(Page $page) { + return $this->is($page) ? false : $this->parents()->has($page); + } + + /** + * Checks if the page is a descendant of the currently active page + * + * @return boolean + */ + public function isDescendantOfActive() { + return $this->isDescendantOf($this->site->page()); + } + + /** + * Checks if the page is an ancestor of the given page + * + * @param object Page the page object to check + * @return boolean + */ + public function isAncestorOf($page) { + return $page->isDescendantOf($this); + } + + /** + * Checks if the page or any of its files are writable + * + * @return boolean + */ + public function isWritable() { + + $folder = new Folder($this->root()); + + if(!$folder->isWritable()) return false; + + foreach($folder->files() as $f) { + if(!$f->isWritable()) return false; + } + + return true; + + } + + /** + * Returns the timestamp when the page + * has been modified + * + * @return int + */ + public function modified($format = null, $handler = null) { + return f::modified($this->root, $format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * Returns the index starting from this page + * + * @return Children + */ + public function index() { + return $this->children()->index(); + } + + /** + * Search in subpages and all descendants of this page + * + * @param string $query + * @param array $params + * @return Children + */ + public function search($query, $params = array()) { + return $this->children()->index()->search($query, $params); + } + + // template stuff + + /** + * Returns the name of the used template + * The name of the template is defined by the name + * of the content text file. + * + * i.e. text file: project.txt / template name: project + * + * This method returns the name of the default template + * if there's no template with such a name + * + * @return string + */ + public function template() { + + // check for a cached template name + if(isset($this->cache['template'])) return $this->cache['template']; + + // get the template name + $templateName = $this->intendedTemplate(); + + if($this->kirby->registry->get('template', $templateName)) { + return $this->cache['template'] = $templateName; + } else { + return $this->cache['template'] = 'default'; + } + + } + + /** + * Returns the full path to the used template file + * + * @return string + */ + public function templateFile() { + if($template = $this->kirby->registry->get('template', $this->intendedTemplate())) { + return $template; + } else { + return $this->kirby->registry->get('template', 'default'); + } + } + + /** + * Additional data, which will be passed to the template + * + * @return array + */ + public function templateData() { + return array(); + } + + /** + * Returns the name of the content text file / intended template + * So even if there's no such template it will return the intended name. + * + * @return string + */ + public function intendedTemplate() { + if(isset($this->cache['intendedTemplate'])) return $this->cache['intendedTemplate']; + return $this->cache['intendedTemplate'] = $this->content()->exists() ? $this->content()->name() : 'default'; + } + + /** + * Returns the full path to the intended template file + * This template file may not exist. + * + * @return string + */ + public function intendedTemplateFile() { + return $this->kirby->component('template')->file($this->intendedTemplate()); + } + + /** + * Checks if there's a dedicated template for this page + * Will return false when the default template is used + * + * @return boolean + */ + public function hasTemplate() { + return $this->intendedTemplate() == $this->template(); + } + + /** + * Sends all appropriate headers for this page + * Can be configured with the headers config array, + * which should contain all header definitions for each template + */ + public function headers() { + + $template = $this->template(); + if(isset($this->kirby->options['headers'][$template])) { + $headers = $this->kirby->options['headers'][$template]; + + if(is_numeric($headers)) { + header::status($headers); + } else if(is_callable($headers)) { + call($headers, $this); + } + + } else if($this->isErrorPage()) { + header::notfound(); + } + + } + + /** + * Returns the root for the content file + * + * @return string + */ + public function textfile($template = null) { + if(is_null($template)) $template = $this->intendedTemplate(); + return textfile($this->diruri(), $template); + } + + /** + * Private method to create a page directory + */ + static protected function createDirectory($uri) { + + $uid = str::slug(basename($uri)); + $parentURI = dirname($uri); + $parent = ($parentURI == '.' or empty($parentURI) or $parentURI == DS) ? site() : page($parentURI); + + if(!$parent) { + throw new Exception('The parent does not exist'); + } + + // check for an entered sorting number + if(preg_match('!^(\d+)\-(.*)!', $uid, $matches)) { + $num = $matches[1]; + $uid = $matches[2]; + $dir = $num . '-' . $uid; + } else { + $num = false; + $dir = $uid; + } + + // make sure to check a fresh page + $parent->reset(); + + if($parent->children()->findBy('uid', $uid)) { + throw new Exception('The page UID exists'); + } + + if(!dir::make($parent->root() . DS . $dir)) { + throw new Exception('The directory could not be created'); + } + + // make sure the new directory is available everywhere + $parent->reset(); + + return $parent->id() . '/' . $uid; + + } + + /** + * Creates a new page object + * + * @param string $uri + * @param string $template + * @param array $data + */ + static public function create($uri, $template, $data = array()) { + + if(!is_string($template) or empty($template)) { + throw new Exception('Please pass a valid template name as second argument'); + } + + // try to create the new directory + $uri = static::createDirectory($uri); + + // create the path for the textfile + $file = textfile($uri, $template); + + // try to store the data in the text file + if(!data::write($file, $data, 'kd')) { + throw new Exception('The page file could not be created'); + } + + // get the new page object + $page = page($uri); + + if(!is_a($page, 'Page')) { + throw new Exception('The new page object could not be found'); + } + + // let's create a model if one is defined + if(isset(static::$models[$template])) { + $model = static::$models[$template]; + $page = new $model($page->parent(), $page->dirname()); + } + + kirby::instance()->cache()->flush(); + + return $page; + + } + + /** + * Update the page with a new set of data + * + * @param array + */ + public function update($input = array()) { + + $data = a::update($this->content()->toArray(), $input); + + if(!data::write($this->textfile(), $data, 'kd')) { + throw new Exception('The page could not be updated'); + } + + $this->kirby->cache()->flush(); + $this->reset(); + $this->touch(); + return true; + + } + + /** + * Increment a field value by one or a given value + * + * @param string $field + * @param int $by + * @param int $max + * @return Page + */ + public function increment($field, $by = 1, $max = null) { + $this->update(array( + $field => function($value) use($by, $max) { + $new = (int)$value + $by; + return ($max and $new >= $max) ? $max : $new; + } + )); + return $this; + } + + /** + * Decrement a field value by one or a given value + * + * @param string $field + * @param int $by + * @param int $min + * @return Page + */ + public function decrement($field, $by = 1, $min = 0) { + $this->update(array( + $field => function($value) use($by, $min) { + $new = (int)$value - $by; + return $new <= $min ? $min : $new; + } + )); + return $this; + } + + /** + * Changes the uid for the page + * + * @param string $uid + */ + public function move($uid) { + + $uid = str::slug($uid); + + if(empty($uid)) { + throw new Exception('The uid is missing'); + } + + // don't do anything if the uid exists + if($this->uid() === $uid) return true; + + // check for an existing page with the same UID + if($this->siblings()->not($this)->find($uid)) { + throw new Exception('A page with this uid already exists'); + } + + $dir = $this->isVisible() ? $this->num() . '-' . $uid : $uid; + $root = dirname($this->root()) . DS . $dir; + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $dir; + $this->root = $root; + $this->uid = $uid; + + // assign a new id and uri + $this->id = $this->uri = ltrim($this->parent->id() . '/' . $this->uid, '/'); + + // clean the cache + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + /** + * Return the prepended number for the page + * or changes it to the number passed as parameter + */ + public function sort($num = null) { + + if(!$num and $num !== 0) return $this->num(); + if($num === $this->num()) return true; + + $dir = $num . '-' . $this->uid(); + $root = dirname($this->root()) . DS . $dir; + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $dir; + $this->num = $num; + $this->root = $root; + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + /** + * Make the page invisible by removing the prepended number + */ + public function hide() { + + if($this->isInvisible()) return true; + + $root = dirname($this->root()) . DS . $this->uid(); + + if(!dir::move($this->root(), $root)) { + throw new Exception('The directory could not be moved'); + } + + $this->dirname = $this->uid(); + $this->num = null; + $this->root = $root; + $this->kirby->cache()->flush(); + $this->reset(); + return true; + + } + + public function isDeletable() { + + if($this->isSite()) return false; + if($this->isHomePage()) return false; + if($this->isErrorPage()) return false; + + return true; + + } + + /** + * Deletes the page + * + * @param boolean $force Forces the page to be deleted even if there are subpages + */ + public function delete($force = false) { + + if(!$this->isDeletable()) { + throw new Exception('The page cannot be deleted'); + } + + if($force === false and $this->children()->count()) { + throw new Exception('This page has subpages'); + } + + $parent = $this->parent(); + + if(!dir::remove($this->root())) { + throw new Exception('The page could not be deleted'); + } + + $this->kirby->cache()->flush(); + $parent->reset(); + return true; + + } + + /** + * Converts the entire page object into + * a plain PHP array + * + * @param closure $callback Filter callback + * @return array + */ + public function toArray($callback = null) { + + $data = array( + 'id' => $this->id(), + 'title' => $this->title()->toString(), + 'parent' => $this->parent()->uri(), + 'dirname' => $this->dirname(), + 'diruri' => $this->diruri(), + 'url' => $this->url(), + 'contentUrl' => $this->contentUrl(), + 'tinyUrl' => $this->tinyUrl(), + 'depth' => $this->depth(), + 'uri' => $this->uri(), + 'root' => $this->root(), + 'uid' => $this->uid(), + 'slug' => $this->slug(), + 'num' => $this->num(), + 'hash' => $this->hash(), + 'modified' => $this->modified(), + 'template' => $this->template(), + 'intendedTemplate' => $this->intendedTemplate(), + 'content' => $this->content()->toArray(), + ); + + if(is_null($callback)) { + return $data; + } else if(is_callable($callback)) { + return $callback($this); + } + + } + + /** + * Tries to find a controller for + * the current page and loads the data + * + * @return array + */ + public function controller($arguments = array()) { + + $controller = $this->kirby->registry->get('controller', $this->template()); + + if(is_a($controller, 'Closure')) { + return (array)call_user_func_array($controller, array( + $this->site, + $this->site->children(), + $this, + $arguments + )); + } + + return array(); + + } + + /** + * Converts the entire page array into + * a json string + * + * @param closure $callback Filter callback + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return (string)$this->id(); + } + +} diff --git a/kirby/core/pages.php b/kirby/core/pages.php new file mode 100644 index 0000000..2095097 --- /dev/null +++ b/kirby/core/pages.php @@ -0,0 +1,330 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class PagesAbstract extends Collection { + + static public $methods = array(); + + /** + * Constructor + */ + public function __construct($data = array()) { + foreach($data as $object) { + $this->add($object); + } + } + + public function __call($method, $arguments) { + + if(isset(static::$methods[$method])) { + array_unshift($arguments, clone $this); + return call(static::$methods[$method], $arguments); + } else { + return $this->get($method); + } + + } + + /** + * Adds a single page object to the + * collection by id or the entire object + * + * @param mixed $page + */ + public function add($page) { + + if(is_a($page, 'Collection')) { + foreach($page as $object) $this->add($object); + } else if(is_string($page) and $object = page($page)) { + $this->data[$object->id()] = $object; + } else if(is_a($page, 'Page')) { + $this->data[$page->id()] = $page; + } + + return $this; + + } + + /** + * Returns a new collection of pages without the given pages + * + * @param args any number of uris or page elements, passed as individual arguments + * @return object a new collection without the pages + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $uri) { + if(is_array($uri) or $uri instanceof Traversable) { + foreach($uri as $u) { + $collection = $collection->not($u); + } + } else if(is_a($uri, 'Page')) { + // unset by Page object + unset($collection->data[$uri->id()]); + } else if(isset($collection->data[$uri])) { + // unset by URI + unset($collection->data[$uri]); + } else if($page = $collection->findBy('uid', $uri)) { + // unset by UID + unset($collection->data[$page->id()]); + } + } + return $collection; + } + + public function find() { + + $args = func_get_args(); + + if(!count($args)) { + return false; + } + + if(count($args) === 1 and is_array($args[0])) { + $args = $args[0]; + } + + if(count($args) > 1) { + $pages = new static(); + foreach($args as $id) { + if($page = $this->find($id)) { + $pages->data[$page->id()] = $page; + } + } + return $pages; + } else { + + // get the first argument and remove slashes + $id = trim($args[0], '/'); + + // fast access to direct uris + return isset($this->data[$id]) ? $this->data[$id] : null; + + } + + } + + /** + * Find a single page by a given value + * + * @param string $field + * @param string $value + * @return Page + */ + public function findBy($field, $value) { + foreach($this->data as $page) { + if($page->$field() == $value) return $page; + } + return false; + } + + /** + * Find the open page in a set + * + * @return Page + */ + public function findOpen() { + return $this->findBy('isOpen', true); + } + + /** + * Filters the collection by visible pages + * + * @return Children + */ + public function visible() { + $collection = clone $this; + return $collection->filterBy('isVisible', true); + } + + /** + * Filters the collection by invisible pages + * + * @return Children + */ + public function invisible() { + $collection = clone $this; + return $collection->filterBy('isInvisible', true); + } + + /** + * Checks if a page is in a set of children + * + * @param Page | string $page + * @return boolean + */ + public function has($page) { + $uri = is_string($page) ? $page : $page->id(); + return parent::has($uri); + } + + /** + * Native search method to search for anything within the collection + */ + public function search($query, $params = array()) { + + if(is_string($params)) { + $params = array('fields' => str::split($params, '|')); + } + + $defaults = array( + 'minlength' => 2, + 'fields' => array(), + 'words' => false, + 'score' => array() + ); + + $options = array_merge($defaults, $params); + $collection = clone $this; + $searchwords = preg_replace('/(\s)/u',',', $query); + $searchwords = str::split($searchwords, ',', $options['minlength']); + + if(!empty($options['stopwords'])) { + $searchwords = array_diff($searchwords, $options['stopwords']); + } + + if(empty($searchwords)) return $collection->limit(0); + + $searchwords = array_map(function($value) use($options) { + return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value); + }, $searchwords); + + $preg = '!(' . implode('|', $searchwords) . ')!i'; + $results = $collection->filter(function($page) use($query, $searchwords, $preg, $options) { + + $data = $page->content()->toArray(); + $keys = array_keys($data); + + if(!empty($options['fields'])) { + $keys = array_intersect($keys, $options['fields']); + } + + $page->searchHits = 0; + $page->searchScore = 0; + + foreach($keys as $key) { + + $score = a::get($options['score'], $key, 1); + + // check for a match + if($matches = preg_match_all($preg, $data[$key], $r)) { + + $page->searchHits += $matches; + $page->searchScore += $matches * $score; + + // check for full matches + if($matches = preg_match_all('!' . preg_quote($query) . '!i', $data[$key], $r)) { + $page->searchScore += $matches * $score; + } + + } + + } + + return $page->searchHits > 0 ? true : false; + + }); + + $results = $results->sortBy('searchScore', SORT_DESC); + + return $results; + + } + + /** + * Returns files from all pages + * + * @return object A collection of all files of the pages (not of their subpages) + */ + public function files() { + + $files = new Collection(); + + foreach($this->data as $page) { + foreach($page->files() as $file) { + $files->append($page->id() . '/' . strtolower($file->filename()), $file); + } + } + + return $files; + + } + + // File type filters + public function images() { return $this->files()->filterBy('type', 'image'); } + public function videos() { return $this->files()->filterBy('type', 'video'); } + public function documents() { return $this->files()->filterBy('type', 'document'); } + public function audio() { return $this->files()->filterBy('type', 'audio'); } + public function code() { return $this->files()->filterBy('type', 'code'); } + public function archives() { return $this->files()->filterBy('type', 'archive'); } + + /** + * Groups the pages by a given field + * + * @param string $field + * @param bool $i (ignore upper/lowercase for group names) + * @return object A collection with an item for each group and a Pages object for each group + */ + public function groupBy($field, $i = true) { + + $groups = array(); + + foreach($this->data as $key => $item) { + + $value = $item->content()->get($field)->value(); + + // make sure that there's always a proper value to group by + if(!$value) throw new Exception('Invalid grouping value for key: ' . $key); + + // ignore upper/lowercase for group names + if($i) $value = str::lower($value); + + if(!isset($groups[$value])) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new Pages(array($key => $item)); + } else { + // add the item to an existing group + $groups[$value]->set($key, $item); + } + + } + + return new Collection($groups); + + } + + /** + * Converts the pages collection + * into a plain array + * + * @param closure $callback Filter callback for each item + * @return array + */ + public function toArray($callback = null) { + $data = array(); + foreach($this as $page) { + $data[] = is_string($page) ? $page : $page->toArray($callback); + } + return $data; + } + + /** + * Converts the pages collection + * into a json string + * + * @param closure $callback Filter callback for each item + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + +} \ No newline at end of file diff --git a/kirby/core/role.php b/kirby/core/role.php new file mode 100644 index 0000000..bbfb897 --- /dev/null +++ b/kirby/core/role.php @@ -0,0 +1,102 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class RoleAbstract { + + protected $id = null; + protected $name = null; + protected $panel = false; + protected $permissions = array( + 'panel.access' => true, + 'panel.site.update' => true, + 'panel.page.create' => true, + 'panel.page.update' => true, + 'panel.page.move' => true, + 'panel.page.sort' => true, + 'panel.page.hide' => true, + 'panel.page.delete' => true, + 'panel.file.upload' => true, + 'panel.file.replace' => true, + 'panel.file.update' => true, + 'panel.file.delete' => true, + 'panel.user.add' => true, + 'panel.user.edit' => true, + 'panel.user.role' => true, + 'panel.user.delete' => true, + ); + + public $default = false; + + public function __construct($data = array()) { + + if(!isset($data['id'])) throw new Exception('The role id is missing'); + if(!isset($data['name'])) throw new Exception('The role name is missing'); + + // required data + $this->id = $data['id']; + $this->name = $data['name']; + + if(isset($data['permissions']) and is_array($data['permissions'])) { + $this->permissions = a::merge($this->permissions, $data['permissions']); + } else if(isset($data['permissions']) and $data['permissions'] === false) { + $this->permissions = array_fill_keys(array_keys($this->permissions), false); + } else { + $this->permissions = $this->permissions; + } + + // fallback permissions support for old 'panel' role variable + if(isset($data['panel']) and is_bool($data['panel'])) { + $this->permissions['panel.access'] = $data['panel']; + } + + // is this role the default role? + if(isset($data['default'])) { + $this->default = $data['default'] === true; + } + + } + + public function id() { + return $this->id; + } + + public function name() { + return $this->name; + } + + // support for old 'panel' role permission + public function hasPanelAccess() { + return $this->hasPermission('panel.access'); + } + + public function hasPermission($target) { + if($this->id == 'admin') { + return true; + } else if(isset($this->permissions[$target]) and $this->permissions[$target] === true) { + return true; + } else { + return false; + } + } + + public function isDefault() { + return $this->default; + } + + public function users() { + return kirby::instance()->site()->users()->filterBy('role', $this->id); + } + + public function __toString() { + return (string)$this->id; + } + +} diff --git a/kirby/core/roles.php b/kirby/core/roles.php new file mode 100644 index 0000000..2d7ff69 --- /dev/null +++ b/kirby/core/roles.php @@ -0,0 +1,84 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class RolesAbstract extends Collection { + + // cache for the default role + protected $default = null; + + /** + * Constructor + */ + public function __construct() { + + $roles = kirby::instance()->option('roles'); + + // set the default set of roles, if roles are not configured + if(empty($roles)) { + $roles = array( + array( + 'id' => 'admin', + 'name' => 'Admin', + 'default' => true + ), + array( + 'id' => 'editor', + 'name' => 'Editor', + 'permissions' => array( + 'panel.access' => true, + 'panel.site.update' => false, + 'panel.page.create' => true, + 'panel.page.update' => true, + 'panel.page.move' => true, + 'panel.page.sort' => true, + 'panel.page.hide' => true, + 'panel.page.delete' => true, + 'panel.file.upload' => true, + 'panel.file.replace' => true, + 'panel.file.update' => true, + 'panel.file.delete' => true, + 'panel.user.add' => false, + 'panel.user.edit' => false, + 'panel.user.role' => false, + 'panel.user.delete' => false + ) + ) + ); + } + + foreach($roles as $role) { + $role = new Role($role); + $this->data[$role->id()] = $role; + } + + // check for a valid admin role + if(!isset($this->data['admin'])) { + throw new Exception('There must be an admin role'); + } + + // check for a valid default role + if(!$this->findDefault()) { + $this->data['admin']->default = true; + } + + } + + /** + * Returns the default role for new users + * + * @return Role + */ + public function findDefault() { + if(!is_null($this->default)) return $this->default; + return $this->default = $this->findBy('isDefault', true); + } + +} diff --git a/kirby/core/site.php b/kirby/core/site.php new file mode 100644 index 0000000..1180471 --- /dev/null +++ b/kirby/core/site.php @@ -0,0 +1,337 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class SiteAbstract extends Page { + + // the current page + public $page = null; + + /** + * Constructor + * + */ + public function __construct(Kirby $kirby) { + + $this->kirby = $kirby; + $this->url = $kirby->urls()->index(); + $this->depth = 0; + $this->uri = ''; + $this->site = $this; + $this->page = null; + + // build ugly urls if rewriting is disabled + if($this->kirby->options['rewrite'] === false) { + $this->url .= '/index.php'; + } + + $this->root = $kirby->roots()->content(); + $this->dirname = basename($this->root); + + } + + /** + * Cleans the temporary internal cache + */ + public function reset() { + $this->cache = array(); + } + + /** + * The id is an empty string in case of the site object + * + * @return string + */ + public function id() { + return ''; + } + + /** + * The base diruri is bascially just an empty string + * + * @return string + */ + public function diruri() { + return ''; + } + + /** + * Returns the base url for the site + * + * @return string + */ + public function url() { + return $this->url; + } + + /** + * Returns the full URL for the content folder + * + * @return string + */ + public function contentUrl() { + return $this->kirby()->urls()->content(); + } + + /** + * Checks if this object is the main site + * + * @return boolean + */ + public function isSite() { + return true; + } + + /** + * Returns the usable template + * + * @return string + */ + public function template() { + return 'site'; + } + + /** + * The site has no template + * + * @return boolean + */ + public function templateFile() { + return false; + } + + /** + * Returns the intended template + * + * @return string + */ + public function intendedTemplate() { + return 'site'; + } + + /** + * Again, the site has no template! + * + * @return boolean + */ + public function intendedTemplateFile() { + return false; + } + + /** + * There can't be a template for the site + * Didn't you still get it yet? + * + * @return boolean + */ + public function hasTemplate() { + return false; + } + + /** + * Sets the currently active page + * and returns its page object + * + * @param string $uri + * @return Page + */ + public function visit($uri = '') { + + $uri = trim($uri, '/'); + + if(empty($uri)) { + return $this->page = $this->homePage(); + } else { + if($page = $this->children()->find($uri)) { + return $this->page = $page; + } else { + return $this->page = $this->errorPage(); + } + } + + } + + /** + * Returns the currently active page or any other page by uri + * + * @param string $uri Optional uri to get any page on the site + * @return Page + */ + public function page($uri = null) { + if(is_null($uri)) { + return is_null($this->page) ? $this->page = $this->homePage() : $this->page; + } else { + return $this->children()->find($uri); + } + } + + /** + * Alternative for $this->children() + * + * @return Children + */ + public function pages() { + return $this->children(); + } + + /** + * Builds a breadcrumb collection + * + * @return Children + */ + public function breadcrumb() { + + if(isset($this->cache['breadcrumb'])) return $this->cache['breadcrumb']; + + // get all parents and flip the order + $crumb = $this->page()->parents()->flip(); + + // add the home page + $crumb->prepend($this->homePage()->uri(), $this->homePage()); + + // add the active page + $crumb->append($this->page()->uri(), $this->page()); + + return $this->cache['breadcrumb'] = $crumb; + + } + + /** + * Alternative for $this->page() + * + * @return Page + */ + public function activePage() { + return $this->page(); + } + + /** + * Returns the error page object + * + * @return Page + */ + public function errorPage() { + if(isset($this->cache['errorPage'])) return $this->cache['errorPage']; + return $this->cache['errorPage'] = $this->children()->find($this->kirby->options['error']); + } + + /** + * Returns the home page object + * + * @return Page + */ + public function homePage() { + if(isset($this->cache['homePage'])) return $this->cache['homePage']; + return $this->cache['homePage'] = $this->children()->find($this->kirby->options['home']); + } + + /** + * Returns the locale for the site + * + * @return string + */ + public function locale() { + return isset($this->kirby->options['locale']) ? $this->kirby->options['locale'] : 'en_US'; + } + + /** + * Checks if the site is a multi language site + * + * @return boolean + */ + public function multilang() { + return false; + } + + /** + * Placeholder for multilanguage sites + */ + public function languages() { + return null; + } + + /** + * Placeholder for multilanguage sites + */ + public function language() { + return null; + } + + /** + * Placeholder for multilanguage sites + */ + public function defaultLanguage() { + return null; + } + + /** + * Return the detected language + */ + public function detectedLanguage() { + return null; + } + + /** + * Returns a collection of all users + * + * @return Users + */ + public function users() { + return new Users(); + } + + /** + * Returns the current user + * + * @param string $username Optional way to search for a single user + * @return User + */ + public function user($username = null) { + if(is_null($username)) return User::current(); + try { + return new User($username); + } catch(Exception $e) { + return null; + } + } + + /** + * Returns a collection of all roles + * + * @return Roles + */ + public function roles() { + return new Roles(); + } + + /** + * Gets the last modification date of all pages + * in the content folder. + * + * @param mixed $format + * @param mixed $handler + * @return mixed + */ + public function modified($format = null, $handler = null) { + return dir::modified($this->root, $format, $handler ? $handler : $this->kirby->options['date.handler']); + } + + /** + * Checks if any content of the site has been + * modified after the given unix timestamp + * This is mainly used to auto-update the cache + * + * @return boolean + */ + public function wasModifiedAfter($time) { + return dir::wasModifiedAfter($this->root(), $time); + } + +} \ No newline at end of file diff --git a/kirby/core/user.php b/kirby/core/user.php new file mode 100644 index 0000000..f004a14 --- /dev/null +++ b/kirby/core/user.php @@ -0,0 +1,349 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class UserAbstract { + + protected $username = null; + protected $cache = array(); + protected $data = null; + + public function __construct($username) { + + $this->username = str::slug(basename($username)); + + // check if the account file exists + if(!file_exists($this->file())) { + throw new Exception('The user account could not be found'); + } + + } + + /** + * Returns the username + * + * @return string + */ + public function username() { + return $this->username; + } + + /** + * get all data for the user + */ + public function data() { + + if(!is_null($this->data)) return $this->data; + + // get all data from the account file + $this->data = data::read($this->file(), 'yaml'); + + // make sure all keys are lowercase + $this->data = array_change_key_case($this->data, CASE_LOWER); + + // remove garbage + unset($this->data[0]); + + // add the username + $this->data['username'] = $this->username; + + // return the data array + return $this->data; + + } + + public function __get($key) { + return a::get($this->data(), strtolower($key)); + } + + public function __call($key, $arguments = null) { + return $this->__get($key); + } + + public function role() { + + $roles = kirby::instance()->site()->roles(); + $data = $this->data(); + + if(empty($data['role'])) { + // apply the default role, if no role is stored for the user + $data['role'] = $roles->findDefault()->id(); + } + + // return the role by id + if($role = $roles->get($data['role'])) { + return $role; + } else { + return $roles->findDefault(); + } + + } + + public function hasRole() { + $roles = func_get_args(); + return in_array($this->role()->id(), $roles); + } + + // support for old 'panel' role permission + public function hasPanelAccess() { + return $this->role()->hasPermission('panel.access'); + } + + public function hasPermission($target) { + return $this->role()->hasPermission($target); + } + + public function isAdmin() { + return $this->role()->id() == 'admin'; + } + + public function avatar() { + + if(isset($this->cache['avatar'])) return $this->cache['avatar']; + + $avatar = new Avatar($this); + + return $this->cache['avatar'] = $avatar->exists() ? $avatar : false; + + } + + public function avatarRoot($extension = 'jpg') { + return kirby::instance()->roots()->avatars() . DS . $this->username() . '.' . $extension; + } + + public function gravatar($size = 256) { + return gravatar($this->email(), $size); + } + + protected function file() { + return kirby::instance()->roots()->accounts() . DS . $this->username() . '.php'; + } + + public function textfile() { + return $this->file(); + } + + public function exists() { + return file_exists($this->file()); + } + + public function generateKey() { + return str::random(64); + } + + public function generateSecret($key) { + return sha1($this->username() . $key); + } + + public function login($password) { + + static::logout(); + + if(!password::match($password, $this->password)) return false; + + // create a new session id + s::regenerateId(); + + $key = $this->generateKey(); + $secret = $this->generateSecret($key); + + s::set('kirby_auth_secret', $secret); + s::set('kirby_auth_username', $this->username()); + + cookie::set( + s::$name . '_auth', + $key, + s::$cookie['lifetime'], + s::$cookie['path'], + s::$cookie['domain'], + s::$cookie['secure'], + s::$cookie['httponly'] + ); + + return true; + + } + + static public function logout() { + s::destroy(); + cookie::remove(s::$name . '_auth'); + } + + public function is($user) { + if(!is_a($user, 'User')) return false; + return $this->username() === $user->username(); + } + + public function isCurrent() { + return $this->is(static::current()); + } + + static public function validate($data = array(), $mode = 'insert') { + + if($mode == 'insert') { + + if(empty($data['username'])) { + throw new Exception('Invalid username'); + } + + if(empty($data['password'])) { + throw new Exception('Invalid password'); + } + + } + + if(!empty($data['email']) and !v::email($data['email'])) { + throw new Exception('Invalid email'); + } + + } + + public function update($data = array()) { + + // sanitize the given data + $data = $this->sanitize($data, 'update'); + + // validate the updated dataset + $this->validate($data, 'update'); + + // don't update the username + unset($data['username']); + + // create a new hash for the password + if(!empty($data['password'])) { + $data['password'] = password::hash($data['password']); + } + + // merge with existing fields + $this->data = array_merge($this->data(), $data); + + foreach($this->data as $key => $value) { + if(is_null($value)) unset($this->data[$key]); + } + + // save the new user data + static::save($this->file(), $this->data); + + // return the updated user project + return $this; + + } + + public function delete() { + + if($avatar = $this->avatar()) { + $avatar->delete(); + } + + if(!f::remove($this->file())) { + throw new Exception('The account could not be deleted'); + } else { + return true; + } + + } + + static public function sanitize($data, $mode = 'insert') { + + // all usernames must be lowercase + $data['username'] = str::slug(a::get($data, 'username')); + + // convert all keys to lowercase + $data = array_change_key_case($data, CASE_LOWER); + + // return the cleaned up data + return $data; + + } + + /** + * Creates a new user + * + * @param array $user + * @return User + */ + static public function create($data = array()) { + + // sanitize the given data for the new user + $data = static::sanitize($data, 'insert'); + + // validate the dataset + static::validate($data, 'insert'); + + // create the file root + $file = kirby::instance()->roots()->accounts() . DS . $data['username'] . '.php'; + + // check for an existing username + if(file_exists($file)) { + throw new Exception('The username is taken'); + } + + // create a new hash for the password + if(!empty($data['password'])) { + $data['password'] = password::hash($data['password']); + } + + static::save($file, $data); + + // return the created user project + return new static($data['username']); + + } + + static protected function save($file, $data) { + + $yaml = '' . PHP_EOL . PHP_EOL; + $yaml .= data::encode($data, 'yaml'); + + if(!f::write($file, $yaml)) { + throw new Exception('The user account could not be saved'); + } else { + return true; + } + + } + + static public function unauthorize() { + s::remove('kirby_auth_secret'); + s::remove('kirby_auth_username'); + cookie::remove('kirby_auth'); + } + + static public function current() { + + $cookey = cookie::get(s::$name . '_auth'); + $username = s::get('kirby_auth_username'); + + if(empty($cookey)) { + static::unauthorize(); + return false; + } + + if(s::get('kirby_auth_secret') !== sha1($username . $cookey)) { + static::unauthorize(); + return false; + } + + // find the logged in user by token + try { + $user = new static($username); + return $user; + } catch(Exception $e) { + static::unauthorize(); + return false; + } + + } + + public function __toString() { + return (string)$this->username; + } + +} \ No newline at end of file diff --git a/kirby/core/users.php b/kirby/core/users.php new file mode 100644 index 0000000..f0e1f31 --- /dev/null +++ b/kirby/core/users.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +abstract class UsersAbstract extends Collection { + + public function __construct() { + + $root = kirby::instance()->roots()->accounts(); + + foreach(dir::read($root) as $file) { + + // skip invalid account files + if(f::extension($file) != 'php') continue; + + $user = new User(f::name($file)); + $this->append($user->username(), $user); + + } + + } + + public function create($data) { + return user::create($data); + } + + public function find($username) { + return $this->findBy('username', $username); + } + +} \ No newline at end of file diff --git a/kirby/extensions/methods.php b/kirby/extensions/methods.php new file mode 100644 index 0000000..33034f4 --- /dev/null +++ b/kirby/extensions/methods.php @@ -0,0 +1,279 @@ +value = html($field->value, $keepTags); + return $field; +}; + +/** + * Escapes unwanted characters in the field value + * to protect from possible xss attacks or other + * unwanted side effects in your html code + * @param Field $field The calling Kirby Field instance + * @param string $context html|attr|css|js|url + * @return Field + */ +field::$methods['escape'] = field::$methods['esc'] = function($field, $context = 'html') { + $field->value = esc($field->value, $context); + return $field; +}; + +/** + * Converts html entities and specialchars in the field + * value to valid xml entities + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['xml'] = field::$methods['x'] = function($field) { + $field->value = xml($field->value); + return $field; +}; + +/** + * Parses the field value as kirbytext + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['kirbytext'] = field::$methods['kt'] = function($field) { + $field->value = kirbytext($field); + return $field; +}; + +/** + * Parses the field value as markdown + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['markdown'] = field::$methods['md'] = function($field) { + $field->value = markdown($field->value); + return $field; +}; + +/** + * Converts the field value to lower case + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['lower'] = function($field) { + $field->value = str::lower($field->value); + return $field; +}; + +/** + * Converts the field value to upper case + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['upper'] = function($field) { + $field->value = str::upper($field->value); + return $field; +}; + +/** + * Applies the widont rule to avoid single + * words on the last line + * @param Field $field The calling Kirby Field instance + * @return Field + */ +field::$methods['widont'] = function($field) { + $field->value = widont($field->value); + return $field; +}; + +/** + * Creates a simple text excerpt without formats + * @param Field $field The calling Kirby Field instance + * @param integer $chars The desired excerpt length + * @return string + */ +field::$methods['excerpt'] = function($field, $chars = 140, $mode = 'chars') { + return excerpt($field, $chars, $mode); +}; + +/** + * Shortens the field value by the given length + * @param Field $field The calling Kirby Field instance + * @param integer $length The desired string length + * @param string $rep The attached ellipsis character if the string is longer + * @return string + */ +field::$methods['short'] = function($field, $length, $rep = '…') { + return str::short($field->value, $length, $rep); +}; + +/** + * Returns the string length of the field value + * @param Field $field The calling Kirby Field instance + * @return integer + */ +field::$methods['length'] = function($field) { + return str::length($field->value); +}; + +/** + * Returns the word count for the field value + * @param Field $field The calling Kirby Field instance + * @return integer + */ +field::$methods['words'] = function($field) { + return str_word_count(strip_tags($field->value)); +}; + +/** + * Splits the field value by the given separator + * @param Field $field The calling Kirby Field instance + * @param string $separator The string to split the field value by + * @return array + */ +field::$methods['split'] = function($field, $separator = ',') { + return str::split($field->value, $separator); +}; + +/** + * Parses the field value as yaml and returns an array + * @param Field $field The calling Kirby Field instance + * @return array + */ +field::$methods['yaml'] = function($field) { + return yaml($field->value); +}; + +/** + * Checks if the field value is empty + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['empty'] = field::$methods['isEmpty'] = function($field) { + return empty($field->value); +}; + +/** + * Checks if the field value is not empty + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['isNotEmpty'] = function($field) { + return !$field->isEmpty(); +}; + +/** + * Returns a page object from a uri in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['toPage'] = function($field) { + return page($field->value); +}; + +/** + * Returns all page objects from a yaml list or a $sep separated string in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['pages'] = field::$methods['toPages'] = function($field, $sep = null) { + + if($sep !== null) { + $array = $field->split($sep); + } else { + $array = $field->yaml(); + } + + return $field->site()->pages()->find($array); + +}; + +/** + * Returns a file object from a filename in a field + * @param Field $field The calling Kirby Field instance + * @return Collection + */ +field::$methods['toFile'] = function($field) { + return $field->page()->file($field->value); +}; + +/** + * Adds 'or' method to Field objects, which allows getting a field + * value or getting back a default value if the field is empty. + * @author fvsch + * @param Field $field The calling Kirby Field instance + * @param mixed $fallback Fallback value returned if field is empty + * @return mixed + */ +field::$methods['or'] = function($field, $fallback = null) { + return $field->empty() ? $fallback : $field; +}; + +/** + * Filter the Field value, or a fallback value if the Field is empty, + * to get a boolean value. '1', 'on', 'true' or 'yes' will be true, + * and everything else will be false. + * @author fvsch + * @param Field $field The calling Kirby Field instance + * @param boolean $default Default value returned if field is empty + * @return boolean + */ +field::$methods['bool'] = field::$methods['isTrue'] = function($field, $default = false) { + $val = $field->empty() ? $default : $field->value; + return filter_var($val, FILTER_VALIDATE_BOOLEAN); +}; + +/** + * Checks if the field content is false + * @param Field $field The calling Kirby Field instance + * @return boolean + */ +field::$methods['isFalse'] = function($field) { + return !$field->bool(); +}; + +/** + * Get an integer value for the Field. + * @author fvsch + * @param Object(Field) [$field] The calling Kirby Field instance + * @param integer [$default] Default value returned if field is empty + * @return integer + */ +field::$methods['int'] = function($field, $default = 0) { + $val = $field->empty() ? $default : $field->value; + return intval($val); +}; + +/** + * Get a float value for the Field + * @param Field $field The calling Kirby Field instance + * @param int $default Default value returned if field is empty + * @return float + */ +field::$methods['float'] = function($field, $default = 0) { + $val = $field->empty() ? $default : $field->value; + return floatval($val); +}; + +field::$methods['toStructure'] = field::$methods['structure'] = function($field) { + return structure($field->yaml(), $field->page()); +}; + +field::$methods['link'] = function($field, $attr1 = array(), $attr2 = array()) { + $a = new Brick('a', $field->value()); + + if(is_string($attr1)) { + $a->attr('href', url($attr1)); + $a->attr($attr2); + } else { + $a->attr('href', $field->page()->url()); + $a->attr($attr1); + } + + return $a; + +}; + +field::$methods['toUrl'] = field::$methods['url'] = function($field) { + return url($field->value()); +}; \ No newline at end of file diff --git a/kirby/extensions/tags.php b/kirby/extensions/tags.php new file mode 100644 index 0000000..3c96550 --- /dev/null +++ b/kirby/extensions/tags.php @@ -0,0 +1,309 @@ + array(), + 'html' => function($tag) { + return strtolower($tag->attr('date')) == 'year' ? date('Y') : date($tag->attr('date')); + } +); + +// email tag +kirbytext::$tags['email'] = array( + 'attr' => array( + 'class', + 'title', + 'text', + 'rel' + ), + 'html' => function($tag) { + return html::email($tag->attr('email'), html($tag->attr('text')), array( + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'rel' => $tag->attr('rel'), + )); + } +); + +// file tag +kirbytext::$tags['file'] = array( + 'attr' => array( + 'text', + 'class', + 'title', + 'rel', + 'target', + 'popup' + ), + 'html' => function($tag) { + + // build a proper link to the file + $file = $tag->file($tag->attr('file')); + $text = $tag->attr('text'); + + if(!$file) return $text; + + // use filename if the text is empty and make sure to + // ignore markdown italic underscores in filenames + if(empty($text)) $text = str_replace('_', '\_', $file->name()); + + return html::a($file->url(), html($text), array( + 'class' => $tag->attr('class'), + 'title' => html($tag->attr('title')), + 'rel' => $tag->attr('rel'), + 'target' => $tag->target(), + )); + + } +); + +// image tag +kirbytext::$tags['image'] = array( + 'attr' => array( + 'width', + 'height', + 'alt', + 'text', + 'title', + 'class', + 'imgclass', + 'linkclass', + 'caption', + 'link', + 'target', + 'popup', + 'rel' + ), + 'html' => function($tag) { + + $url = $tag->attr('image'); + $alt = $tag->attr('alt'); + $title = $tag->attr('title'); + $link = $tag->attr('link'); + $caption = $tag->attr('caption'); + $file = $tag->file($url); + + // use the file url if available and otherwise the given url + $url = $file ? $file->url() : url($url); + + // alt is just an alternative for text + if($text = $tag->attr('text')) $alt = $text; + + // try to get the title from the image object and use it as alt text + if($file) { + + if(empty($alt) and $file->alt() != '') { + $alt = $file->alt(); + } + + if(empty($title) and $file->title() != '') { + $title = $file->title(); + } + + } + + // at least some accessibility for the image + if(empty($alt)) $alt = ' '; + + // link builder + $_link = function($image) use($tag, $url, $link, $file) { + + if(empty($link)) return $image; + + // build the href for the link + if($link == 'self') { + $href = $url; + } else if($file and $link == $file->filename()) { + $href = $file->url(); + } else if($tag->file($link)) { + $href = $tag->file($link)->url(); + } else { + $href = $link; + } + + return html::a(url($href), $image, array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('linkclass'), + 'title' => $tag->attr('title'), + 'target' => $tag->target() + )); + + }; + + // image builder + $_image = function($class) use($tag, $url, $alt, $title) { + return html::img($url, array( + 'width' => $tag->attr('width'), + 'height' => $tag->attr('height'), + 'class' => $class, + 'title' => $title, + 'alt' => $alt + )); + }; + + if(kirby()->option('kirbytext.image.figure') or !empty($caption)) { + $image = $_link($_image($tag->attr('imgclass'))); + $figure = new Brick('figure'); + $figure->addClass($tag->attr('class')); + $figure->append($image); + if(!empty($caption)) { + $figure->append('
' . html($caption) . '
'); + } + return $figure; + } else { + $class = trim($tag->attr('class') . ' ' . $tag->attr('imgclass')); + return $_link($_image($class)); + } + + } +); + +// link tag +kirbytext::$tags['link'] = array( + 'attr' => array( + 'text', + 'class', + 'title', + 'rel', + 'lang', + 'target', + 'popup' + ), + 'html' => function($tag) { + + $link = url($tag->attr('link'), $tag->attr('lang')); + $text = $tag->attr('text'); + + if(empty($text)) { + $text = $link; + } + + if(str::isURL($text)) { + $text = url::short($text); + } + + return html::a($link, $text, array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'target' => $tag->target(), + )); + + } +); + +// tel tag +kirbytext::$tags['tel'] = array( + 'attr' => array( + 'text', + 'class', + 'title' + ), + 'html' => function($tag) { + + $text = $tag->attr('text'); + $tel = str_replace(array('/', ' ', '-'), '', $tag->attr('tel')); + + if(empty($text)) $text = $tag->attr('tel'); + + return html::a('tel:' . $tel, html($text), array( + 'rel' => $tag->attr('rel'), + 'class' => $tag->attr('class'), + 'title' => html($tag->attr('title')) + )); + } +); + + +// twitter tag +kirbytext::$tags['twitter'] = array( + 'attr' => array( + 'class', + 'title', + 'text', + 'rel', + 'target', + 'popup', + ), + 'html' => function($tag) { + + // get and sanitize the username + $username = str_replace('@', '', $tag->attr('twitter')); + + // build the profile url + $url = 'https://twitter.com/' . $username; + + // sanitize the link text + $text = $tag->attr('text', '@' . $username); + + // build the final link + return html::a($url, $text, array( + 'class' => $tag->attr('class'), + 'title' => $tag->attr('title'), + 'rel' => $tag->attr('rel'), + 'target' => $tag->target(), + )); + + } +); + +kirbytext::$tags['youtube'] = array( + 'attr' => array( + 'width', + 'height', + 'class', + 'caption' + ), + 'html' => function($tag) { + + $caption = $tag->attr('caption'); + + if(!empty($caption)) { + $figcaption = '
' . escape::html($caption) . '
'; + } else { + $figcaption = null; + } + + return '
' . embed::youtube($tag->attr('youtube'), array( + 'width' => $tag->attr('width', kirby()->option('kirbytext.video.width')), + 'height' => $tag->attr('height', kirby()->option('kirbytext.video.height')), + 'options' => kirby()->option('kirbytext.video.youtube.options') + )) . $figcaption . '
'; + + } +); + +kirbytext::$tags['vimeo'] = array( + 'attr' => array( + 'width', + 'height', + 'class', + 'caption' + ), + 'html' => function($tag) { + + $caption = $tag->attr('caption'); + + if(!empty($caption)) { + $figcaption = '
' . escape::html($caption) . '
'; + } else { + $figcaption = null; + } + + return '
' . embed::vimeo($tag->attr('vimeo'), array( + 'width' => $tag->attr('width', kirby()->option('kirbytext.video.width')), + 'height' => $tag->attr('height', kirby()->option('kirbytext.video.height')), + 'options' => kirby()->option('kirbytext.video.vimeo.options') + )) . $figcaption . '
'; + + } +); + +kirbytext::$tags['gist'] = array( + 'attr' => array( + 'file' + ), + 'html' => function($tag) { + return embed::gist($tag->attr('gist'), $tag->attr('file')); + } +); diff --git a/kirby/helpers.php b/kirby/helpers.php new file mode 100644 index 0000000..229d6be --- /dev/null +++ b/kirby/helpers.php @@ -0,0 +1,327 @@ +component('snippet')->render($file, $data, $return); +} + +/** + * Builds a css link tag for relative or absolute urls + * + * @param string $url + * @param string $media + * @return string + */ +function css() { + return call([kirby::instance()->component('css'), 'tag'], func_get_args()); +} + +/** + * Builds a script tag for relative or absolute links + * + * @param string $src + * @param boolean $async + * @return string + */ +function js($src, $async = false) { + return call([kirby::instance()->component('js'), 'tag'], func_get_args()); +} + +/** + * Global markdown parser shortcut + * + * @param string $text + * @return string + */ +function markdown($text) { + return kirby::instance()->component('markdown')->parse($text); +} + +/** + * Global smartypants parser shortcut + * + * @param string $text + * @return string + */ +function smartypants($text) { + return kirby::instance()->component('smartypants')->parse($text); +} + +/** + * Converts a string to Kirbytext + * + * @param Field $field + * @return string + */ +function kirbytext($field) { + return (string)new Kirbytext($field); +} + +/** + * Returns the Kirby class singleton + * + * @return Kirby + */ +function kirby($class = null) { + return kirby::instance($class); +} + +/** + * Returns the site object + * + * @return Site + */ +function site() { + return kirby::instance()->site(); +} + +/** + * Returns either the current page or any page for a given uri + * + * @return Page + */ +function page() { + return call_user_func_array(array(kirby::instance()->site(), 'page'), func_get_args()); +} + +/** + * Helper to build page collections + * + * @param array $data + */ +function pages($data = array()) { + return new Pages($data); +} + +/** + * Creates an excerpt without html and kirbytext + * + * @param mixed $text Variable object or string + * @param int $length The number of characters which should be included in the excerpt + * @param array $params an array of options for kirbytext: array('markdown' => true, 'smartypants' => true) + * @return string The shortened text + */ +function excerpt($text, $length = 140, $mode = 'chars') { + + if(strtolower($mode) == 'words') { + $text = str::excerpt(kirbytext($text), 0); + + if(str_word_count($text, 0) > $length) { + $words = str_word_count($text, 2); + $pos = array_keys($words); + $text = str::substr($text, 0, $pos[$length]) . '...'; + } + return $text; + + } else { + return str::excerpt(kirbytext($text), $length); + } + +} + +/** + * Helper to create correct text file names for content files + * + * @param string $uri + * @param string $template + * @param string $lang + * @return string + */ +function textfile($uri, $template, $lang = null) { + + $curi = ''; + $parts = str::split($uri, '/'); + $parent = site(); + + foreach($parts as $p) { + + if($parent and $child = $parent->children()->find($p)) { + $curi .= '/' . $child->dirname(); + $parent = $child; + } else { + $curi .= '/' . $p; + $parent = null; + } + + } + + $uri = ltrim($curi, '/'); + $root = kirby::instance()->roots()->content(); + $ext = kirby::instance()->option('content.file.extension', 'txt'); + return $root . DS . r(!empty($uri), str_replace('/', DS, $uri) . DS) . $template . r($lang, '.' . $lang) . '.' . $ext; + +} + +/** + * Renders a kirbytag + * + * @param array $attr + * @return Kirbytag + */ +function kirbytag($attr) { + return new Kirbytag(null, key($attr), $attr); +} + +/** + * Builds a Youtube video iframe + * + * @param string $url + * @param mixed $width + * @param mixed $height + * @param string $class + * @return string + */ +function youtube($url, $width = null, $height = null, $class = null) { + return kirbytag(array( + 'youtube' => $url, + 'width' => $width, + 'height' => $height, + 'class' => $class + )); +} + +/** + * Builds a Vimeo video iframe + * + * @param string $url + * @param mixed $width + * @param mixed $height + * @param string $class + * @return string + */ +function vimeo($url, $width = null, $height = null, $class = null) { + return kirbytag(array( + 'vimeo' => $url, + 'width' => $width, + 'height' => $height, + 'class' => $class + )); +} + +/** + * Builds a Twitter link + * + * @param string $username + * @param string $text + * @param string $title + * @param string $class + * @return string + */ +function twitter($username, $text = null, $title = null, $class = null) { + return kirbytag(array( + 'twitter' => $username, + 'text' => $text, + 'title' => $title, + 'class' => $class + )); +} + +/** + * Embeds a Github Gist + * + * @param string $url + * @param string $file + * @return string + */ +function gist($url, $file = null) { + return kirbytag(array( + 'gist' => $url, + 'file' => $file, + )); +} + +/** + * Returns the current url + * + * @return string + */ +function thisUrl() { + return url::current(); +} + +/** + * Give this any kind of array + * to get some kirby style structure + * + * @param mixed $data + * @param mixed $page + * @param mixed $key + * @return mixed + */ +function structure($data, $page = null, $key = null) { + + if(is_null($page)) { + $page = page(); + } + + if(is_array($data)) { + $result = new Structure(); + $result->page = $page; + foreach($data as $key => $value) { + $result->append($key, structure($value, $page, $key)); + } + return $result; + } else if(is_a($data, 'Field')) { + return $data; + } else { + return new Field($page, $key, $data); + } + +}; + + +/** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string $path + * @return File|null + */ +function image($path = null) { + + if($path === null) { + return page()->image(); + } + + $uri = dirname($path); + $filename = basename($path); + + if($uri == '.') { + $uri = null; + } + + $page = $uri == '/' ? site() : page($uri); + + if($page) { + return $page->image($filename); + } else { + return null; + } + +} + +/** + * Shortcut to create a new thumb object + * + * @param mixed Either a file path or a Media object + * @param array An array of additional params for the thumb + * @return object Thumb + */ +function thumb($image, $params = array(), $obj = true) { + if(is_a($image, 'File') || is_a($image, 'Asset')) { + return $obj ? $image->thumb($params) : $image->thumb($params)->url(); + } else { + $class = new Thumb($image, $params); + return $obj ? $class : $class->url(); + } +} \ No newline at end of file diff --git a/kirby/kirby.php b/kirby/kirby.php new file mode 100644 index 0000000..0e5b0cc --- /dev/null +++ b/kirby/kirby.php @@ -0,0 +1,785 @@ +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; + + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/component.php b/kirby/kirby/component.php new file mode 100644 index 0000000..6ffe1f0 --- /dev/null +++ b/kirby/kirby/component.php @@ -0,0 +1,27 @@ +kirby = $kirby; + } + + public function defaults() { + return []; + } + + public function configure() { + + } + + public function kirby() { + return $this->kirby; + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/css.php b/kirby/kirby/component/css.php new file mode 100644 index 0000000..fc811b3 --- /dev/null +++ b/kirby/kirby/component/css.php @@ -0,0 +1,52 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class CSS extends \Kirby\Component { + + /** + * Builds the html link tag for the given css file + * + * @param string $url + * @param null|string $media + * @return string + */ + public function tag($url, $media = null) { + + if(is_array($url)) { + $css = array(); + foreach($url as $u) $css[] = $this->tag($u, $media); + return implode(PHP_EOL, $css) . PHP_EOL; + } + + // auto template css files + if($url == '@auto') { + + $file = $this->kirby->site()->page()->template() . '.css'; + $root = $this->kirby->roots()->autocss() . DS . $file; + $url = $this->kirby->urls()->autocss() . '/' . $file; + + if(!file_exists($root)) return false; + + } + + return html::tag('link', null, array( + 'rel' => 'stylesheet', + 'href' => url($url), + 'media' => $media + )); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/js.php b/kirby/kirby/component/js.php new file mode 100644 index 0000000..6322c9f --- /dev/null +++ b/kirby/kirby/component/js.php @@ -0,0 +1,51 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class JS extends \Kirby\Component { + + /** + * Builds the html script tag for the given javascript file + * + * @param string $src + * @param boolean async + * @return string + */ + public function tag($src, $async = false) { + + if(is_array($src)) { + $js = array(); + foreach($src as $s) $js[] = $this->tag($s, $async); + return implode(PHP_EOL, $js) . PHP_EOL; + } + + // auto template css files + if($src == '@auto') { + + $file = $this->kirby->site()->page()->template() . '.js'; + $root = $this->kirby->roots()->autojs() . DS . $file; + $src = $this->kirby->urls()->autojs() . '/' . $file; + + if(!file_exists($root)) return false; + + } + + return html::tag('script', '', array( + 'src' => url($src), + 'async' => $async + )); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/markdown.php b/kirby/kirby/component/markdown.php new file mode 100644 index 0000000..8314958 --- /dev/null +++ b/kirby/kirby/component/markdown.php @@ -0,0 +1,56 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Markdown extends \Kirby\Component { + + /** + * Returns the default options for the component + * + * @return array + */ + public function defaults() { + return [ + 'markdown' => true, + 'markdown.extra' => false, + 'markdown.breaks' => true, + ]; + } + + /** + * Initializes the Parsedown parser and + * transforms the given markdown to HTML + * + * @param string $markdown + * @return string + */ + public function parse($markdown) { + + if(!$this->kirby->options['markdown']) { + return $markdown; + } else { + // initialize the right markdown class + $parsedown = $this->kirby->options['markdown.extra'] ? new ParsedownExtra() : new Parsedown(); + + // set markdown auto-breaks + $parsedown->setBreaksEnabled($this->kirby->options['markdown.breaks']); + + // parse it! + return $parsedown->text($markdown); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/response.php b/kirby/kirby/component/response.php new file mode 100644 index 0000000..5a90927 --- /dev/null +++ b/kirby/kirby/component/response.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Response extends \Kirby\Component { + + /** + * Builds and return the response by various input + * + * @param mixed $response + * @return mixed + */ + public function make($response) { + + if(is_string($response)) { + return $this->kirby->render(page($response)); + } else if(is_array($response)) { + return $this->kirby->render(page($response[0]), $response[1]); + } else if(is_a($response, 'Page')) { + return $this->kirby->render($response); + } else if(is_a($response, 'Response')) { + return $response; + } else { + return null; + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/smartypants.php b/kirby/kirby/component/smartypants.php new file mode 100644 index 0000000..0f74ae4 --- /dev/null +++ b/kirby/kirby/component/smartypants.php @@ -0,0 +1,61 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Smartypants extends \Kirby\Component { + + /** + * Returns the default options for + * the smartypants parser + * + * @return array + */ + public function defaults() { + return [ + 'smartypants' => false, + 'smartypants.attr' => 1, + 'smartypants.doublequote.open' => '“', + 'smartypants.doublequote.close' => '”', + 'smartypants.space.emdash' => ' ', + 'smartypants.space.endash' => ' ', + 'smartypants.space.colon' => ' ', + 'smartypants.space.semicolon' => ' ', + 'smartypants.space.marks' => ' ', + 'smartypants.space.frenchquote' => ' ', + 'smartypants.space.thousand' => ' ', + 'smartypants.space.unit' => ' ', + 'smartypants.skip' => 'pre|code|kbd|script|style|math', + ]; + } + + /** + * Initializes the parser and transforms + * the given text. + * + * @param string $text + * @return string + */ + public function parse($text) { + if(!$this->kirby->options['smartypants']) { + return $text; + } else { + // prepare the text + $text = str_replace('"', '"', $text); + // run the parser + $parser = new SmartyPantsTypographer_Parser($this->kirby->options['smartypants.attr']); + return $parser->transform($text); + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/snippet.php b/kirby/kirby/component/snippet.php new file mode 100644 index 0000000..57356d9 --- /dev/null +++ b/kirby/kirby/component/snippet.php @@ -0,0 +1,41 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Snippet extends \Kirby\Component { + + /** + * Returns a snippet file path by name + * + * @param string $name + * @return string + */ + public function file($name) { + return $this->kirby->roots()->snippets() . DS . str_replace('/', DS, $name) . '.php'; + } + + /** + * Renders the snippet with the given data + * + * @param string $name + * @param array $data + * @param boolean $return + * @return string + */ + public function render($name, $data = [], $return = false) { + if(is_object($data)) $data = ['item' => $data]; + return tpl::load($this->kirby->registry->get('snippet', $name), $data, $return); + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/template.php b/kirby/kirby/component/template.php new file mode 100644 index 0000000..21f5ffd --- /dev/null +++ b/kirby/kirby/component/template.php @@ -0,0 +1,89 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Template extends \Kirby\Component { + + /** + * Collects all template data by page + * + * @param mixed $page + * @param array $data + * @return array + */ + public function data($page, $data = []) { + + if($page instanceof Page) { + $data = array_merge( + $page->templateData(), + $data, + $page->controller($data) + ); + } + + // apply the basic template vars + return array_merge(array( + 'kirby' => $this->kirby, + 'site' => $this->kirby->site(), + 'pages' => $this->kirby->site()->children(), + 'page' => $page + ), $data); + + } + + /** + * Returns a template file path by name + * + * @param string $name + * @return string + */ + public function file($name) { + return $this->kirby->roots()->templates() . DS . str_replace('/', DS, $name) . '.php'; + } + + /** + * Renders the template by page with the additional data + * + * @param Page|string $template + * @param array $data + * @param boolean $return + * @return string + */ + public function render($template, $data = [], $return = true) { + + if($template instanceof Page) { + $page = $template; + $file = $page->templateFile(); + $data = $this->data($page, $data); + } else { + $file = $template; + $data = $this->data(null, $data); + } + + // check for an existing template + if(!file_exists($file)) { + throw new Exception('The template could not be found'); + } + + // merge and register the template data globally + tpl::$data = array_merge(tpl::$data, $data); + + // load the template + return tpl::load($file, null, $return); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/component/thumb.php b/kirby/kirby/component/thumb.php new file mode 100644 index 0000000..8532007 --- /dev/null +++ b/kirby/kirby/component/thumb.php @@ -0,0 +1,206 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Thumb extends Component { + + /** + * Returns the default options for the thumb component + * + * @return array + */ + public function defaults() { + + $self = $this; + + return [ + 'thumbs.driver' => 'gd', + 'thumbs.bin' => 'convert', + 'thumbs.interlace' => false, + 'thumbs.quality' => 90, + 'thumbs.memory' => '128M', + 'thumbs.filename' => false, + 'thumbs.destination' => function($thumb) use($self) { + + $path = $self->path($thumb); + + return new Obj([ + 'root' => $self->kirby->roots()->thumbs() . DS . str_replace('/', DS, $path), + 'url' => $self->kirby->urls()->thumbs() . '/' . $path, + ]); + + } + ]; + } + + /** + * Configures the thumb driver + */ + public function configure() { + + $self = $this; + + // setup the thumbnail location + generator::$defaults['root'] = $this->kirby->roots->thumbs(); + generator::$defaults['url'] = $this->kirby->urls->thumbs(); + + // setup the default thumbnail options + generator::$defaults['driver'] = $this->kirby->option('thumbs.driver'); + generator::$defaults['bin'] = $this->kirby->option('thumbs.bin'); + generator::$defaults['quality'] = $this->kirby->option('thumbs.quality'); + generator::$defaults['interlace'] = $this->kirby->option('thumbs.interlace'); + generator::$defaults['memory'] = $this->kirby->option('thumbs.memory'); + generator::$defaults['destination'] = $this->kirby->option('thumbs.destination'); + generator::$defaults['filename'] = $this->kirby->option('thumbs.filename'); + + } + + public function create($file, $params) { + + if(!$file->isWebsafe()) { + return $file; + } + + $thumb = new Generator($file, $params); + $asset = new Asset($thumb->result); + + // store a reference to the original file + $asset->original($file); + + return $thumb->exists() ? $asset : $file; + + } + + /** + * Returns the clean path for a thumbnail + * + * @param Generator $thumb + * @return string + */ + protected function path(Generator $thumb) { + return ltrim($this->dir($thumb) . '/' . $this->filename($thumb), '/'); + } + + /** + * @param Generator $thumb + * @return string + */ + protected function dir(Generator $thumb) { + if(is_a($thumb->source, 'File')) { + return $thumb->source->page()->id(); + } else { + return str_replace($this->kirby->urls()->index(), '', dirname($thumb->source->url())); + } + } + + /** + * Returns the filename for a thumb including the + * identifying option hash + * + * @param Generator $thumb + * @return string + */ + protected function filename(Generator $thumb) { + + $dimensions = $this->dimensions($thumb); + $wh = $dimensions->width() . 'x' . $dimensions->height(); + $safeName = f::safeName($thumb->source->name()); + $options = $this->options($thumb); + $extension = $thumb->source->extension(); + + if($thumb->options['filename'] === false) { + return $safeName . '-' . $wh . r($options, '-' . $options) . '.' . $extension; + } else { + return str::template($thumb->options['filename'], [ + 'extension' => $extension, + 'name' => $thumb->source->name(), + 'filename' => $thumb->source->filename(), + 'safeName' => $safeName, + 'safeFilename' => $safeName . '.' . $extension, + 'width' => $dimensions->width(), + 'height' => $dimensions->height(), + 'dimensions' => $wh, + 'options' => $options, + 'hash' => md5($thumb->source->root() . $thumb->settingsIdentifier()), + ]); + } + + } + + /** + * Returns an identifying option hash for thumb filenames + * + * @param Generator $thumb + * @return string + */ + protected function options(Generator $thumb) { + + $keys = [ + 'blur' => 'blur', + 'grayscale' => 'bw', + 'quality' => 'q', + ]; + + $string = []; + + foreach($keys as $long => $key) { + + $value = a::get($thumb->options, $long); + + if($value === true) { + $string[] = $key; + } else if($value === false) { + continue; + } else if($key === 'q' && $value == generator::$defaults['quality']) { + // ignore the default quality setting + continue; + } else { + $string[] = $key . $value; + } + + } + + return implode('-', array_filter($string)); + + } + + /** + * @param Generator $thumb + * @return string + */ + protected function dimensions(Generator $thumb) { + + $dimensions = clone $thumb->source->dimensions(); + + if(isset($thumb->options['crop']) && $thumb->options['crop']) { + $dimensions->crop(a::get($thumb->options, 'width'), a::get($thumb->options, 'height')); + } else { + $dimensions->resize(a::get($thumb->options, 'width'), a::get($thumb->options, 'height'), a::get($thumb->options, 'upscale')); + } + + return $dimensions; + + } + +} diff --git a/kirby/kirby/component/tinyurl.php b/kirby/kirby/component/tinyurl.php new file mode 100644 index 0000000..6575c4e --- /dev/null +++ b/kirby/kirby/component/tinyurl.php @@ -0,0 +1,54 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class TinyUrl extends \Kirby\Component { + + /** + * Returns the default options for the tinyurl component + * + * @return array + */ + public function defaults() { + return [ + 'tinyurl.enabled' => true, + 'tinyurl.folder' => 'x', + ]; + } + + /** + * Returns the tinyurl fetching route + * + * @return array + */ + public function route() { + if(!$this->kirby->options['tinyurl.enabled']) { + return false; + } else { + return [ + 'pattern' => $this->kirby->options['tinyurl.folder'] . '/(:any)/(:any?)', + 'action' => function($hash, $lang = null) { + // get the site object + $site = site(); + // make sure the language is set + $site->visit('/', $lang); + // find the page by it's tiny hash + if($page = $site->index()->findBy('hash', $hash)) { + go($page->url($lang)); + } else { + return $site->errorPage(); + } + } + ]; + } + } +} \ No newline at end of file diff --git a/kirby/kirby/registry.php b/kirby/kirby/registry.php new file mode 100644 index 0000000..e420cb1 --- /dev/null +++ b/kirby/kirby/registry.php @@ -0,0 +1,118 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Registry { + + /** + * Kirby Instance + * + * @var Kirby + */ + protected $kirby; + + /** + * @param Kirby $kirby + */ + public function __construct(Kirby $kirby) { + + $this->kirby = $kirby; + + // start the registry entry autoloader + load([ + 'kirby\\registry\\entry' => __DIR__ . DS . 'registry' . DS . 'entry.php', + 'kirby\\registry\\blueprint' => __DIR__ . DS . 'registry' . DS . 'blueprint.php', + 'kirby\\registry\\component' => __DIR__ . DS . 'registry' . DS . 'component.php', + 'kirby\\registry\\controller' => __DIR__ . DS . 'registry' . DS . 'controller.php', + 'kirby\\registry\\hook' => __DIR__ . DS . 'registry' . DS . 'hook.php', + 'kirby\\registry\\field' => __DIR__ . DS . 'registry' . DS . 'field.php', + 'kirby\\registry\\method' => __DIR__ . DS . 'registry' . DS . 'method.php', + 'kirby\\registry\\model' => __DIR__ . DS . 'registry' . DS . 'model.php', + 'kirby\\registry\\option' => __DIR__ . DS . 'registry' . DS . 'option.php', + 'kirby\\registry\\route' => __DIR__ . DS . 'registry' . DS . 'route.php', + 'kirby\\registry\\snippet' => __DIR__ . DS . 'registry' . DS . 'snippet.php', + 'kirby\\registry\\template' => __DIR__ . DS . 'registry' . DS . 'template.php', + 'kirby\\registry\\tag' => __DIR__ . DS . 'registry' . DS . 'tag.php', + 'kirby\\registry\\widget' => __DIR__ . DS . 'registry' . DS . 'widget.php', + ]); + + } + + /** + * Returns the Kirby instance + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns a registry entry object by type + * + * @param string $type + * @param string $subtype + * @return Kirby\Registry\Entry + */ + public function entry($type, $subtype = null) { + + $class = 'kirby\\registry\\' . $type; + + if(!class_exists('kirby\\registry\\' . $type)) { + + if(str::contains($type, '::')) { + $parts = str::split($type, '::'); + $subtype = $parts[0]; + $type = $parts[1]; + return $this->entry($type, $subtype); + } + + throw new Exception('Unsupported registry entry type: ' . $type); + + } + + return new $class($this, $subtype); + + } + + /** + * Adds a new entry to the registry + * This will initialize a registry object + * and call the set method of it + * with the passed arguments + */ + public function set() { + $args = func_get_args(); + $type = strtolower(array_shift($args)); + return $this->entry($type)->call('set', $args); + } + + /** + * Retrieves an entry from the registry + * + * This will initialize a registry object + * and call the get method of it + * with the passed arguments + * + * @return Entry + */ + public function get() { + $args = func_get_args(); + $type = array_shift($args); + return $this->entry($type)->call('get', $args); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/blueprint.php b/kirby/kirby/registry/blueprint.php new file mode 100644 index 0000000..351e69c --- /dev/null +++ b/kirby/kirby/registry/blueprint.php @@ -0,0 +1,69 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Blueprint extends Entry { + + /** + * Blueprint store + * + * @var array $blueprints + */ + protected static $blueprints = []; + + /** + * Adds a new blueprint entry + * + * Pass a path to an existing blueprint file + * to add it to the registry + * + * @param string $name + * @param string $path + * @return $path + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$blueprints[$name] = $path; + } + + throw new Exception('The blueprint does not exist at the specified path: ' . $path); + + } + + /** + * Retreives a registered blueprint file path + * + * @param string $name + * @return string + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$blueprints; + } + + $file = f::resolve($this->kirby->roots()->blueprints() . DS . str_replace('/', DS, $name), ['php', 'yml', 'yaml']); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$blueprints, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/component.php b/kirby/kirby/registry/component.php new file mode 100644 index 0000000..3679773 --- /dev/null +++ b/kirby/kirby/registry/component.php @@ -0,0 +1,39 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Component extends Entry { + + /** + * Adds a new core component to Kirby + * + * This will directly call the component method of the + * Kirby instance to register the component + * + * @param string $name The name of the component + * @param string $class A valid component classname. Must be extend the according Kirby component type class + */ + public function set($name, $class) { + return $this->kirby->component($name, $class); + } + + /** + * Retreives a component from the Kirby component registry + * + * @param string $name + * @return Kirby\Component + */ + public function get($name) { + return $this->kirby->component($name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/controller.php b/kirby/kirby/registry/controller.php new file mode 100644 index 0000000..3fd39a2 --- /dev/null +++ b/kirby/kirby/registry/controller.php @@ -0,0 +1,78 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Controller extends Entry { + + /** + * Store of registered controllers + * + * @var array $controllers + */ + protected static $controllers = []; + + /** + * Adds a new controller to the registry + * + * @param string $name + * @param Closure $callback Must be a valid controller callback + * @return Closure + */ + public function set($name, $callback) { + + $name = strtolower($name); + + if($name === 'site') { + throw new Exception('You are not allowed to set the site controller'); + } + + if(!$this->kirby->option('debug') || is_a($callback, 'Closure') || file_exists($callback)) { + return static::$controllers[$name] = $callback; + } else { + throw new Exception('Invalid controller. You must pass a closure or an existing file'); + } + + } + + /** + * Retreives a controller from the registry + * + * @param string $name + * @return Closure + */ + public function get($name) { + + $name = strtolower($name); + $file = $this->kirby->roots()->controllers() . DS . $name . '.php'; + + if(file_exists($file)) { + return include_once $file; + } + + if(isset(static::$controllers[$name])) { + if(is_a(static::$controllers[$name], 'Closure')) { + return static::$controllers[$name]; + } else if(file_exists(static::$controllers[$name])) { + return include_once static::$controllers[$name]; + } + } + + if(file_exists($this->kirby->roots()->controllers() . DS . 'site.php')) { + return include_once $this->kirby->roots()->controllers() . DS . 'site.php'; + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/entry.php b/kirby/kirby/registry/entry.php new file mode 100644 index 0000000..9d14009 --- /dev/null +++ b/kirby/kirby/registry/entry.php @@ -0,0 +1,98 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Entry { + + /** + * Kirby instance + * + * @var Kirby + */ + protected $kirby; + + /** + * Kirby Registry instance + * + * @var Kirby\Registry + */ + protected $registry; + + /** + * Optional subtype for something + * like $kirby->set('field::method', '…') + * where `field` is the subtype of type `method`. + * + * @param string $subtype + */ + protected $subtype; + + /** + * @param Kirby $kirby + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype = null) { + $this->registry = $registry; + $this->kirby = $registry->kirby(); + $this->subtype = $subtype; + } + + /** + * Interface to call any registry entry method + * + * Mostly used for set() and get() + * + * @param string $method + * @param array $args + * @return mixed + */ + public function call($method, $args) { + return call([$this, $method], $args); + } + + /** + * Returns the Kirby instance + * + * @return Kirby + */ + public function kirby() { + return $this->kirby; + } + + /** + * Returns the Registry instance + * + * @return Kirby\Registry + */ + public function registry() { + return $this->registry; + } + + /** + * Returns the optional subtype + * + * @return string + */ + public function subtype() { + return $this->subtype; + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/field.php b/kirby/kirby/registry/field.php new file mode 100644 index 0000000..1c60f72 --- /dev/null +++ b/kirby/kirby/registry/field.php @@ -0,0 +1,68 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Field extends Entry { + + /** + * Store for registered fields + * + * @var array $fields + */ + protected static $fields = []; + + /** + * Adds a new field to the registry + * + * @param string $name + * @param string $root valid field directory path + * @return Obj generic Kirby object with info about the field + */ + public function set($name, $root) { + + $name = strtolower($name); + $file = $root . DS . $name . '.php'; + + if(!$this->kirby->option('debug') || (is_dir($root) && is_file($file))) { + return static::$fields[$name] = new Obj([ + 'root' => $root, + 'file' => $file, + 'name' => $name, + 'class' => $name . 'field', + ]); + } + + throw new Exception('The field does not exist at the specified path: ' . $root); + + } + + /** + * Retreives a field info object from the registry + * + * @param string|null $name If null, all registered fields will be returned as array + * @param Obj|null|array + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$fields; + } + + return a::get(static::$fields, $name); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/hook.php b/kirby/kirby/registry/hook.php new file mode 100644 index 0000000..970ef96 --- /dev/null +++ b/kirby/kirby/registry/hook.php @@ -0,0 +1,30 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Hook extends Entry { + + /** + * Registers a new hook + * + * This will directly call the $kirby->hook() method + * A hook has to be a valid closure + * + * @param string $name + * @param Closure $callback + * @return Closure + */ + public function set($name, $callback) { + return $this->kirby->hook($name, $callback); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/method.php b/kirby/kirby/registry/method.php new file mode 100644 index 0000000..d91dec3 --- /dev/null +++ b/kirby/kirby/registry/method.php @@ -0,0 +1,71 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Method extends Entry { + + /** + * List of allowed subtypes + * + * @var array $subtypes + */ + protected $subtypes = ['site', 'page', 'pages', 'file', 'files', 'field']; + + /** + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype) { + parent::__construct($registry, $subtype); + if(!in_array($this->subtype, $this->subtypes)) { + throw new Exception('Invalid method type: ' . $this->subtype . '::method'); + } + } + + /** + * Adds a new method to the registry + * + * A method can be registered for any of the allowed + * subtypes, by using the static method syntax: + * $kirby->set('page::method') + * $kirby->set('field::method') + * etc. + * + * The first part of the name is the subtype. + * The second part of the name is the main type (`method` in this case) + * + * @param string $name + * @param Closure $callback + * @return Closure + */ + public function set($name, $callback) { + $class = $this->subtype; + return $class::$methods[$name] = $callback; + } + + /** + * Retrieves a registered method + * + * @param string $name + * @return Closure + */ + public function get($name) { + $class = $this->subtype; + return a::get($class::$methods, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/model.php b/kirby/kirby/registry/model.php new file mode 100644 index 0000000..09cd72d --- /dev/null +++ b/kirby/kirby/registry/model.php @@ -0,0 +1,77 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Model extends Entry { + + /** + * List of allowed subtypes + * + * @var array $subtypes + */ + protected $subtypes = ['page']; + + /** + * @param Kirby\Registry $registry + * @param string $subtype + */ + public function __construct(Registry $registry, $subtype) { + parent::__construct($registry, $subtype); + if(!in_array($this->subtype, $this->subtypes)) { + throw new Exception('Invalid model type: ' . $this->subtype . '::model'); + } + } + + /** + * Adds a new model to the registry + * + * A model can be registered for any of the allowed + * subtypes, by using the static method syntax: + * + * $kirby->set('page::model') + * + * The first part of the name is the subtype. + * The second part of the name is the main type (`model` in this case) + * + * @param string $name + * @param string $classname Must be a valid classname of a loaded/auto-loaded class + * @return string + */ + public function set($name, $classname) { + + $class = $this->subtype; + + if(!class_exists($classname)) { + throw new Exception('The model class does not exist: ' . $classname); + } + + return $class::$models[$name] = $classname; + + } + + /** + * Retrieves a registered model + * + * @param string $name + * @return string + */ + public function get($name) { + $class = $this->subtype; + return a::get($class::$models, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/option.php b/kirby/kirby/registry/option.php new file mode 100644 index 0000000..03cd176 --- /dev/null +++ b/kirby/kirby/registry/option.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Option extends Entry { + + /** + * Sets a Kirby option + * + * This directly adds passed options to the + * $kirby->options array and is just a convenient + * way to do this through the registry + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function set($key, $value) { + return $this->kirby->options[$key] = $value; + } + + /** + * Retreives an option from the $kirby->$options array + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) { + return $this->kirby->option($key, $default); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/route.php b/kirby/kirby/registry/route.php new file mode 100644 index 0000000..e812c2d --- /dev/null +++ b/kirby/kirby/registry/route.php @@ -0,0 +1,28 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Route extends Entry { + + /** + * Registers a new route + * + * This will directly add a route to + * Kirby's route system, by calling $kirby->routes() + * + * @param string $attr + */ + public function set($attr) { + $this->kirby->routes([$attr]); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/snippet.php b/kirby/kirby/registry/snippet.php new file mode 100644 index 0000000..9158fe5 --- /dev/null +++ b/kirby/kirby/registry/snippet.php @@ -0,0 +1,64 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Snippet extends Entry { + + /** + * List of registered snippet files + * + * @var array $snippets + */ + protected static $snippets = []; + + /** + * Registers a new snippet file + * + * You must pass an existing file in order + * to register it as a valid snippet + * + * @param string $name The name of the snippet. Can contain slashes (i.e. form/field) + * @param string $path + * @return string + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$snippets[$name] = $path; + } + + throw new Exception('The snippet does not exist at the specified path: ' . $path); + + } + + /** + * Retrieve the file path for a registered snippet + * + * @param string $name + * @return string + */ + public function get($name) { + + $file = $this->kirby->component('snippet')->file($name); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$snippets, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/tag.php b/kirby/kirby/registry/tag.php new file mode 100644 index 0000000..740d84a --- /dev/null +++ b/kirby/kirby/registry/tag.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Tag extends Entry { + + /** + * Registers a new kirby tag array + * + * This will directly add the tag to the + * kirbytext::$tags array. + * + * @param string $name + * @param array $tag + */ + public function set($name, $tag) { + kirbytext::$tags[$name] = $tag; + } + + /** + * Retreives a registered kirby tag + * + * @param string $name + * @return array + */ + public function get($name) { + return a::get(kirbytext::$tags, $name); + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/template.php b/kirby/kirby/registry/template.php new file mode 100644 index 0000000..b1d1df0 --- /dev/null +++ b/kirby/kirby/registry/template.php @@ -0,0 +1,62 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Template extends Entry { + + /** + * List of registered template files + * + * @var array $templates + */ + protected static $templates = []; + + /** + * Registers a new template file + * + * Must be an existing file + * + * @param string $name + * @param string $path + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || file_exists($path)) { + return static::$templates[$name] = $path; + } + + throw new Exception('The template does not exist at the specified path: ' . $path); + + } + + /** + * Retrieves a registered template file + * + * @param string $name + * @return string + */ + public function get($name) { + + $file = $this->kirby->component('template')->file($name); + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$templates, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/registry/widget.php b/kirby/kirby/registry/widget.php new file mode 100644 index 0000000..4d1ff85 --- /dev/null +++ b/kirby/kirby/registry/widget.php @@ -0,0 +1,67 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://getkirby.com/license + */ +class Widget extends Entry { + + /** + * List of registered widget directories + * + * @var array $widgets + */ + protected static $widgets = []; + + /** + * Registers a new widget + * + * You must pass an existing widget directory + * + * @param string $name + * @param string $path + * @return string + */ + public function set($name, $path) { + + if(!$this->kirby->option('debug') || is_dir($path)) { + return static::$widgets[$name] = $path; + } + + throw new Exception('The widget does not exist at the specified path: ' . $path); + + } + + /** + * Retreives a registered widget directory + * + * @param string|null $name If null, all registered widgets will be returned as array + * @return string|array + */ + public function get($name = null) { + + if(is_null($name)) { + return static::$widgets; + } + + $file = $this->kirby->roots()->widgets() . DS . str_replace('/', DS, $name) . '.php'; + + if(file_exists($file)) { + return $file; + } else { + return a::get(static::$widgets, $name); + } + + } + +} \ No newline at end of file diff --git a/kirby/kirby/request.php b/kirby/kirby/request.php new file mode 100644 index 0000000..5b5b0d5 --- /dev/null +++ b/kirby/kirby/request.php @@ -0,0 +1,41 @@ +kirby = $kirby; + } + + public function url() { + return url::current(); + } + + public function params() { + return new Request\Params(url::params()); + } + + public function query() { + return new Request\Query(url::query()); + } + + public function path() { + return new Request\Path($this->kirby->path()); + } + + public function __call($method, $arguments) { + if(method_exists('r', $method)) { + return call('r::' . $method, $arguments); + } else { + throw new Exception('Invalid method: ' . $method); + } + } + +} diff --git a/kirby/kirby/request/params.php b/kirby/kirby/request/params.php new file mode 100644 index 0000000..ccdb541 --- /dev/null +++ b/kirby/kirby/request/params.php @@ -0,0 +1,22 @@ + $value) { + $params[] = $key . url::paramSeparator() . $value; + } + + return implode('/', $params); + + } + +} \ No newline at end of file diff --git a/kirby/kirby/request/path.php b/kirby/kirby/request/path.php new file mode 100644 index 0000000..dda2af0 --- /dev/null +++ b/kirby/kirby/request/path.php @@ -0,0 +1,18 @@ +data); + } + +} \ No newline at end of file diff --git a/kirby/kirby/request/query.php b/kirby/kirby/request/query.php new file mode 100644 index 0000000..bd58f8b --- /dev/null +++ b/kirby/kirby/request/query.php @@ -0,0 +1,13 @@ +index = $index; + } + + public function content() { + return isset($this->content) ? $this->content : $this->index . DS . 'content'; + } + + public function site() { + return isset($this->site) ? $this->site : $this->index . DS . 'site'; + } + + public function kirby() { + return isset($this->kirby) ? $this->kirby : $this->index . DS . 'kirby'; + } + + public function thumbs() { + return isset($this->thumbs) ? $this->thumbs : $this->index . DS . 'thumbs'; + } + + public function assets() { + return isset($this->assets) ? $this->assets : $this->index . DS . 'assets'; + } + + public function autocss() { + return isset($this->autocss) ? $this->autocss : $this->assets() . DS . 'css' . DS . 'templates'; + } + + public function autojs() { + return isset($this->autojs) ? $this->autojs : $this->assets() . DS . 'js' . DS . 'templates'; + } + + public function avatars() { + return isset($this->avatars) ? $this->avatars : $this->assets() . DS . 'avatars'; + } + + public function config() { + return $this->site() . DS . 'config'; + } + + public function accounts() { + return isset($this->accounts) ? $this->accounts : $this->site() . DS . 'accounts'; + } + + public function blueprints() { + return $this->site() . DS . 'blueprints'; + } + + public function plugins() { + return $this->site() . DS . 'plugins'; + } + + public function cache() { + return isset($this->cache) ? $this->cache : $this->site() . DS . 'cache'; + } + + public function tags() { + return $this->site() . DS . 'tags'; + } + + public function fields() { + return $this->site() . DS . 'fields'; + } + + public function widgets() { + return $this->site() . DS . 'widgets'; + } + + public function controllers() { + return $this->site() . DS . 'controllers'; + } + + public function models() { + return $this->site() . DS . 'models'; + } + + public function templates() { + return $this->site() . DS . 'templates'; + } + + public function snippets() { + return $this->site() . DS . 'snippets'; + } + + public function languages() { + return $this->site() . DS . 'languages'; + } + +} diff --git a/kirby/kirby/traits/image.php b/kirby/kirby/traits/image.php new file mode 100644 index 0000000..5cd43a4 --- /dev/null +++ b/kirby/kirby/traits/image.php @@ -0,0 +1,207 @@ +original; + } else { + $this->original = $original; + return $this; + } + } + + /** + * Creates a thumbnail for the image + * + * @param array $params + * @return Asset + */ + public function thumb($params = []) { + // don't scale thumbs further down + if($this->original()) { + throw new Exception('Thumbnails cannot be modified further'); + } else { + return $this->kirby->component('thumb')->create($this, $params); + } + } + + /** + * Scales the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Asset + */ + public function resize($width, $height = null, $quality = null) { + + $params = ['width' => $width]; + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales and crops the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Asset + */ + public function crop($width, $height = null, $quality = null) { + + $params = ['width' => $width, 'crop' => true]; + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales the width of the image + * + * @param int $width + * @param mixed $quality + * @return Asset + */ + public function width($width = null, $quality = null) { + + if($width === null) { + return parent::width(); + } + + $params = ['width' => $width]; + + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * Scales the height of the image + * + * @param int $height + * @param mixed $quality + * @return Asset + */ + public function height($height = null, $quality = null) { + + if($height === null) { + return parent::height(); + } + + $params = ['height' => $height]; + + if($quality) $params['quality'] = $quality; + + return $this->thumb($params); + + } + + /** + * + */ + public function ratio($ratio = null) { + + if($ratio === null) { + return parent::ratio(); + } + + if($this->isLandscape() || $this->isSquare()) { + $width = $this->width(); + $height = round($width / $ratio); + } else { + $height = $this->height(); + $width = round($height * $ratio); + } + + return $this->crop($width, $height); + + } + + /** + * + */ + public function scale($value) { + return $this->thumb(['width' => $this->width() * $value, 'upscale' => true]); + } + + /** + * Converts the image to grayscale + * + * @return Asset + */ + public function bw() { + return $this->thumb(['grayscale' => true]); + } + + /** + * Blurs the image + * + * @return Asset + */ + public function blur() { + return $this->thumb(['blur' => true]); + } + + /** + * Checks if the asset is a thumbnail + * + * @return boolean + */ + public function isThumb() { + return str::startsWith($this->url(), $this->kirby->urls()->thumbs()); + } + + /** + * Check if the file/image has a websafe format + * + * @return boolean + */ + public function isWebsafe() { + return in_array(strtolower($this->extension()), ['jpg', 'jpeg', 'gif', 'png']); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + if($this->isWebsafe()) { + return (string)$this->html(); + } else { + return (string)$this->root; + } + } + +} \ No newline at end of file diff --git a/kirby/kirby/urls.php b/kirby/kirby/urls.php new file mode 100644 index 0000000..1da8565 --- /dev/null +++ b/kirby/kirby/urls.php @@ -0,0 +1,47 @@ +index)) return $this->index; + + if(r::cli()) { + return $this->index = '/'; + } else { + return $this->index = url::base() . preg_replace('!\/index\.php$!i', '', server::get('SCRIPT_NAME')); + } + + } + + public function content() { + return isset($this->content) ? $this->content : url::makeAbsolute('content', $this->index); + } + + public function thumbs() { + return isset($this->thumbs) ? $this->thumbs : url::makeAbsolute('thumbs', $this->index); + } + + public function assets() { + return isset($this->assets) ? $this->assets : url::makeAbsolute('assets', $this->index); + } + + public function autocss() { + return isset($this->autocss) ? $this->autocss : $this->assets() . '/css/templates'; + } + + public function autojs() { + return isset($this->autojs) ? $this->autojs : $this->assets() . '/js/templates'; + } + + public function avatars() { + return isset($this->avatars) ? $this->avatars : $this->assets() . '/avatars'; + } + +} \ No newline at end of file diff --git a/kirby/lib/pageextension.php b/kirby/lib/pageextension.php new file mode 100644 index 0000000..4517394 --- /dev/null +++ b/kirby/lib/pageextension.php @@ -0,0 +1,17 @@ +parent(), $page->dirname()); + } else { + throw new Exception('The page could not be found'); + } + } +} \ No newline at end of file diff --git a/kirby/lib/structure.php b/kirby/lib/structure.php new file mode 100644 index 0000000..4d8ff81 --- /dev/null +++ b/kirby/lib/structure.php @@ -0,0 +1,42 @@ +data[$key])) { + return $this->data[$key]; + } else { + $lowerkeys = array_change_key_case($this->data, CASE_LOWER); + $lowerkey = strtolower($key); + if(isset($lowerkeys[$lowerkey])) { + return $lowerkeys[$lowerkey]; + } + } + + return new Field($this->page, $key, null); + + } + + /** + * Get formatted date fields + * + * @param string $format + * @param string $field + * @return string + */ + public function date($format = null, $field = 'date') { + + if($timestamp = strtotime($this->get($field))) { + if(is_null($format)) { + return $timestamp; + } else { + return kirby()->options['date.handler']($format, $timestamp); + } + } + + } + +} \ No newline at end of file diff --git a/kirby/readme.md b/kirby/readme.md new file mode 100644 index 0000000..78ec751 --- /dev/null +++ b/kirby/readme.md @@ -0,0 +1,18 @@ +# Kirby Core + +This is the Kirby Core submodule. + +Please refer to the [Kirby Starterkit](http://github.com/getkirby/starterkit) +for a complete installation of Kirby + +![Build Status](https://travis-ci.org/getkirby/kirby.svg?branch=master) + +## Author +Bastian Allgeier + + +## Website + + +## License + diff --git a/kirby/system.php b/kirby/system.php new file mode 100644 index 0000000..2338915 --- /dev/null +++ b/kirby/system.php @@ -0,0 +1,21 @@ +roots->index = $root; +$kirby->roots->site = $rootSite; +$kirby->roots->content = $rootContent; + +// render +echo $kirby->launch(); \ No newline at end of file diff --git a/kirby/toolkit/bootstrap.php b/kirby/toolkit/bootstrap.php new file mode 100644 index 0000000..6aeb70a --- /dev/null +++ b/kirby/toolkit/bootstrap.php @@ -0,0 +1,97 @@ + __DIR__ . DS . 'lib' . DS . 'a.php', + 'bitmask' => __DIR__ . DS . 'lib' . DS . 'bitmask.php', + 'brick' => __DIR__ . DS . 'lib' . DS . 'brick.php', + 'c' => __DIR__ . DS . 'lib' . DS . 'c.php', + 'cookie' => __DIR__ . DS . 'lib' . DS . 'cookie.php', + 'cache' => __DIR__ . DS . 'lib' . DS . 'cache.php', + 'cache\\driver' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver.php', + 'cache\\driver\\apc' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'apc.php', + 'cache\\driver\\file' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'file.php', + 'cache\\driver\\memcached' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'memcached.php', + 'cache\\driver\\mock' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'mock.php', + 'cache\\driver\\session' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'driver' . DS . 'session.php', + 'cache\\value' => __DIR__ . DS . 'lib' . DS . 'cache' . DS . 'value.php', + 'collection' => __DIR__ . DS . 'lib' . DS . 'collection.php', + 'crypt' => __DIR__ . DS . 'lib' . DS . 'crypt.php', + 'data' => __DIR__ . DS . 'lib' . DS . 'data.php', + 'database' => __DIR__ . DS . 'lib' . DS . 'database.php', + 'database\\query' => __DIR__ . DS . 'lib' . DS . 'database' . DS . 'query.php', + 'db' => __DIR__ . DS . 'lib' . DS . 'db.php', + 'detect' => __DIR__ . DS . 'lib' . DS . 'detect.php', + 'dimensions' => __DIR__ . DS . 'lib' . DS . 'dimensions.php', + 'dir' => __DIR__ . DS . 'lib' . DS . 'dir.php', + 'email' => __DIR__ . DS . 'lib' . DS . 'email.php', + 'embed' => __DIR__ . DS . 'lib' . DS . 'embed.php', + 'error' => __DIR__ . DS . 'lib' . DS . 'error.php', + 'errorreporting' => __DIR__ . DS . 'lib' . DS . 'errorreporting.php', + 'escape' => __DIR__ . DS . 'lib' . DS . 'escape.php', + 'exif' => __DIR__ . DS . 'lib' . DS . 'exif.php', + 'exif\\camera' => __DIR__ . DS . 'lib' . DS . 'exif' . DS . 'camera.php', + 'exif\\location' => __DIR__ . DS . 'lib' . DS . 'exif' . DS . 'location.php', + 'f' => __DIR__ . DS . 'lib' . DS . 'f.php', + 'folder' => __DIR__ . DS . 'lib' . DS . 'folder.php', + 'header' => __DIR__ . DS . 'lib' . DS . 'header.php', + 'html' => __DIR__ . DS . 'lib' . DS . 'html.php', + 'i' => __DIR__ . DS . 'lib' . DS . 'i.php', + 'l' => __DIR__ . DS . 'lib' . DS . 'l.php', + 'media' => __DIR__ . DS . 'lib' . DS . 'media.php', + 'obj' => __DIR__ . DS . 'lib' . DS . 'obj.php', + 'pagination' => __DIR__ . DS . 'lib' . DS . 'pagination.php', + 'password' => __DIR__ . DS . 'lib' . DS . 'password.php', + 'r' => __DIR__ . DS . 'lib' . DS . 'r.php', + 'redirect' => __DIR__ . DS . 'lib' . DS . 'redirect.php', + 'remote' => __DIR__ . DS . 'lib' . DS . 'remote.php', + 'response' => __DIR__ . DS . 'lib' . DS . 'response.php', + 'router' => __DIR__ . DS . 'lib' . DS . 'router.php', + 's' => __DIR__ . DS . 'lib' . DS . 's.php', + 'server' => __DIR__ . DS . 'lib' . DS . 'server.php', + 'silo' => __DIR__ . DS . 'lib' . DS . 'silo.php', + 'sql' => __DIR__ . DS . 'lib' . DS . 'sql.php', + 'str' => __DIR__ . DS . 'lib' . DS . 'str.php', + 'system' => __DIR__ . DS . 'lib' . DS . 'system.php', + 'thumb' => __DIR__ . DS . 'lib' . DS . 'thumb.php', + 'timer' => __DIR__ . DS . 'lib' . DS . 'timer.php', + 'toolkit' => __DIR__ . DS . 'lib' . DS . 'toolkit.php', + 'tpl' => __DIR__ . DS . 'lib' . DS . 'tpl.php', + 'upload' => __DIR__ . DS . 'lib' . DS . 'upload.php', + 'url' => __DIR__ . DS . 'lib' . DS . 'url.php', + 'v' => __DIR__ . DS . 'lib' . DS . 'v.php', + 'visitor' => __DIR__ . DS . 'lib' . DS . 'visitor.php', + 'xml' => __DIR__ . DS . 'lib' . DS . 'xml.php', + 'yaml' => __DIR__ . DS . 'lib' . DS . 'yaml.php', + + // vendors + 'spyc' => __DIR__ . DS . 'vendors' . DS . 'yaml' . DS . 'yaml.php', + 'abeautifulsite\\simpleimage' => __DIR__ . DS . 'vendors' . DS . 'abeautifulsite' . DS . 'SimpleImage.php', + 'mimereader' => __DIR__ . DS . 'vendors' . DS . 'mimereader' . DS . 'mimereader.php', + +)); + +// load all helpers +include(__DIR__ . DS . 'helpers.php'); \ No newline at end of file diff --git a/kirby/toolkit/helpers.php b/kirby/toolkit/helpers.php new file mode 100644 index 0000000..cf39329 --- /dev/null +++ b/kirby/toolkit/helpers.php @@ -0,0 +1,353 @@ +' . print_r($variable, true) . ''; + } + if($echo === true) echo $output; + return $output; +} + +/** + * Generates a single attribute or a list of attributes + * + * @see html::attr(); + * @param string $name mixed string: a single attribute with that name will be generated. array: a list of attributes will be generated. Don't pass a second argument in that case. + * @param string $value if used for a single attribute, pass the content for the attribute here + * @return string the generated html + */ +function attr($name, $value = null) { + return html::attr($name, $value); +} + +/** + * Creates safe html by encoding special characters + * + * @param string $text unencoded text + * @param bool $keepTags + * @return string + */ +function html($text, $keepTags = true) { + return html::encode($text, $keepTags); +} + +/** + * Shortcut for html() + * + * @see html() + * @param $text + * @param bool $keepTags + * @return string + */ +function h($text, $keepTags = true) { + return html::encode($text, $keepTags); +} + +/** + * Shortcut for xml::encode() + * + * @param $text + * @return string + */ +function xml($text) { + return xml::encode($text); +} + +/** + * Escape context specific output + * + * @param string $string Untrusted data + * @param string $context Location of output + * @param boolean $strict Whether to escape an extended set of characters (HTML attributes only) + * @return string Escaped data + */ +function esc($string, $context = 'html', $strict = false) { + if (method_exists('escape', $context)) { + return escape::$context($string, $strict); + } +} + +/** + * The widont function makes sure that there are no + * typographical widows at the end of a paragraph – + * that's a single word in the last line + * + * @param string $string + * @return string + */ +function widont($string = '') { + return str::widont($string); +} + +/** + * Convert a text to multiline text + * + * @param string $text + * @return string + */ +function multiline($text) { + return nl2br(html($text)); +} + +/** + * Returns the memory usage in a readable format + * + * @return string + */ +function memory() { + return f::niceSize(memory_get_usage()); +} + +/** + * Determines the size/length of numbers, strings, arrays and files + * + * @param mixed $value + * @return int + */ +function size($value) { + if(is_numeric($value)) return $value; + if(is_string($value)) return str::length(trim($value)); + if(is_array($value)) return count($value); + if(f::exists($value)) return f::size($value) / 1024; +} + +/** + * Generates a gravatar image link + * + * @param string $email + * @param int $size + * @param string $default + * @return string + */ +function gravatar($email, $size = 256, $default = 'mm') { + return 'https://gravatar.com/avatar/' . md5(strtolower(trim($email))) . '?d=' . urlencode($default) . '&s=' . $size; +} + +/** + * Checks / returns a csrf token + * + * @param string $check Pass a token here to compare it to the one in the session + * @return mixed Either the token or a boolean check result + */ +function csrf($check = null) { + + // make sure a session is started + s::start(); + + if(is_null($check)) { + $token = str::random(64); + s::set('csrf', $token); + return $token; + } + + return ($check === s::get('csrf')) ? true : false; + +} + +/** + * Facepalm typo alias + * @see csrf() + */ +function csfr($check = null) { + return csrf($check); +} + +/** + * Shortcut for call_user_func_array with a better handling of arguments + * + * @param mixed $function + * @param mixed $arguments + * @return mixed + */ +function call($function, $arguments = array()) { + if(!is_callable($function)) return false; + if(!is_array($arguments)) $arguments = array($arguments); + return call_user_func_array($function, $arguments); +} + +/** + * Parses yaml structured text + * + * @param $string + * @return array + */ +function yaml($string) { + return yaml::decode($string); +} + +/** + * Simple email sender helper + * + * @param array $params + * @return Email + */ +function email($params = array()) { + return new Email($params); +} + +/** + * Shortcut for the upload class + * + * @param $to + * @param array $params + * @return Upload + */ +function upload($to, $params = array()) { + return new Upload($to, $params); +} + +/** + * Checks for invalid data + * + * @param array $data + * @param array $rules + * @param array $messages + * @return mixed + */ +function invalid($data, $rules, $messages = array()) { + $errors = array(); + foreach($rules as $field => $validations) { + foreach($validations as $method => $options) { + if(is_numeric($method)) $method = $options; + if($method == 'required') { + if(!isset($data[$field]) || (empty($data[$field]) && $data[$field] !== 0)) { + $errors[$field] = a::get($messages, $field, $field); + } + } else if(!empty($data[$field]) || $data[$field] === 0) { + if(!is_array($options)) $options = array($options); + array_unshift($options, a::get($data, $field)); + if(!call(array('v', $method), $options)) { + $errors[$field] = a::get($messages, $field, $field); + } + } + } + } + return array_unique($errors); +} + + +/** + * Shortcut for the language variable getter + * + * @param string $key + * @param mixed $default + * @return string + */ +function l($key, $default = null) { + return l::get($key, $default); +} + +/** + * @param $tag + * @param bool $html + * @param array $attr + * @return Brick + */ +function brick($tag, $html = false, $attr = array()) { + return new Brick($tag, $html, $attr); +} diff --git a/kirby/toolkit/lib/a.php b/kirby/toolkit/lib/a.php new file mode 100644 index 0000000..309649f --- /dev/null +++ b/kirby/toolkit/lib/a.php @@ -0,0 +1,495 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class A { + + /** + * Gets an element of an array by key + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * echo a::get($array, 'cat'); + * // output: 'miao' + * + * echo a::get($array, 'elephant', 'shut up'); + * // output: 'shut up' + * + * $catAndDog = a::get(array('cat', 'dog')); + * // result: array( + * // 'cat' => 'miao', + * // 'dog' => 'wuff' + * // ); + * + * + * + * @param array $array The source array + * @param mixed $key The key to look for + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + public static function get($array, $key, $default = null) { + + // get an array of keys + if(is_array($key)) { + $result = array(); + foreach($key as $k) $result[$k] = static::get($array, $k); + return $result; + + // get a single + } else if(isset($array[$key])) { + return $array[$key]; + + // return the entire array if the key is null + } else if(is_null($key)) { + return $array; + + // get the default value if nothing else worked out + } else { + return $default; + } + + } + + /** + * Shows an entire array or object in a human readable way + * This is perfect for debugging + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * a::show($array); + * + * // output: + * // Array + * // ( + * // [cat] => miao + * // [dog] => wuff + * // [bird] => tweet + * // ) + * + * + * + * @param array $array The source array + * @param boolean $echo By default the result will be echoed instantly. You can switch that off here. + * @return mixed If echo is false, this will return the generated array output. + */ + public static function show($array, $echo = true) { + return dump($array, $echo); + } + + /** + * Converts an array to a JSON string + * It's basically a shortcut for json_encode() + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * echo a::json($array); + * // output: {"cat":"miao","dog":"wuff","bird":"tweet"} + * + * + * + * @param array $array The source array + * @return string The JSON string + */ + public static function json($array) { + return json_encode((array)$array); + } + + /** + * Converts an array to a XML string + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * echo a::xml($array, 'animals'); + * // output: + * // + * // miao + * // wuff + * // tweet + * // + * + * + * + * @param array $array The source array + * @param string $tag The name of the root element + * @param boolean $head Include the xml declaration head or not + * @param string $charset The charset, which should be used for the header + * @param int $level The indendation level + * @return string The XML string + */ + public static function xml($array, $tag = 'root', $head = true, $charset = 'utf-8', $tab = ' ', $level = 0) { + return xml::create($array, $tag, $head, $charset, $tab, $level); + } + + /** + * Extracts a single column from an array + * + * + * + * $array[0] = array( + * 'id' => 1, + * 'username' => 'bastian', + * ); + * + * $array[1] = array( + * 'id' => 2, + * 'username' => 'peter', + * ); + * + * $array[3] = array( + * 'id' => 3, + * 'username' => 'john', + * ); + * + * $extract = a::extract($array, 'username'); + * + * // result: array( + * // 'bastian', + * // 'peter', + * // 'john' + * // ); + * + * + * + * @param array $array The source array + * @param string $key The key name of the column to extract + * @return array The result array with all values from that column. + */ + public static function extract($array, $key) { + $output = array(); + foreach($array AS $a) if(isset($a[$key])) $output[] = $a[ $key ]; + return $output; + } + + /** + * Shuffles an array and keeps the keys + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * $shuffled = a::shuffle($array); + * // output: array( + * // 'dog' => 'wuff', + * // 'cat' => 'miao', + * // 'bird' => 'tweet' + * // ); + * + * + * + * @param array $array The source array + * @return array The shuffled result array + */ + public static function shuffle($array) { + + $keys = array_keys($array); + $new = array(); + + shuffle($keys); + + // resort the array + foreach($keys as $key) $new[$key] = $array[$key]; + return $new; + + } + + /** + * Returns the first element of an array + * + * I always have to lookup the names of that function + * so I decided to make this shortcut which is + * easier to remember. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $first = a::first($array); + * // first: 'cat' + * + * + * + * @param array $array The source array + * @return mixed The first element + */ + public static function first($array) { + return array_shift($array); + } + + /** + * Returns the last element of an array + * + * I always have to lookup the names of that function + * so I decided to make this shortcut which is + * easier to remember. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $last = a::last($array); + * // first: 'bird' + * + * + * + * @param array $array The source array + * @return mixed The last element + */ + public static function last($array) { + return array_pop($array); + } + + /** + * Fills an array up with additional elements to certain amount. + * + * + * + * $array = array( + * 'cat', + * 'dog', + * 'bird', + * ); + * + * $result = a::fill($array, 5, 'elephant'); + * + * // result: array( + * // 'cat', + * // 'dog', + * // 'bird', + * // 'elephant', + * // 'elephant', + * // ); + * + * + * + * @param array $array The source array + * @param int $limit The number of elements the array should contain after filling it up. + * @param mixed $fill The element, which should be used to fill the array + * @return array The filled-up result array + */ + public static function fill($array, $limit, $fill='placeholder') { + if(count($array) < $limit) { + $diff = $limit-count($array); + for($x=0; $x<$diff; $x++) $array[] = $fill; + } + return $array; + } + + /** + * Checks for missing elements in an array + * + * This is very handy to check for missing + * user values in a request for example. + * + * + * + * $array = array( + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ); + * + * $required = array('cat', 'elephant'); + * + * $missng = a::missing($array, $required); + * // missing: array( + * // 'elephant' + * // ); + * + * + * + * @param array $array The source array + * @param array $required An array of required keys + * @return array An array of missing fields. If this is empty, nothing is missing. + */ + public static function missing($array, $required=array()) { + $missing = array(); + foreach($required AS $r) { + if(empty($array[$r])) $missing[] = $r; + } + return $missing; + } + + /** + * Sorts a multi-dimensional array by a certain column + * + * + * + * $array[0] = array( + * 'id' => 1, + * 'username' => 'bastian', + * ); + * + * $array[1] = array( + * 'id' => 2, + * 'username' => 'peter', + * ); + * + * $array[3] = array( + * 'id' => 3, + * 'username' => 'john', + * ); + * + * $sorted = a::sort($array, 'username ASC'); + * // Array + * // ( + * // [0] => Array + * // ( + * // [id] => 1 + * // [username] => bastian + * // ) + * // [1] => Array + * // ( + * // [id] => 3 + * // [username] => john + * // ) + * // [2] => Array + * // ( + * // [id] => 2 + * // [username] => peter + * // ) + * // ) + * + * + * + * @param array $array The source array + * @param string $field The name of the column + * @param string $direction desc (descending) or asc (ascending) + * @param const $method A PHP sort method flag or 'natural' for natural sorting, which is not supported in PHP by sort flags + * @return array The sorted array + */ + public static function sort($array, $field, $direction = 'desc', $method = SORT_REGULAR) { + + $direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC; + $helper = array(); + $result = array(); + + // build the helper array + foreach($array as $key => $row) $helper[$key] = $row[$field]; + + // natural sorting + if($method === SORT_NATURAL) { + natsort($helper); + if($direction === SORT_DESC) $helper = array_reverse($helper); + } else if($direction === SORT_DESC) { + arsort($helper, $method); + } else { + asort($helper, $method); + } + + // rebuild the original array + foreach($helper as $key => $val) $result[$key] = $array[$key]; + + return $result; + + } + + /** + * Checks wether an array is associative or not (experimental) + * + * @param array $array The array to analyze + * @return boolean true: The array is associative false: It's not + */ + public static function isAssociative($array) { + return !ctype_digit(implode(NULL, array_keys($array))); + } + + /** + * Returns the average value of an array + * + * @param array $array The source array + * @param int $decimals The number of decimals to return + * @return int The average value + */ + public static function average($array, $decimals = 0) { + return round(array_sum($array), $decimals) / sizeof($array); + } + + /** + * Merges arrays recursively + * + * @param array $array1 + * @param array $array2 + * @return array + */ + public static function merge($array1, $array2) { + $merged = $array1; + foreach($array2 as $key => $value) { + if(is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { + $merged[$key] = static::merge($merged[$key], $value); + } else { + $merged[$key] = $value; + } + } + return $merged; + } + + /** + * Update an array with a second array + * The second array can contain callbacks as values, + * which will get the original values as argument + * + * @param array $array + * @param array $update + */ + public static function update($array, $update) { + + foreach($update as $key => $value) { + if(is_a($value, 'Closure')) { + $array[$key] = call($value, static::get($array, $key)); + } else { + $array[$key] = $value; + } + } + + return $array; + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/bitmask.php b/kirby/toolkit/lib/bitmask.php new file mode 100644 index 0000000..db5e7f7 --- /dev/null +++ b/kirby/toolkit/lib/bitmask.php @@ -0,0 +1,75 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Bitmask { + + /** + * Checks if a value can be used as bitmask value (checks for a power of two) + * + * @param mixed $value The value to check for + * @return boolean + */ + public static function validValue($value) { + return is_int($value) && ($value & ($value - 1)) == 0; + } + + /** + * Checks if a bitmask includes a value + * + * @param int $value The value to check for + * @param int $bitmask The bitmask to check in + * @return boolean + */ + public static function includes($value, $bitmask) { + if(!static::validValue($value)) return false; + + return ($bitmask & $value) !== 0; + } + + /** + * Adds a value to a bitmask + * + * @param int $value The value to add + * @param int $bitmask The bitmask to add the value to + * @return int + */ + public static function add($value, $bitmask) { + if(!static::validValue($value)) { + throw new Exception('The value "' . $value . '" is not appropriate for usage in bitmasks.'); + } + + // check if the bitmask already includes the value + if(static::includes($value, $bitmask)) return $bitmask; + + return $bitmask | $value; + } + + /** + * Removes a value from a bitmask + * + * @param int $value The value to remove + * @param int $bitmask The bitmask to remove the value from + * @return int + */ + public static function remove($value, $bitmask) { + if(!static::validValue($value)) { + throw new Exception('The value "' . $value . '" is not appropriate for usage in bitmasks.'); + } + + // check if the bitmask even includes the value + if(!static::includes($value, $bitmask)) return $bitmask; + + return $bitmask ^ $value; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/brick.php b/kirby/toolkit/lib/brick.php new file mode 100644 index 0000000..e83e97f --- /dev/null +++ b/kirby/toolkit/lib/brick.php @@ -0,0 +1,204 @@ +tag($tag); + $this->html($html); + $this->attr($attr); + + } + + public function __set($attr, $value) { + $this->attr($attr, $value); + } + + public function on($event, $callback) { + if(!isset($this->events[$event])) $this->events[$event] = array(); + $this->events[$event][] = $callback; + return $this; + } + + public function trigger($event, $args = array()) { + if(isset($this->events[$event])) { + array_unshift($args, $this); + foreach($this->events[$event] as $e) { + call_user_func_array($e, $args); + } + } + } + + public function tag($tag = null) { + if(is_null($tag)) return $this->tag; + $this->tag = $tag; + return $this; + } + + public function attr($key = null, $value = null) { + if(is_null($key)) { + return $this->attr; + } else if(is_array($key)) { + foreach($key as $k => $v) { + $this->attr($k, $v); + } + return $this; + } else if(is_null($value)) { + return a::get($this->attr, $key); + } else if($key == 'class') { + $this->addClass($value); + return $this; + } else { + $this->attr[$key] = $value; + return $this; + } + } + + public function data($key = null, $value = null) { + if(is_null($key)) { + $data = array(); + foreach($this->attr as $key => $val) { + if(str::startsWith($key, 'data-')) { + $data[$key] = $val; + } + } + return $data; + } else if(is_array($key)) { + foreach($key as $k => $v) { + $this->data($k, $v); + } + return $this; + } else if(is_null($value)) { + return a::get($this->attr, 'data-' . $key); + } else { + $this->attr['data-' . $key] = $value; + return $this; + } + } + + + public function removeAttr($key) { + unset($this->attr[$key]); + } + + public function classNames() { + + if(!isset($this->attr['class'])) { + $this->attr['class'] = array(); + } else if(is_string($this->attr['class'])) { + $raw = $this->attr['class']; + $this->attr['class'] = array(); + $this->addClass($raw); + } + + return $this->attr['class']; + + } + + public function val($value = null) { + return $this->attr('value', $value); + } + + public function addClass($class) { + + $classNames = $this->classNames(); + $classIndex = array_map('strtolower', $classNames); + + foreach(str::split($class, ' ') as $c) { + if(!in_array(strtolower($c), $classIndex)) { + $classNames[] = $c; + } + } + + $this->attr['class'] = $classNames; + + return $this; + + } + + public function removeClass($class) { + + $classNames = $this->classNames(); + + foreach(str::split($class, ' ') as $c) { + $classNames = array_filter($classNames, function($e) use($c) { + return (strtolower($e) !== strtolower($c)); + }); + } + + $this->attr['class'] = $classNames; + + return $this; + + } + + public function replaceClass($classA, $classB) { + return $this->removeClass($classA)->addClass($classB); + } + + public function text($text = null) { + if(is_null($text)) return trim(strip_tags($this->html)); + $this->html = html($text, false); + return $this; + } + + public function html($html = null) { + if(is_null($html)) { + return $this->html = $this->isVoid() ? null : $this->html; + } + $this->html = $html; + return $this; + } + + public function prepend($html) { + if(is_callable($html)) $html = $html(); + $this->html = $html . $this->html; + return $this; + } + + public function append($html) { + if(is_callable($html)) $html = $html(); + $this->html = $this->html . $html; + return $this; + } + + public function isVoid() { + return html::isVoid($this->tag()); + } + + public function toString() { + $this->attr['class'] = implode(' ', $this->classNames()); + return html::tag($this->tag(), $this->html(), $this->attr()); + } + + public function __toString() { + try { + return $this->toString(); + } catch(Exception $e) { + return 'Error: ' . $e->getMessage(); + } + } + + public static function make($id, $callback) { + static::$bricks[$id] = $callback; + } + + public static function get($id) { + if(!isset(static::$bricks[$id])) return false; + $args = array_slice(func_get_args(), 1); + return call_user_func_array(static::$bricks[$id], $args); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/c.php b/kirby/toolkit/lib/c.php new file mode 100644 index 0000000..cdb9b73 --- /dev/null +++ b/kirby/toolkit/lib/c.php @@ -0,0 +1,17 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class C extends Silo { + public static $data = array(); +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache.php b/kirby/toolkit/lib/cache.php new file mode 100644 index 0000000..6f449e4 --- /dev/null +++ b/kirby/toolkit/lib/cache.php @@ -0,0 +1,59 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cache { + + const ERROR_INVALID_DRIVER = 0; + const ERROR_INVALID_DRIVER_INSTANCE = 1; + const ERROR_UNKNOWN_METHOD = 2; + + public static $driver = null; + + /** + * Setup simplifier for the current driver + * + * @param string $driver + * @param mixed $args + * @return Cache\Driver + */ + public static function setup($driver, $args = null) { + $ref = new ReflectionClass('Cache\\Driver\\' . $driver); + return static::$driver = $ref->newInstanceArgs(array($args)); + } + + /** + * Accessor for all static driver methods + * + * @param string $method + * @param mixed $args + * @return mixed + */ + public static function __callStatic($method, $args) { + + if(is_null(static::$driver)) { + throw new Error('Please define a cache driver', static::ERROR_INVALID_DRIVER); + } + + if(!is_a(static::$driver, 'Cache\\Driver')) { + throw new Error('The cache driver must be an instance of the Cache\\Driver class', static::ERROR_INVALID_DRIVER_INSTANCE); + } + + if(method_exists(static::$driver, $method)) { + return call(array(static::$driver, $method), $args); + } else { + throw new Error('Invalid cache method: ' . $method, static::ERROR_UNKNOWN_METHOD); + } + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver.php b/kirby/toolkit/lib/cache/driver.php new file mode 100755 index 0000000..be6f4ae --- /dev/null +++ b/kirby/toolkit/lib/cache/driver.php @@ -0,0 +1,190 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +abstract class Driver { + + // stores all options for the driver + protected $options = array(); + + /** + * Set all parameters which are needed to connect to the cache storage + * + * @param array $params + */ + public function __construct($params = array()) {} + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public abstract function set($key, $value, $minutes = null); + + /** + * Private method to retrieve the cache value + * This needs to be defined by the driver + * + * @param string $key + * @return object Value + */ + public abstract function retrieve($key); + + /** + * Get an item from the cache. + * + * + * // Get an item from the cache driver + * $value = Cache::get('value'); + * + * // Return a default value if the requested item isn't cached + * $value = Cache::get('value', 'default value'); + * + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) { + + // get the Value + $value = $this->retrieve($key); + + // check for a valid cache value + if(!is_a($value, 'Cache\\Value')) return $default; + + // remove the item if it is expired + if(time() > $value->expires()) { + $this->remove($key); + return $default; + } + + // get the pure value + $cache = $value->value(); + + // return the cache value or the default + return (!is_null($cache)) ? $cache : $default; + + } + + /** + * Calculates the expiration timestamp + * + * @param int $minutes + * @return int + */ + protected function expiration($minutes = null) { + // keep forever if minutes are not defined + if(is_null($minutes)) $minutes = 2628000; + + // calculate the time + return time() + ($minutes * 60); + } + + /** + * Checks when an item in the cache expires + * + * @param string $key + * @return mixed + */ + public function expires($key) { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if(!is_a($value, 'Cache\\Value')) return false; + + // return the expires timestamp + return $value->expires(); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return int + */ + public function expired($key) { + return $this->expires($key) <= time(); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return mixed + */ + public function created($key) { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if(!is_a($value, 'Cache\\Value')) return false; + + // return the expires timestamp + return $value->created(); + } + + /** + * Alternate version for cache::created($key) + */ + public function modified($key) { + return static::created($key); + } + + /** + * An array with value, created timestamp and expires timestamp + * + * @param mixed $value The value, which should be cached + * @param int $minutes The number of minutes before expiration + * @return array + */ + protected function value($value, $minutes) { + return new Value($value, $minutes); + } + + /** + * Determine if an item exists in the cache. + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return !$this->expired($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public abstract function remove($key); + + /** + * Flush the entire cache + * + * @return boolean + */ + public abstract function flush(); + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/apc.php b/kirby/toolkit/lib/cache/driver/apc.php new file mode 100644 index 0000000..ec7603b --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/apc.php @@ -0,0 +1,74 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Apc extends Driver { + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function set($key, $value, $minutes = null) { + return apc_store($key, $this->value($value, $minutes), $this->expiration($minutes)); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve($key) { + return apc_fetch($key); + } + + /** + * Checks if the current key exists in cache + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return apc_exists($key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return apc_delete($key); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return apc_clear_cache('user'); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/file.php b/kirby/toolkit/lib/cache/driver/file.php new file mode 100644 index 0000000..7563bd4 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/file.php @@ -0,0 +1,121 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class File extends Driver { + + const ERROR_MISSING_CACHE_DIRECTORY = 0; + + /** + * Set all parameters which are needed for the file cache + * see defaults for available parameters + * + * @param array $params + */ + public function __construct($params = array()) { + + if(is_string($params)) { + $params = array('root' => $params); + } + + $defaults = array( + 'root' => null, + 'extension' => null + ); + + $this->options = array_merge($defaults, $params); + + // check for a valid cache directory + if(!is_dir($this->options['root'])) { + throw new Error('The cache directory does not exist', static::ERROR_MISSING_CACHE_DIRECTORY); + } + + } + + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file($key) { + return $this->options['root'] . DS . $key . r($this->options['extension'], '.' . $this->options['extension']); + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function set($key, $value, $minutes = null) { + return f::write($this->file($key), serialize($this->value($value, $minutes))); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + // unserialized value array (see $this->value()) + return unserialize(f::read($this->file($key))); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created($key) { + // use the modification timestamp + // as indicator when the cache has been created/overwritten + clearstatcache(); + // get the file for this cache key + $file = $this->file($key); + return file_exists($file) ? filemtime($this->file($key)) : 0; + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return f::remove($this->file($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return dir::clean($this->options['root']); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/memcached.php b/kirby/toolkit/lib/cache/driver/memcached.php new file mode 100644 index 0000000..8320467 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/memcached.php @@ -0,0 +1,128 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Memcached extends Driver { + + // store for the memache connection + protected $connection = null; + + /** + * Set all parameters which are needed for the memcache client + * see defaults for available parameters + * + * @param array $params + */ + public function __construct($params = array()) { + + $defaults = array( + 'host' => 'localhost', + 'port' => 11211, + 'prefix' => null, + ); + + $this->options = array_merge($defaults, (array)$params); + $this->connection = new \Memcached(); + $this->connection->addServer($this->options['host'], $this->options['port']); + + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function set($key, $value, $minutes = null) { + return $this->connection->set($this->key($key), $this->value($value, $minutes), $this->expiration($minutes)); + } + + /** + * Returns the full keyname + * including the prefix (if set) + * + * @param string $key + * @return string + */ + public function key($key) { + return $this->options['prefix'] . $key; + } + + /** + * Retrieve the CacheValue object from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + return $this->connection->get($this->key($key)); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return $this->connection->delete($this->key($key)); + } + + /** + * Checks when an item in the cache expires + * + * @param string $key + * @return int + */ + public function expires($key) { + return parent::expires($this->key($key)); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return int + */ + public function expired($key) { + return parent::expired($this->key($key)); + } + + /** + * Checks when the cache has been created + * + * @param string $key + * @return int + */ + public function created($key) { + return parent::created($this->key($key)); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return $this->connection->flush(); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/mock.php b/kirby/toolkit/lib/cache/driver/mock.php new file mode 100644 index 0000000..0c95484 --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/mock.php @@ -0,0 +1,74 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Mock extends Driver { + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function set($key, $value, $minutes = null) { + return true; + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return mixed + */ + public function retrieve($key) { + return null; + } + + /** + * Checks if the current key exists in cache + * + * @param string $key + * @return boolean + */ + public function exists($key) { + return null; + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + return true; + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + return true; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/driver/session.php b/kirby/toolkit/lib/cache/driver/session.php new file mode 100644 index 0000000..5971c8f --- /dev/null +++ b/kirby/toolkit/lib/cache/driver/session.php @@ -0,0 +1,78 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Session extends Driver { + + /** + * Make sure the session is started within the constructor + */ + public function __construct() { + s::start(); + if(!isset($_SESSION['_cache'])) { + $_SESSION['_cache'] = array(); + } + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function set($key, $value, $minutes = null) { + return $_SESSION['_cache'][$key] = $this->value($value, $minutes); + } + + /** + * Retrieve an item from the cache. + * + * @param string $key + * @return object CacheValue + */ + public function retrieve($key) { + return a::get($_SESSION['_cache'], $key); + } + + /** + * Remove an item from the cache + * + * @param string $key + * @return boolean + */ + public function remove($key) { + unset($_SESSION['_cache'][$key]); + } + + /** + * Flush the entire cache directory + * + * @return boolean + */ + public function flush() { + $_SESSION['_cache'] = array(); + return true; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/cache/value.php b/kirby/toolkit/lib/cache/value.php new file mode 100644 index 0000000..f2bfc8a --- /dev/null +++ b/kirby/toolkit/lib/cache/value.php @@ -0,0 +1,75 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Value { + + // the cached value + protected $value; + + // the expiration timestamp + protected $expires; + + // the creation timestamp + protected $created; + + /** + * Constructor + * + * @param mixed $value + * @param int $minutes the number of minutes until the value expires + */ + public function __construct($value, $minutes = null) { + + // keep forever if minutes are not defined + if(is_null($minutes)) $minutes = 2628000; + + // take the current time + $time = time(); + + $this->value = $value; + $this->expires = $time + ($minutes * 60); + $this->created = $time; + + } + + /** + * Returns the value + * + * @return mixed + */ + public function value() { + return $this->value; + } + + /** + * Returns the expiration date as UNIX timestamp + * + * @return int + */ + public function expires() { + return $this->expires; + } + + /** + * Returns the creation date as UNIX timestamp + * + * @return int + */ + public function created() { + return $this->created; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/collection.php b/kirby/toolkit/lib/collection.php new file mode 100644 index 0000000..79b88af --- /dev/null +++ b/kirby/toolkit/lib/collection.php @@ -0,0 +1,652 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Collection extends I { + + public static $filters = array(); + + protected $pagination; + + /** + * Returns a slice of the collection + * + * @param int $offset The optional index to start the slice from + * @param int $limit The optional number of elements to return + * @return Collection + */ + public function slice($offset = null, $limit = null) { + if($offset === null && $limit === null) return $this; + $collection = clone $this; + $collection->data = array_slice($collection->data, $offset, $limit); + return $collection; + } + + /** + * Returns a new combined collection + * + * @return Collection + */ + + public function merge($collection2) { + $collection = clone $this; + $collection->data = a::merge($collection->data, $collection2->data); + return $collection; + } + + /** + * Returns a new collection with a limited number of elements + * + * @param int $limit The number of elements to return + * @return Collection + */ + public function limit($limit) { + return $this->slice(0, $limit); + } + + /** + * Returns a new collection starting from the given offset + * + * @param int $offset The index to start from + * @return Collection + */ + public function offset($offset) { + return $this->slice($offset); + } + + /** + * Returns the array in reverse order + * + * @return Collection + */ + public function flip() { + $collection = clone $this; + $collection->data = array_reverse($collection->data, true); + return $collection; + } + + /** + * Counts all elements in the array + * + * @return int + */ + public function count() { + return count($this->data); + } + + /** + * Returns the first element from the array + * + * @return mixed + */ + public function first() { + $array = $this->data; + return array_shift($array); + } + + /** + * Checks if an element is in the collection by key. + * + * @param string $key + * @return boolean + */ + public function has($key) { + return isset($this->data[$key]); + } + + /** + * Returns the last element from the array + * + * @return mixed + */ + public function last() { + $array = $this->data; + return array_pop($array); + } + + /** + * Returns the nth element from the array + * + * @return mixed + */ + public function nth($n) { + $array = array_values($this->data); + return (isset($array[$n])) ? $array[$n] : false; + } + + /** + * Converts the current object into an array + * + * @return array + */ + public function toArray($callback = null) { + if(is_null($callback)) return $this->data; + return array_map($callback, $this->data); + } + + /** + * Converts the current object into a json string + * + * @return string + */ + public function toJson() { + return json_encode($this->data); + } + + /** + * Appends an element to the data array + * + * @param string $key + * @param mixed $object + * @return Collection + */ + public function append($key, $object) { + $this->data = $this->data + array($key => $object); + return $this; + } + + /** + * Prepends an element to the data array + * + * @param string $key + * @param mixed $object + * @return Collection + */ + public function prepend($key, $object) { + $this->data = array($key => $object) + $this->data; + return $this; + } + + /** + * Returns a new collection without the given element(s) + * + * @param args any number of keys, passed as individual arguments + * @return Collection + */ + public function not() { + $collection = clone $this; + foreach(func_get_args() as $kill) { + unset($collection->data[$kill]); + } + return $collection; + } + + /** + * Returns a new collection without the given element(s) + * + * @param args any number of keys, passed as individual arguments + * @return Collection + */ + public function without() { + return call_user_func_array(array($this, 'not'), func_get_args()); + } + + /** + * Shuffle all elements in the array + * + * @return object a new shuffled collection + */ + public function shuffle() { + $collection = clone $this; + $keys = array_keys($collection->data); + shuffle($keys); + $collection->data = array_merge(array_flip($keys), $collection->data); + return $collection; + } + + /** + * Returns an array of all keys in the collection + * + * @return array + */ + public function keys() { + return array_keys($this->data); + } + + /** + * Tries to find the key for the given element + * + * @param mixed $needle the element to search for + * @return mixed the name of the key or false + */ + public function keyOf($needle) { + return array_search($needle, $this->data); + } + + /** + * Tries to find the index number for the given element + * + * @param mixed $needle the element to search for + * @return mixed the name of the key or false + */ + public function indexOf($needle) { + return array_search($needle, array_values($this->data)); + } + + /** + * Filter the elements in the array by a callback function + * + * @param func $callback the callback function + * @return Collection + */ + public function filter($callback) { + $collection = clone $this; + $collection->data = array_filter($collection->data, $callback); + return $collection; + } + + /** + * Find a single item by a key and value pair + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function findBy($key, $value) { + foreach($this->data as $item) { + if($this->extractValue($item, $key) == $value) return $item; + } + } + + /** + * Filters the current collection by a field, operator and search value + * + * @return Collection + */ + public function filterBy() { + + $args = func_get_args(); + $operator = '=='; + $field = @$args[0]; + $value = @$args[1]; + $split = @$args[2]; + $collection = clone $this; + + if(is_string($value) && array_key_exists($value, static::$filters)) { + $operator = $value; + $value = @$args[2]; + $split = @$args[3]; + } + + if(is_object($value)) { + $value = (string)$value; + } + + if(array_key_exists($operator, static::$filters)) { + + $collection = call_user_func_array(static::$filters[$operator], array( + $collection, + $field, + $value, + $split + )); + + } + + return $collection; + + } + + /** + * Makes sure to provide a valid value for each filter method + * no matter if an object or an array is given + * + * @param mixed $item + * @param string $field + * @return mixed + */ + static public function extractValue($item, $field) { + if(is_array($item) && isset($item[$field])) { + return $item[$field]; + } else if(is_object($item)) { + return $item->$field(); + } else { + return false; + } + } + + /** + * Sorts the collection by any number of fields + * + * @return Collection + */ + public function sortBy() { + + $args = func_get_args(); + $collection = clone $this; + $array = $collection->data; + $params = array(); + + if(empty($array)) return $collection; + + foreach($args as $i => $param) { + if(is_string($param)) { + if(strtolower($param) === 'desc') { + ${"param_$i"} = SORT_DESC; + } else if(strtolower($param) === 'asc') { + ${"param_$i"} = SORT_ASC; + } else { + ${"param_$i"} = array(); + foreach($array as $index => $row) { + ${"param_$i"}[$index] = is_array($row) ? str::lower($row[$param]) : str::lower($row->$param()); + } + } + } else { + ${"param_$i"} = $args[$i]; + } + $params[] = &${"param_$i"}; + } + + $params[] = &$array; + + call_user_func_array('array_multisort', $params); + + $collection->data = $array; + + return $collection; + + } + + /** + * Add pagination + * + * @param int $limit the number of items per page + * @param array $options and optional array with options for the pagination class + * @return object a sliced set of data + */ + public function paginate($limit, $options = array()) { + + if(is_a($limit, 'Pagination')) { + $this->pagination = $limit; + return $this; + } + + $pagination = new Pagination($this->count(), $limit, $options); + $pages = $this->slice($pagination->offset(), $pagination->limit()); + $pages->pagination = $pagination; + + return $pages; + + } + + /** + * Get the previously added pagination object + * + * @return object + */ + public function pagination() { + return $this->pagination; + } + + /** + * Map a function to each item in the collection + * + * @param function $callback + * @return Collection + */ + public function map($callback) { + $this->data = array_map($callback, $this->data); + return $this; + } + + /** + * Extracts all values for a single field into + * a new array + * + * @param string $field + * @return array + */ + public function pluck($field, $split = null, $unique = false) { + + $result = array(); + + foreach($this->data as $item) { + $row = $this->extractValue($item, $field); + + if($split) { + $result = array_merge($result, str::split($row, $split)); + } else { + $result[] = $row; + } + + } + + if($unique) { + $result = array_unique($result); + } + + return array_values($result); + + } + + /** + * Groups the collection by a given callback + * + * @param callable $callback + * @return object A new collection with an item for each group and a subcollection in each group + */ + public function group($callback) { + + if (!is_callable($callback)) throw new Exception($callback . ' is not callable. Did you mean to use groupBy()?'); + + $groups = array(); + + foreach($this->data as $key => $item) { + + // get the value to group by + $value = call_user_func($callback, $item); + + // make sure that there's always a proper value to group by + if(!$value) throw new Exception('Invalid grouping value for key: ' . $key); + + // make sure we have a proper key for each group + if(is_array($value)) { + throw new Exception('You cannot group by arrays or objects'); + } else if(is_object($value)) { + if(!method_exists($value, '__toString')) { + throw new Exception('You cannot group by arrays or objects'); + } else { + $value = (string)$value; + } + } + + if(!isset($groups[$value])) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new static(array($key => $item)); + } else { + // add the item to an existing group + $groups[$value]->set($key, $item); + } + + } + + return new Collection($groups); + + } + + /** + * Groups the collection by a given field + * + * @param string $field + * @return object A new collection with an item for each group and a subcollection in each group + */ + public function groupBy($field, $i = true) { + + if (!is_string($field)) throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); + + return $this->group(function($item) use ($field, $i) { + + $value = $this->extractValue($item, $field); + + // ignore upper/lowercase for group names + return ($i == true) ? str::lower($value) : $value; + + }); + + } + + public function set($key, $value) { + if(is_array($key)) { + $this->data = array_merge($this->data, $key); + return $this; + } + $this->data[$key] = $value; + return $this; + } + + public function __set($key, $value) { + $this->set($key, $value); + } + + public function get($key, $default = null) { + if(isset($this->data[$key])) { + return $this->data[$key]; + } else { + $lowerkeys = array_change_key_case($this->data, CASE_LOWER); + if(isset($lowerkeys[strtolower($key)])) { + return $lowerkeys[$key]; + } else { + return $default; + } + } + } + + public function __get($key) { + return $this->get($key); + } + + public function __call($key, $arguments) { + return $this->get($key); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return implode('
', array_map(function($item) { + return (string)$item; + }, $this->data)); + } + +} + + +/** + * Add all available collection filters + * Those can be extended by creating your own: + * collection::$filters['your operator'] = function($collection, $field, $value, $split = false) { + * // your filter code + * }; + */ + +// take all matching elements +collection::$filters['=='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + if(!in_array($value, $values)) unset($collection->$key); + } else if(collection::extractValue($item, $field) != $value) { + unset($collection->$key); + } + + } + + return $collection; + +}; + +// take all elements that won't match +collection::$filters['!='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + if(in_array($value, $values)) unset($collection->$key); + } else if(collection::extractValue($item, $field) == $value) { + unset($collection->$key); + } + } + + return $collection; + +}; + +// take all elements that partly match +collection::$filters['*='] = function($collection, $field, $value, $split = false) { + + foreach($collection->data as $key => $item) { + if($split) { + $values = str::split((string)collection::extractValue($item, $field), $split); + foreach($values as $val) { + if(strpos($val, $value) === false) { + unset($collection->$key); + break; + } + } + } else if(strpos(collection::extractValue($item, $field), $value) === false) { + unset($collection->$key); + } + } + + return $collection; + +}; + +// greater than +collection::$filters['>'] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) > $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// greater and equals +collection::$filters['>='] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) >= $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// less than +collection::$filters['<'] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) < $value) continue; + unset($collection->$key); + } + + return $collection; + +}; + +// less and equals +collection::$filters['<='] = function($collection, $field, $value) { + + foreach($collection->data as $key => $item) { + if(collection::extractValue($item, $field) <= $value) continue; + unset($collection->$key); + } + + return $collection; + +}; diff --git a/kirby/toolkit/lib/cookie.php b/kirby/toolkit/lib/cookie.php new file mode 100644 index 0000000..f3b685a --- /dev/null +++ b/kirby/toolkit/lib/cookie.php @@ -0,0 +1,168 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cookie { + + // configuration + public static $salt = 'KirbyToolkitCookieSalt'; + + /** + * Set a new cookie + * + * + * + * cookie::set('mycookie', 'hello', 60); + * // expires in 1 hour + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param int $lifetime The number of minutes until the cookie expires + * @param string $path The path on the server to set the cookie for + * @param string $domain the domain + * @param boolean $secure only sets the cookie over https + * @param boolean $httpOnly avoids the cookie to be accessed via javascript + * @return boolean true: the cookie has been created, false: cookie creation failed + */ + public static function set($key, $value, $lifetime = 0, $path = '/', $domain = null, $secure = false, $httpOnly = true) { + + // convert array values to json + if(is_array($value)) $value = a::json($value); + + // hash the value + $value = static::hash($value) . '+' . $value; + + // store that thing in the cookie global + $_COOKIE[$key] = $value; + + // store the cookie + return setcookie($key, $value, static::lifetime($lifetime), $path, $domain, $secure, $httpOnly); + + } + + /** + * Calculates the lifetime for a cookie + * + * @return int + */ + public static function lifetime($minutes) { + return $minutes > 0 ? (time() + ($minutes * 60)) : 0; + } + + /** + * Stores a cookie forever + * + * + * + * cookie::forever('mycookie', 'hello'); + * // never expires + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param string $path The path on the server to set the cookie for + * @param string $domain the domain + * @param boolean $secure only sets the cookie over https + * @return boolean true: the cookie has been created, false: cookie creation failed + */ + public static function forever($key, $value, $path = '/', $domain = null, $secure = false) { + return static::set($key, $value, 2628000, $path, $domain, $secure); + } + + /** + * Get a cookie value + * + * + * + * cookie::get('mycookie', 'peter'); + * // sample output: 'hello' or if the cookie is not set 'peter' + * + * + * + * @param string $key The name of the cookie + * @param string $default The default value, which should be returned if the cookie has not been found + * @return mixed The found value + */ + public static function get($key = null, $default = null) { + if(is_null($key)) return $_COOKIE; + $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : null; + return empty($value) ? $default : static::parse($value); + } + + /** + * Checks if a cookie exists + * + * @return boolean + */ + public static function exists($key) { + return !is_null(static::get($key)); + } + + /** + * Creates a hash for the cookie value + * salted with the secret cookie salt string from the defaults + * + * @param string $value + * @return string + */ + protected static function hash($value) { + return sha1($value . static::$salt); + } + + /** + * Parses the hashed value from a cookie + * and tries to extract the value + * + * @param string $string + * @return mixed + */ + protected static function parse($string) { + + // extract hash and value + $parts = str::split($string, '+'); + $hash = a::first($parts); + $value = a::last($parts); + + // if the hash or the value is missing at all return null + if(empty($hash) || empty($value)) return null; + + // compare the extracted hash with the hashed value + if($hash !== static::hash($value)) return null; + + return $value; + + } + + /** + * Remove a cookie + * + * + * + * cookie::remove('mycookie'); + * // mycookie is now gone + * + * + * + * @param string $key The name of the cookie + * @return mixed true: the cookie has been removed, false: the cookie could not be removed + */ + public static function remove($key) { + if(isset($_COOKIE[$key])) { + unset($_COOKIE[$key]); + return setcookie($key, '', time() - 3600, '/'); + } + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/crypt.php b/kirby/toolkit/lib/crypt.php new file mode 100644 index 0000000..9399f7f --- /dev/null +++ b/kirby/toolkit/lib/crypt.php @@ -0,0 +1,86 @@ +, Arno Richter + * @link http://getkirby.com + * @copyright Bastian Allgeier, Arno Richter + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Crypt { + + // encryption salt - should be changed + public static $salt = '-'; + + // all available encryption modes + public static $encryption = array( + 'rijndael-128', + 'rijndael-256', + 'blowfish', + 'twofish', + 'des' + ); + + /** + * Encodes a string + * + * @param string $text + * @param string $key An optional encryption key + * @param string $mode Check out the $encryption array for available modes + * @return string + */ + public static function encode($text, $key = null, $mode = 'blowfish') { + + // check for mcrypt support + if(!function_exists('mcrypt_get_iv_size')) { + throw new Exception('The mcrypt extension is missing'); + } + + // all modes are lowercase so we try to avoid errors here + $mode = strtolower($mode); + + // check for a valid encryption mode + if(!in_array($mode, static::$encryption)) throw new Exception('Invalid encryption mode: ' . $mode); + + $size = mcrypt_get_iv_size($mode, MCRYPT_MODE_ECB); + $iv = mcrypt_create_iv($size, MCRYPT_RAND); + $result = mcrypt_encrypt($mode, static::$salt . $key, $text, MCRYPT_MODE_ECB, $iv); + + return trim($result); + + } + + /** + * Decodes a string + * + * @param string $text + * @param string $key An optional encryption key + * @param string $mode Check out the $encryption array for available modes + * @return string + */ + public static function decode($text, $key = null, $mode = 'blowfish') { + + // check for mcrypt support + if(!function_exists('mcrypt_get_iv_size')) { + throw new Exception('The mcrypt extension is missing'); + } + + // all modes are lowercase so we try to avoid errors here + $mode = strtolower($mode); + + // check for a valid encryption mode + if(!in_array($mode, static::$encryption)) throw new Exception('Invalid encryption mode: ' . $mode); + + $size = mcrypt_get_iv_size($mode, MCRYPT_MODE_ECB); + $iv = mcrypt_create_iv($size, MCRYPT_RAND); + $result = mcrypt_decrypt($mode, static::$salt . $key, $text, MCRYPT_MODE_ECB, $iv); + + return trim($result); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/data.php b/kirby/toolkit/lib/data.php new file mode 100644 index 0000000..4cab0f0 --- /dev/null +++ b/kirby/toolkit/lib/data.php @@ -0,0 +1,175 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Data { + + const ERROR_INVALID_ADAPTER = 0; + + public static $adapters = array(); + + public static function adapter($type) { + + if(isset(static::$adapters[$type])) return static::$adapters[$type]; + + foreach(static::$adapters as $adapter) { + if(is_array($adapter['extension']) && in_array($type, $adapter['extension'])) { + return $adapter; + } else if($adapter['extension'] == $type) { + return $adapter; + } + } + + throw new Error('Invalid adapter type', static::ERROR_INVALID_ADAPTER); + + } + + public static function encode($data, $type) { + $adapter = static::adapter($type); + return call_user_func($adapter['encode'], $data); + } + + public static function decode($data, $type) { + $adapter = static::adapter($type); + return call_user_func($adapter['decode'], $data); + } + + public static function read($file, $type = null) { + + // type autodetection + if(is_null($type)) $type = f::extension($file); + + // get the adapter + $adapter = static::adapter($type); + + if(isset($adapter['read'])) { + return call($adapter['read'], $file); + } else { + return data::decode(f::read($file), $type); + } + + + } + + public static function write($file, $data, $type = null) { + // type autodetection + if(is_null($type)) $type = f::extension($file); + return f::write($file, data::encode($data, $type)); + } + +} + + +/** + * Json adapter + */ +data::$adapters['json'] = array( + 'extension' => 'json', + 'encode' => function($data) { + return json_encode($data); + }, + 'decode' => function($string) { + return json_decode($string, true); + } +); + + +/** + * Kirby data adapter + */ +data::$adapters['kd'] = array( + 'extension' => array('md', 'txt'), + 'encode' => function($data) { + + $result = array(); + foreach($data AS $key => $value) { + $key = str::ucfirst(str::slug($key)); + + if(empty($key) || is_null($value)) continue; + + // avoid problems with arrays + if(is_array($value)) { + $value = ''; + } + + // escape accidental dividers within a field + $value = preg_replace('!(\n|^)----(.*?\R*)!', "$1\\----$2", $value); + + // multi-line content + if(preg_match('!\R!', $value, $matches)) { + $result[$key] = $key . ": \n\n" . trim($value); + // single-line content + } else { + $result[$key] = $key . ': ' . trim($value); + } + + } + return implode("\n\n----\n\n", $result); + + }, + 'decode' => function($string) { + + // remove BOM + $string = str_replace(BOM, '', $string); + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $string); + // start the data array + $data = array(); + + // loop through all fields and add them to the content + foreach($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(array('-', ' '), '_', strtolower(trim(substr($field, 0, $pos)))); + + // Don't add fields with empty keys + if(empty($key)) continue; + $data[$key] = trim(substr($field, $pos+1)); + + } + + return $data; + + } +); + + +/** + * PHP serializer adapter + */ +data::$adapters['php'] = array( + 'extension' => array('php'), + 'encode' => function($array) { + return ''; + }, + 'decode' => function() { + throw new Error('Decoding PHP strings is not supported'); + }, + 'read' => function($file) { + $array = require $file; + return $array; + } +); + + +/** + * YAML adapter + */ +data::$adapters['yaml'] = array( + 'extension' => array('yaml', 'yml'), + 'encode' => function($data) { + return yaml::encode($data); + }, + 'decode' => function($string) { + return yaml::decode($string); + } +); \ No newline at end of file diff --git a/kirby/toolkit/lib/database.php b/kirby/toolkit/lib/database.php new file mode 100644 index 0000000..2570b1e --- /dev/null +++ b/kirby/toolkit/lib/database.php @@ -0,0 +1,496 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Database { + + public static $connectors = array(); + + // a global array of started connections + public static $connections = array(); + + // the established connection + protected $connection; + + // dsn + protected $dsn; + + // the database type (mysql, sqlite) + protected $type; + + // the connection id + protected $id; + + // the optional prefix for table names + protected $prefix; + + // the PDO query statement + protected $statement; + + // whitelists for tables and their columns + protected $tableWhitelist; + protected $columnWhitelist = array(); + + // the number of affected rows for the last query + protected $affected; + + // the last insert id + protected $lastId; + + // the last query + protected $lastQuery; + + // the last result set + protected $lastResult; + + // the last error + protected $lastError; + + // set to true to throw exceptions on failed queries + protected $fail = false; + + // an array with all queries which are being made + protected $trace = array(); + + /** + * Constructor + */ + public function __construct($params = null) { + $this->connect($params); + } + + /** + * Returns one of the started instance + * + * @param string $id + * @return object + */ + public static function instance($id = null) { + return (is_null($id)) ? a::last(static::$connections) : a::get(static::$connections, $id); + } + + /** + * Returns all started instances + * + * @return array + */ + public static function instances() { + return static::$connections; + } + + /** + * Connects to a database + * + * @param mixed $params This can either be a config key or an array of parameters for the connection + * @return object + */ + public function connect($params = null) { + + $defaults = array( + 'database' => null, + 'type' => 'mysql', + 'prefix' => null, + 'user' => null, + 'password' => null, + 'id' => uniqid() + ); + + $options = array_merge($defaults, $params); + + // store the database information + $this->database = $options['database']; + $this->type = $options['type']; + $this->prefix = $options['prefix']; + $this->id = $options['id']; + + if(!isset(static::$connectors[$this->type])) { + throw new Exception('Invalid database connector: ' . $this->type); + } + + // fetch the dsn and store it + $this->dsn = call_user_func(static::$connectors[$this->type], $options); + + // try to connect + $this->connection = new PDO($this->dsn, $options['user'], $options['password']); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + // store the connection + static::$connections[$this->id] = $this; + + // return the connection + return $this->connection; + + } + + /** + * Returns the currently active connection + * + * @return object + */ + public function connection() { + return $this->connection; + } + + /** + * Sets the exception mode for the next query + * + * @param boolean $fail + */ + public function fail($fail = true) { + $this->fail = $fail; + return $this; + } + + /** + * Returns the used database type + * + * @return string + */ + public function type() { + return $this->type; + } + + /** + * Returns the used table name prefix + * + * @return string + */ + public function prefix() { + return $this->prefix; + } + + /** + * Escapes a value to be used for a safe query + * NOTE: Prepared statements using bound parameters are more secure and solid + * + * @param string $value + * @return string + */ + public function escape($value) { + return substr($this->connection()->quote($value), 1, -1); + } + + /** + * Adds a value to the db trace and also returns the entire trace if nothing is specified + * + * @param array $data + * @return array + */ + public function trace($data = null) { + if(is_null($data)) return $this->trace; + $this->trace[] = $data; + } + + /** + * Returns the number of affected rows for the last query + * + * @return int + */ + public function affected() { + return $this->affected; + } + + /** + * Returns the last id if available + * + * @return int + */ + public function lastId() { + return $this->lastId; + } + + /** + * Returns the last query + * + * @return string + */ + public function lastQuery() { + return $this->lastQuery; + } + + /** + * Returns the last set of results + * + * @return mixed + */ + public function lastResult() { + return $this->lastResult; + } + + /** + * Returns the last db error (exception object) + * + * @return object + */ + public function lastError() { + return $this->lastError; + } + + /** + * Private method to execute database queries. + * This is used by the query() and execute() methods + * + * @param string $query + * @param array $bindings + * @return mixed + */ + protected function hit($query, $bindings = array()) { + + // try to prepare and execute the sql + try { + + $this->statement = $this->connection->prepare($query); + $this->statement->execute($bindings); + + $this->affected = $this->statement->rowCount(); + $this->lastId = $this->connection->lastInsertId(); + $this->lastError = null; + + // store the final sql to add it to the trace later + $this->lastQuery = $this->statement->queryString; + + } catch(\Exception $e) { + + // store the error + $this->affected = 0; + $this->lastError = $e; + $this->lastId = null; + $this->lastQuery = $query; + + // only throw the extension if failing is allowed + if($this->fail) throw $e; + + } + + // add a new entry to the singleton trace array + $this->trace(array( + 'query' => $this->lastQuery, + 'bindings' => $bindings, + 'error' => $this->lastError + )); + + // reset some stuff + $this->fail = false; + + // return true or false on success or failure + return is_null($this->lastError); + + } + + /** + * Exectues a sql query, which is expected to return a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public function query($query, $bindings = array(), $params = array()) { + + $defaults = array( + 'flag' => null, + 'method' => 'fetchAll', + 'fetch' => 'Obj', + 'iterator' => 'Collection', + ); + + $options = array_merge($defaults, $params); + + if(!$this->hit($query, $bindings)) return false; + + // define the default flag for the fetch method + $flags = $options['fetch'] == 'array' ? PDO::FETCH_ASSOC : PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE; + + // add optional flags + if(!empty($options['flag'])) $flags |= $options['flag']; + + // set the fetch mode + if($options['fetch'] == 'array') { + $this->statement->setFetchMode($flags); + } else { + $this->statement->setFetchMode($flags, $options['fetch']); + } + + // fetch that stuff + $results = $this->statement->{$options['method']}(); + + if($options['iterator'] == 'array') return $this->lastResult = $results; + return $this->lastResult = new $options['iterator']($results); + + } + + /** + * Executes a sql query, which is expected to not return a set of results + * + * @param string $query + * @param array $bindings + * @return boolean + */ + public function execute($query, $bindings = array()) { + return $this->lastResult = $this->hit($query, $bindings); + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return object Returns a Query object, which can be used to build a full query for that table + */ + public function table($table) { + return new Database\Query($this, $this->prefix() . $table); + } + + /** + * Checks if a table exists in the current database + * + * @param string $table + * @return boolean + */ + public function validateTable($table) { + if(!$this->tableWhitelist) { + // Get the table whitelist from the database + $sql = new SQL($this); + $query = $sql->tableList($this->database); + $results = $this->query($query, $sql->bindings($query)); + + if($results) { + $this->tableWhitelist = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($table, $this->tableWhitelist); + } + + /** + * Checks if a column exists in a specified table + * + * @param string $table + * @param string $column + * @return boolean + */ + public function validateColumn($table, $column) { + if(!isset($this->columnWhitelist[$table])) { + if(!$this->validateTable($table)) { + $this->columnWhitelist[$table] = array(); + return false; + } + + // Get the column whitelist from the database + $sql = new SQL($this); + $query = $sql->columnList($this->database, $table); + $results = $this->query($query, $sql->bindings($query)); + + if($results) { + $this->columnWhitelist[$table] = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($column, $this->columnWhitelist[$table]); + } + + /** + * Creates a new table + * + * @param string $table + * @param array $columns + * @return boolean + */ + public function createTable($table, $columns = array()) { + $sql = new SQL($this); + $query = $sql->createTable($table, $columns); + $queries = str::split($query, ';'); + + foreach($queries as $query) { + $query = trim($query); + + if(!$this->execute($query, $sql->bindings($query))) return false; + } + + return true; + + } + + /** + * Drops a table + * + * @param string $table + * @return boolean + */ + public function dropTable($table) { + $sql = new SQL($this); + $query = $sql->dropTable($table); + return $this->execute($query, $sql->bindings($query)); + } + + /** + * Magic way to start queries for tables by + * using a method named like the table. + * I.e. $db->users()->all() + */ + public function __call($method, $arguments = null) { + return $this->table($method); + } + +} + + +/** + * MySQL database connector + */ +database::$connectors['mysql'] = function($params) { + + if(!isset($params['host']) && !isset($params['socket'])) { + throw new Error('The mysql connection requires either a "host" or a "socket" parameter'); + } + + if(!isset($params['database'])) { + throw new Error('The mysql connection requires a "database" parameter'); + } + + $parts = array(); + + if(!empty($params['host'])) { + $parts[] = 'host=' . $params['host']; + } + + if(!empty($params['port'])) { + $parts[] = 'port=' . $params['port']; + } + + if(!empty($params['socket'])) { + $parts[] = 'unix_socket=' . $params['socket']; + } + + if(!empty($params['database'])) { + $parts[] = 'dbname=' . $params['database']; + } + + $parts[] = 'charset=' . a::get($params, 'charset', 'utf8'); + + return 'mysql:' . implode(';', $parts); + +}; + + +/** + * SQLite database connector + */ +database::$connectors['sqlite'] = function($params) { + if(!isset($params['database'])) throw new Error('The sqlite connection requires a "database" parameter'); + return 'sqlite:' . $params['database']; +}; diff --git a/kirby/toolkit/lib/database/query.php b/kirby/toolkit/lib/database/query.php new file mode 100644 index 0000000..8e6d68d --- /dev/null +++ b/kirby/toolkit/lib/database/query.php @@ -0,0 +1,923 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Query { + + const ERROR_INVALID_QUERY_METHOD = 0; + + protected $database = null; + + // The object which should be fetched for each row + protected $fetch = 'Obj'; + + // The iterator class, which should be used for result sets + protected $iterator = 'Collection'; + + // An array of bindings for the final query + protected $bindings = array(); + + // The table name + protected $table; + + // The name of the primary key column + protected $primaryKeyName = 'id'; + + // An array with additional join parameters + protected $join; + + // A list of columns, which should be selected + protected $select; + + // Boolean for distinct select clauses + protected $distinct; + + // Boolean for if exceptions should be thrown on failing queries + protected $fail = false; + + // A list of values for update and insert clauses + protected $values; + + // WHERE clause + protected $where; + + // GROUP BY clause + protected $group; + + // HAVING clause + protected $having; + + // ORDER BY clause + protected $order; + + // The offset, which should be applied to the select query + protected $offset = 0; + + // The limit, which should be applied to the select query + protected $limit; + + // Boolean to enable query debugging + protected $debug = false; + + /** + * Constructor + * + * @param Database $database Database object + * @param string $table Optional name of the table, which should be queried + */ + public function __construct($database, $table) { + $this->database = $database; + + $this->table($table); + if(!$this->table) throw new Error('Invalid table ' . $table); + } + + /** + * Reset the query class after each db hit + */ + protected function reset() { + $this->join = null; + $this->select = null; + $this->distinct = null; + $this->fail = false; + $this->values = null; + $this->where = null; + $this->group = null; + $this->having = null; + $this->order = null; + $this->offset = null; + $this->limit = null; + $this->debug = null; + } + + /** + * Enables query debugging. + * If enabled, the query will return an array with all important info about + * the query instead of actually executing the query and returning results + * + * @param boolean $debug + * @return object + */ + public function debug($debug = true) { + $this->debug = $debug; + return $this; + } + + /** + * Enables distinct select clauses. + * + * @param boolean $distinct + * @return object + */ + public function distinct($distinct = true) { + $this->distinct = $distinct; + return $this; + } + + /** + * Enables failing queries. + * If enabled queries will no longer fail silently but throw an exception + * + * @param boolean $fail + * @return object + */ + public function fail($fail = true) { + $this->fail = $fail; + return $this; + } + + /** + * Sets the object class, which should be fetched + * Set this to array to get a simple array instead of an object + * + * @param string $fetch + * @return object + */ + public function fetch($fetch) { + if(!is_null($fetch)) $this->fetch = $fetch; + return $this; + } + + /** + * Sets the iterator class, which should be used for multiple results + * Set this to array to get a simple array instead of an iterator object + * + * @param string $iterator + * @return object + */ + public function iterator($iterator) { + if(!is_null($iterator)) $this->iterator = $iterator; + return $this; + } + + /** + * Sets the name of the table, which should be queried + * + * @param string $table + * @return object + */ + public function table($table) { + if(!is_null($table) && $this->database->validateTable($table)) $this->table = $table; + return $this; + } + + /** + * Sets the name of the primary key column + * + * @param string $primaryKeyName + * @return object + */ + public function primaryKeyName($primaryKeyName) { + $this->primaryKeyName = $primaryKeyName; + return $this; + } + + /** + * Sets the columns, which should be selected from the table + * By default all columns will be selected + * + * @param mixed $select Pass either a string of columns or an array + * @return object + */ + public function select($select) { + $this->select = $select; + return $this; + } + + /** + * Adds a new join clause to the query + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @param string $type The join type. Uses an inner join by default + * @return object + */ + public function join($table, $on, $type = '') { + + $join = array( + 'table' => $table, + 'on' => $on, + 'type' => $type + ); + + $this->join[] = $join; + return $this; + + } + + /** + * Shortcut for creating a left join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return object + */ + public function leftJoin($table, $on) { + return $this->join($table, $on, 'left'); + } + + /** + * Shortcut for creating a right join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return object + */ + public function rightJoin($table, $on) { + return $this->join($table, $on, 'right'); + } + + /** + * Shortcut for creating an inner join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return object + */ + public function innerJoin($table, $on) { + return $this->join($table, $on, 'inner'); + } + + /** + * Sets the values which should be used for the update or insert clause + * + * @param mixed $values Can either be a string or an array of values + * @return object + */ + public function values($values = array()) { + if(!is_null($values)) $this->values = $values; + return $this; + } + + /** + * Attaches additional bindings to the query. + * Also can be used as getter for all attached bindings by not passing an argument. + * + * @param mixed $bindings Array of bindings or null to use this method as getter + * @return mixed + */ + public function bindings($bindings = null) { + + if(is_array($bindings)) { + $this->bindings = array_merge($this->bindings, $bindings); + return $this; + } + + return $this->bindings; + + } + + /** + * Attaches an additional where clause + * + * All available ways to add where clauses + * + * ->where('username like "myuser"'); (args: 1) + * ->where(array('username' => 'myuser')); (args: 1) + * ->where(function($where) { $where->where('id', '=', 1) }) (args: 1) + * ->where('username like ?', 'myuser') (args: 2) + * ->where('username', 'like', 'myuser'); (args: 3) + * + * @param list + * @return object + */ + public function where() { + $this->where = $this->filterQuery(func_get_args(), $this->where); + return $this; + } + + /** + * Shortcut to attach a where clause with an OR operator. + * Check out the where() method docs for additional info. + * + * @param list + * @return object + */ + public function orWhere() { + + $args = func_get_args(); + $mode = a::last($args); + + // if there's a where clause mode attribute attached… + if(in_array($mode, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the OR mode indicator + $args[] = 'OR'; + + call_user_func_array(array($this, 'where'), $args); + return $this; + } + + /** + * Shortcut to attach a where clause with an AND operator. + * Check out the where() method docs for additional info. + * + * @param list + * @return object + */ + public function andWhere() { + + $args = func_get_args(); + $mode = a::last($args); + + // if there's a where clause mode attribute attached… + if(in_array($mode, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the AND mode indicator + $args[] = 'AND'; + + call_user_func_array(array($this, 'where'), func_get_args()); + return $this; + } + + /** + * Attaches a group by clause + * + * @param string $group + * @return object + */ + public function group($group) { + $this->group = $group; + return $this; + } + + /** + * Attaches an additional having clause + * + * All available ways to add having clauses + * + * ->having('username like "myuser"'); (args: 1) + * ->having(array('username' => 'myuser')); (args: 1) + * ->having(function($having) { $having->having('id', '=', 1) }) (args: 1) + * ->having('username like ?', 'myuser') (args: 2) + * ->having('username', 'like', 'myuser'); (args: 3) + * + * @param list + * @return object + */ + public function having() { + $this->having = $this->filterQuery(func_get_args(), $this->having); + return $this; + } + + /** + * Attaches an order clause + * + * @param string $order + * @return object + */ + public function order($order) { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int $offset + * @return object + */ + public function offset($offset) { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int $limit + * @return object + */ + public function limit($limit) { + $this->limit = $limit; + return $this; + } + + /** + * Builds the different types of SQL queries + * This uses the SQL class to build stuff. + * + * @param string $type (select, update, insert) + * @return string The final query + */ + public function build($type) { + + $sql = new SQL($this->database, $this); + + switch($type) { + case 'select': + + return $sql->select(array( + 'table' => $this->table, + 'columns' => $this->select, + 'join' => $this->join, + 'distinct' => $this->distinct, + 'where' => $this->where, + 'group' => $this->group, + 'having' => $this->having, + 'order' => $this->order, + 'offset' => $this->offset, + 'limit' => $this->limit + )); + + case 'update': + + return $sql->update(array( + 'table' => $this->table, + 'where' => $this->where, + 'values' => $this->values, + )); + + case 'insert': + + return $sql->insert(array( + 'table' => $this->table, + 'values' => $this->values, + )); + + case 'delete': + + return $sql->delete(array( + 'table' => $this->table, + 'where' => $this->where, + )); + + } + + } + + /** + * Builds a count query + * + * @return object + */ + public function count() { + return $this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return object + */ + public function max($column) { + return $this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return object + */ + public function min($column) { + return $this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return object + */ + public function sum($column) { + return $this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return object + */ + public function avg($column) { + return $this->aggregate('AVG', $column); + } + + /** + * Builds an aggregation query. + * This is used by all the aggregation methods above + * + * @param string $method + * @param string $column + * @param string $default An optional default value, which should be returned if the query fails + * @return object + */ + public function aggregate($method, $column = '*', $default = 0) { + + // reset the sorting to avoid counting issues + $this->order = null; + + // validate column + if($column !== '*') { + $sql = new SQL($this->database, $this); + list($table, $columnPart) = $sql->splitIdentifier($this->table, $column); + if(!$this->database->validateColumn($table, $columnPart)) { + throw new Error('Invalid column ' . $column); + } + + $column = $sql->combineIdentifier($table, $columnPart); + } + + $fetch = $this->fetch; + $row = $this->select($method . '(' . $column . ') as aggregation')->fetch('Obj')->first(); + $result = $row ? $row->get('aggregation') : $default; + $this->fetch($fetch); + return $result; + } + + /** + * Used as an internal shortcut for firing a db query + * + * @param string $query + * @param array $params + * @return mixed + */ + protected function query($query, $params = array()) { + + if($this->debug) return array( + 'query' => $query, + 'bindings' => $this->bindings(), + 'options' => $params + ); + + if($this->fail) $this->database->fail(); + + $result = $this->database->query($query, $this->bindings(), $params); + $this->reset(); + return $result; + + } + + /** + * Used as an internal shortcut for executing a db query + * + * @param string $query + * @param array $params + * @return mixed + */ + protected function execute($query, $params = array()) { + + if($this->debug) return array( + 'query' => $query, + 'bindings' => $this->bindings(), + 'options' => $params + ); + + if($this->fail) $this->database->fail(); + + $result = $this->database->execute($query, $this->bindings(), $params); + $this->reset(); + return $result; + + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function first() { + return $this->query($this->offset(0)->limit(1)->build('select'), array( + 'fetch' => $this->fetch, + 'iterator' => 'array', + 'method' => 'fetch', + )); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function row() { + return $this->first(); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function one() { + return $this->first(); + } + + /** + * Automatically adds pagination to a query + * + * @param int $page + * @param int $limit The number of rows, which should be returned for each page + * @param array $params Optional params for the pagination object + * @return object Collection iterator with attached pagination object + */ + public function page($page, $limit, $params = array()) { + + $defaults = array( + 'page' => $page + ); + + $options = array_merge($defaults, $params); + + // clone this to create a counter query + $counter = clone $this; + + // count the total number of rows for this query + $count = $counter->count(); + + // pagination + $pagination = new Pagination($count, $limit, $options); + + // apply it to the dataset and retrieve all rows. make sure to use Collection as the iterator to be able to attach the pagination object + $collection = $this->offset($pagination->offset())->limit($pagination->limit())->all(); + + // store all pagination vars in a separate object + if($collection) $collection->paginate($pagination); + + // return the limited collection + return $collection; + + } + + /** + * Returns all matching rows from a table + * + * @return mixed + */ + public function all() { + + return $this->query($this->build('select'), array( + 'fetch' => $this->fetch, + 'iterator' => $this->iterator, + )); + } + + /** + * Returns only values from a single column + * + * @param string $column + * @return mixed + */ + public function column($column) { + + $sql = new SQL($this->database, $this); + $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); + + $results = $this->query($this->select(array($column))->order($primaryKey . ' ASC')->build('select'), array( + 'iterator' => 'array', + 'fetch' => 'array', + )); + + $results = a::extract($results, $column); + + if($this->iterator == 'array') return $results; + + $iterator = $this->iterator; + return new $iterator($results); + + } + + /** + * Find a single row by column and value + * + * @param string $column + * @param mixed $value + * @return mixed + */ + public function findBy($column, $value) { + return $this->where(array($column => $value))->first(); + } + + /** + * Find a single row by its primary key + * + * @param mixed $id + * @return mixed + */ + public function find($id) { + return $this->findBy($this->primaryKeyName, $id); + } + + /** + * Fires an insert query + * + * @param array $values You can pass values here or set them with ->values() before + * @return mixed Returns the last inserted id on success or false. + */ + public function insert($values = null) { + $query = $this->execute($this->values($values)->build('insert')); + return ($query) ? $this->database->lastId() : false; + } + + /** + * Fires an update query + * + * @param array $values You can pass values here or set them with ->values() before + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return boolean + */ + public function update($values = null, $where = null) { + return $this->execute($this->values($values)->where($where)->build('update')); + } + + /** + * Fires a delete query + * + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return boolean + */ + public function delete($where = null) { + return $this->execute($this->where($where)->build('delete')); + } + + /** + * Enables magic queries like findByUsername or findByEmail + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call($method, $arguments) { + + if(preg_match('!^findBy([a-z]+)!i', $method, $match)) { + $column = str::lower($match[1]); + return $this->findBy($column, $arguments[0]); + } else { + throw new Error('Invalid query method: ' . $method, static::ERROR_INVALID_QUERY_METHOD); + } + + } + + /** + * Builder for where and having clauses + * + * @param array $args Arguments, see where() description + * @param string $current Current value (like $this->where) + * @return string + */ + protected function filterQuery($args, $current) { + + $mode = a::last($args); + $result = ''; + + // if there's a where clause mode attribute attached… + if(in_array($mode, array('AND', 'OR'))) { + // remove that from the list of arguments + array_pop($args); + } else { + $mode = 'AND'; + } + + switch(count($args)) { + case 1: + + if(is_null($args[0])) { + + return $current; + + // ->where('username like "myuser"'); + } else if(is_string($args[0])) { + + // simply add the entire string to the where clause + // escaping or using bindings has to be done before calling this method + $result = $args[0]; + + // ->where(array('username' => 'myuser')); + } else if(is_array($args[0])) { + + $sql = new SQL($this->database, $this); + + // simple array mode (AND operator) + $result = $sql->values($this->table, $args[0], ' AND ', true, true); + + } else if(is_callable($args[0])) { + + $query = clone $this; + call_user_func($args[0], $query); + $result = '(' . $query->where . ')'; + + } + + break; + case 2: + + // ->where('username like :username', array('username' => 'myuser')) + if(is_string($args[0]) && is_array($args[1])) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings($args[1]); + + // ->where('username like ?', 'myuser') + } else if(is_string($args[0]) && is_string($args[1])) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings(array($args[1])); + + } + + break; + case 3: + + // ->where('username', 'like', 'myuser'); + if(is_string($args[0]) && is_string($args[1])) { + + // validate column + $sql = new SQL($this->database, $this); + list($table, $column) = $sql->splitIdentifier($this->table, $args[0]); + if(!$this->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $args[0]); + } + $key = $sql->combineIdentifier($table, $column); + + // ->where('username', 'in', array('myuser', 'myotheruser')); + if(is_array($args[2])) { + + $predicate = trim(strtoupper($args[1])); + if(!in_array($predicate, array( + 'IN', 'NOT IN' + ))) throw new Error('Invalid predicate ' . $predicate); + + // build a list of bound values + $values = array(); + $bindings = array(); + foreach($args[2] as $value) { + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $value; + $values[] = $valueBinding; + } + + // add that to the where clause in parenthesis + $result = $key . ' ' . $predicate . ' (' . implode(', ', $values) . ')'; + + $this->bindings($bindings); + + // ->where('username', 'like', 'myuser'); + } else { + + $predicate = trim(strtoupper($args[1])); + if(!in_array($predicate, array( + '=', '>=', '>', '<=', '<', '<>', '!=', '<=>', + 'IS', 'IS NOT', + 'BETWEEN', 'NOT BETWEEN', + 'LIKE', 'NOT LIKE', + 'SOUNDS LIKE', + 'REGEXP', 'NOT REGEXP' + ))) throw new Error('Invalid predicate/operator ' . $predicate); + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $args[2]; + + $result = $key . ' ' . $predicate . ' ' . $valueBinding; + + $this->bindings($bindings); + + } + + } + + break; + + } + + // attach the where clause + if(!empty($current)) { + return $current . ' ' . $mode . ' ' . $result; + } else { + return $result; + } + + } + +} diff --git a/kirby/toolkit/lib/db.php b/kirby/toolkit/lib/db.php new file mode 100644 index 0000000..32c7f22 --- /dev/null +++ b/kirby/toolkit/lib/db.php @@ -0,0 +1,251 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class DB { + + const ERROR_UNKNOWN_METHOD = 0; + + // query shortcuts + public static $queries = array(); + + // The singleton Database object + public static $connection = null; + + /** + * (Re)connect the database + * + * @param mixed $params Pass array() to use the default params from the config + * @return object + */ + public static function connect($params = null) { + if(is_null($params) && !is_null(static::$connection)) return static::$connection; + if(is_null($params)) { + + // try to connect with the default connection settings + $params = array( + 'type' => c::get('db.type', 'mysql'), + 'host' => c::get('db.host', 'localhost'), + 'user' => c::get('db.user', 'root'), + 'password' => c::get('db.password', ''), + 'database' => c::get('db.name', ''), + 'prefix' => c::get('db.prefix', ''), + ); + + } + + return static::$connection = new Database($params); + } + + /** + * Returns the current database connection + * + * @return object + */ + public static function connection() { + return static::$connection; + } + + /** + * Sets the current table, which should be queried + * + * @param string $table + * @return object Returns a DBQuery object, which can be used to build a full query for that table + */ + public static function table($table) { + $connection = db::connect(); + return $connection->table($table); + } + + /** + * Executes a raw sql query which expects a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public static function query($query, $bindings = array(), $params = array()) { + $connection = db::connect(); + return $connection->query($query, $bindings, $params); + } + + /** + * Executes a raw sql query which expects no set of results (i.e. update, insert, delete) + * + * @param string $query + * @param array $bindings + * @return mixed + */ + public static function execute($query, $bindings = array()) { + $connection = db::connect(); + return $connection->execute($query, $bindings); + } + + /** + * Magic calls for other static db methods, + * which are redircted to the database class if available + * + * @param string $method + * @param mixed $arguments + * @return mixed + */ + public static function __callStatic($method, $arguments) { + + if(isset(static::$queries[$method])) { + return call(static::$queries[$method], $arguments); + } else if(!is_callable(array(static::$connection, $method))) { + throw new Error('invalid static db method: ' . $method, static::ERROR_UNKNOWN_METHOD); + } else { + return call(array(static::$connection, $method), $arguments); + } + } + +} + +/** + * Shortcut for select clauses + * + * @param string $table The name of the table, which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The where clause. Can be a string or an array + * @param mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['select'] = function($table, $columns = '*', $where = null, $order = null, $offset = 0, $limit = null) { + return db::table($table)->select($columns)->where($where)->order($order)->offset($offset)->limit($limit)->all(); +}; + +/** + * Shortcut for selecting a single row in a table + * + * @param string $table The name of the table, which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The where clause. Can be a string or an array + * @param mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['first'] = db::$queries['row'] = db::$queries['one'] = function($table, $columns = '*', $where = null, $order = null) { + return db::table($table)->select($columns)->where($where)->order($order)->first(); +}; + +/** + * Returns only values from a single column + * + * @param string $table The name of the table, which should be queried + * @param mixed $column The name of the column to select from + * @param mixed $where The where clause. Can be a string or an array + * @param mixed $order + * @param int $offset + * @param int $limit + * @return mixed + */ +db::$queries['column'] = function($table, $column, $where = null, $order = null, $offset = 0, $limit = null) { + return db::table($table)->where($where)->order($order)->offset($offset)->limit($limit)->column($column); +}; + +/** + * Shortcut for inserting a new row into a table + * + * @param string $table The name of the table, which should be queried + * @param string $values An array of values, which should be inserted + * @return boolean + */ +db::$queries['insert'] = function($table, $values) { + return db::table($table)->insert($values); +}; + +/** + * Shortcut for updating a row in a table + * + * @param string $table The name of the table, which should be queried + * @param string $values An array of values, which should be inserted + * @param mixed $where An optional where clause + * @return boolean + */ +db::$queries['update'] = function($table, $values, $where = null) { + return db::table($table)->where($where)->update($values); +}; + +/** + * Shortcut for deleting rows in a table + * + * @param string $table The name of the table, which should be queried + * @param mixed $where An optional where clause + * @return boolean + */ +db::$queries['delete'] = function($table, $where = null) { + return db::table($table)->where($where)->delete(); +}; + +/** + * Shortcut for counting rows in a table + * + * @param string $table The name of the table, which should be queried + * @param string $where An optional where clause + * @return int + */ +db::$queries['count'] = function($table, $where = null) { + return db::table($table)->where($where)->count(); +}; + +/** + * Shortcut for calculating the minimum value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the minimum should be calculated + * @param string $where An optional where clause + * @return mixed + */ +db::$queries['min'] = function($table, $column, $where = null) { + return db::table($table)->where($where)->min($column); +}; + +/** + * Shortcut for calculating the maximum value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the maximum should be calculated + * @param string $where An optional where clause + * @return mixed + */ +db::$queries['max'] = function($table, $column, $where = null) { + return db::table($table)->where($where)->max($column); +}; + +/** + * Shortcut for calculating the average value in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the average should be calculated + * @param string $where An optional where clause + * @return mixed + */ +db::$queries['avg'] = function($table, $column, $where = null) { + return db::table($table)->where($where)->avg($column); +}; + +/** + * Shortcut for calculating the sum of all values in a column + * + * @param string $table The name of the table, which should be queried + * @param string $column The name of the column of which the sum should be calculated + * @param string $where An optional where clause + * @return mixed + */ +db::$queries['sum'] = function($table, $column, $where = null) { + return db::table($table)->where($where)->sum($column); +}; \ No newline at end of file diff --git a/kirby/toolkit/lib/detect.php b/kirby/toolkit/lib/detect.php new file mode 100644 index 0000000..a436568 --- /dev/null +++ b/kirby/toolkit/lib/detect.php @@ -0,0 +1,261 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Detect { + + /** + * Checks if the mb string extension is installed + * + * @return boolean + */ + public static function mbstring() { + return function_exists('mb_split'); + } + + /** + * Checks if the required php version is installed + * + * @param mixed $min + * @return boolean + */ + public static function php($min = '5.3') { + return version_compare(PHP_VERSION, $min, '>='); + } + + /** + * Checks if PHP is running on Apache + * + * @return boolean + */ + public static function apache() { + return apache_get_version() ? true : false; + } + + /** + * Checks if the site is running on Windows + * + * @return boolean + */ + public static function windows() { + return DS == '/' ? false : true; + } + + /** + * Checks if the site is running on IIS + * + * @return boolean + */ + public static function iis() { + return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'],'IIS') !== false ? true : false; + } + + /** + * Checks if mysql installed with the minimum required version + * + * @param mixed $min + * @return boolean + */ + public static function mysql($min = '5') { + $extensions = get_loaded_extensions(); + if(!in_array('mysql', $extensions)) return false; + $version = preg_replace('#(^\D*)([0-9.]+).*$#', '\2', mysql_get_client_info()); + return version_compare($version, $min, '>='); + } + + /** + * Checks if SQLite 3 is installed + * + * @return boolean + */ + public static function sqlite() { + return in_array('sqlite3', get_loaded_extensions()); + } + + /** + * Checks if safe mode is enabled + * + * @return boolean + */ + public static function safemode() { + return ini_get('safe_mode'); + } + + /** + * Checks if gdlib is installed + * + * @return boolean + */ + public static function gdlib() { + return function_exists('gd_info'); + } + + /** + * Checks if imageick is installed + * + * @return boolean + */ + public static function imagick() { + return class_exists('Imagick'); + } + + /** + * Checks if CURL is installed + * + * @return boolean + */ + public static function curl() { + return in_array('curl', get_loaded_extensions()); + } + + /** + * Check if APC cache is installed + * + * @return boolean + */ + public static function apc() { + return function_exists('apc_add'); + } + + /** + * Check if the Memcache extension is installed + * + * @return boolean + */ + public static function memcache() { + return class_exists('Memcache'); + } + + /** + * Check if the Memcached extension is installed + * + * @return boolean + */ + public static function memcached() { + return class_exists('Memcached'); + } + + /** + * Check if the imap extension is installed + * + * @return boolean + */ + public static function imap() { + return function_exists('imap_body'); + } + + /** + * Check if the mcrypt extension is installed + * + * @return boolean + */ + public static function mcrypt() { + return function_exists('mcrypt_encrypt'); + } + + /** + * Check if the exif extension is installed + * + * @return boolean + */ + public static function exif() { + return function_exists('read_exif_data'); + } + + /** + * Detect if the script is installed in a subfolder + * + * @return string + */ + public static function subfolder() { + return trim(dirname($_SERVER['SCRIPT_NAME']), '/\\'); + } + + /** + * Detects the current path + * + * @return string + */ + public static function path() { + $uri = explode('/', url::path()); + $script = explode('/', trim($_SERVER['SCRIPT_NAME'], '/\\')); + $parts = array_diff_assoc($uri, $script); + if(empty($parts)) return false; + return implode('/', $parts); + } + + /** + * Detect the document root + * + * @return string + */ + public static function documentRoot() { + $local = $_SERVER['SCRIPT_NAME']; + $absolute = $_SERVER['SCRIPT_FILENAME']; + return substr($absolute, 0, strpos($absolute, $local)); + } + + /** + * Converts any ini size value to an integer + * + * @param string $key + * @return int + */ + public static function iniSize($key) { + + $size = ini_get($key); + $size = trim($size); + $last = strtolower($size[strlen($size)-1]); + switch($last) { + case 'g': + $size *= 1024; + case 'm': + $size *= 1024; + case 'k': + $size *= 1024; + } + return $size; + + } + + /** + * Returns the max accepted upload size + * defined in the php.ini + * + * @return int + */ + public static function maxUploadSize() { + return static::iniSize('upload_max_filesize'); + } + + /** + * Returns the max accepted post size + * defined in the php.ini + * + * @return int + */ + public static function maxPostSize() { + return static::iniSize('post_max_size'); + } + + /** + * Dirty browser sniffing for an ios device + * + * @return boolean + */ + public static function ios() { + $ua = visitor::ua(); + return (str::contains($ua, 'iPod') || str::contains($ua, 'iPhone') || str::contains($ua, 'iPad')); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/dimensions.php b/kirby/toolkit/lib/dimensions.php new file mode 100644 index 0000000..4d52ecf --- /dev/null +++ b/kirby/toolkit/lib/dimensions.php @@ -0,0 +1,316 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Dimensions { + + // the width of the parent object + public $width = 0; + + // the height of the parent object + public $height = 0; + + /** + * Constructor + * + * @param int $width + * @param int $height + */ + public function __construct($width, $height) { + $this->width = $width; + $this->height = $height; + } + + /** + * Returns the width + * + * @return int + */ + public function width() { + return $this->width; + } + + /** + * Returns the height + * + * @return int + */ + public function height() { + return $this->height; + } + + /** + * Calculates and returns the ratio + * + * + * + * $dimensions = new Dimensions(1200, 768); + * echo $dimensions->ratio(); + * // output: 1.5625 + * + * + * + * @return float + */ + public function ratio() { + if($this->width && $this->height) { + return ($this->width / $this->height); + } else { + return 0; + } + } + + /** + * Recalculates the width and height + * to fit into the given box. + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fit(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int $box the max width and/or height + * @param boolean $force If true, the dimensions will be upscaled to fit the box if smaller + * @return object returns this object with recalculated dimensions + */ + public function fit($box, $force = false) { + + if($this->width == 0 || $this->height == 0) { + $this->width = $box; + $this->height = $box; + return $this; + } + + $ratio = $this->ratio(); + + if($this->width > $this->height) { + if($this->width > $box || $force === true) $this->width = $box; + $this->height = round($this->width / $ratio); + } elseif($this->height > $this->width) { + if($this->height > $box || $force === true) $this->height = $box; + $this->width = round($this->height * $ratio); + } elseif($this->width > $box) { + $this->width = $box; + $this->height = $box; + } + + return $this; + + } + + /** + * Recalculates the width and height + * to fit the given width + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitWidth(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int $fit the max width + * @param boolean $force If true, the dimensions will be upscaled to fit the width if smaller + * @return object returns this object with recalculated dimensions + */ + public function fitWidth($fit, $force = false) { + + if(!$fit) return $this; + + if($this->width <= $fit && !$force) return $this; + + $ratio = $this->ratio(); + + $this->width = $fit; + $this->height = round($fit / $ratio); + + return $this; + + } + + /** + * Recalculates the width and height + * to fit the given height + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitHeight(500); + * + * echo $dimensions->width(); + * // output: 781 + * + * echo $dimensions->height(); + * // output: 500 + * + * + * + * @param int $fit the max height + * @param boolean $force If true, the dimensions will be upscaled to fit the height if smaller + * @return object returns this object with recalculated dimensions + */ + public function fitHeight($fit, $force = false) { + + if(!$fit) return $this; + + if($this->height <= $fit && !$force) return $this; + + $ratio = $this->ratio(); + + $this->width = round($fit * $ratio); + $this->height = $fit; + + return $this; + + } + + /** + * Recalculates the dimensions by the width and height + * + * @param int $width the max height + * @param int $height the max width + * @return object + */ + public function fitWidthAndHeight($width, $height, $force = false) { + + if($this->width > $this->height) { + + $this->fitWidth($width, $force); + + // do another check for the max height + if($this->height > $height) $this->fitHeight($height); + + } else { + + $this->fitHeight($height, $force); + + // do another check for the max width + if($this->width > $width) $this->fitWidth($width); + + } + + return $this; + + } + + /** + * @param int $width + * @param int $height + * @param boolean $force + * @return Dimensions + */ + public function resize($width, $height, $force = false) { + $this->fitWidthAndHeight($width, $height, $force); + return $this; + } + + /** + * Crops the dimensions by width and height + * + * @param int $width + * @param int $height + * @return object + */ + public function crop($width, $height = null) { + + $this->width = $width; + $this->height = $width; + + if($height) { + $this->height = $height; + } + + return $this; + + } + + /** + * Returns a string representation of the orientation + * + * @return string + */ + public function orientation() { + if(!$this->ratio()) return false; + if($this->portrait()) return 'portrait'; + if($this->landscape()) return 'landscape'; + if($this->square()) return 'square'; + } + + /** + * Checks if the dimensions are portrait + * + * @return boolean + */ + public function portrait() { + return $this->height > $this->width; + } + + /** + * Checks if the dimensions are landscape + * + * @return boolean + */ + public function landscape() { + return $this->width > $this->height; + } + + /** + * Checks if the dimensions are square + * + * @return boolean + */ + public function square() { + return $this->width == $this->height; + } + + /** + * Converts the dimensions object + * to a plain PHP array + * + * @return array + */ + public function toArray() { + return array( + 'width' => $this->width(), + 'height' => $this->height(), + 'ratio' => $this->ratio(), + 'orientation' => $this->orientation(), + ); + } + + /** + * Echos the dimensions as width × height + * + * @return string + */ + public function __toString() { + return $this->width . ' × ' . $this->height; + } + +} diff --git a/kirby/toolkit/lib/dir.php b/kirby/toolkit/lib/dir.php new file mode 100644 index 0000000..1d97293 --- /dev/null +++ b/kirby/toolkit/lib/dir.php @@ -0,0 +1,209 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Dir { + + public static $defaults = array( + 'permissions' => 0755, + 'ignore' => array('.', '..', '.DS_Store', '.gitignore', '.git', '.svn', '.htaccess', 'Thumb.db', '@eaDir') + ); + + /** + * Creates a new directory + * + * + * + * $create = dir::make('/app/test/new-directory'); + * + * if($create) echo 'the directory has been created'; + * + * + * + * @param string $dir The path for the new directory + * @return boolean True: the dir has been created, false: creating failed + */ + public static function make($dir, $recursive = true) { + return is_dir($dir) ? true : @mkdir($dir, static::$defaults['permissions'], $recursive); + } + + /** + * Reads all files from a directory and returns them as an array. + * It skips unwanted invisible stuff. + * + * + * + * $files = dir::read('mydirectory'); + * // returns array('file-1.txt', 'file-2.txt', 'file-3.txt', etc...); + * + * + * + * @param string $dir The path of directory + * @param array $ignore Optional array with filenames, which should be ignored + * @return mixed An array of filenames or false + */ + public static function read($dir, $ignore = array()) { + if(!is_dir($dir)) return array(); + $skip = array_merge(static::$defaults['ignore'], $ignore); + return (array)array_diff(scandir($dir),$skip); + } + + /** + * Moves a directory to a new location + * + * + * + * $move = dir::move('mydirectory', 'mynewdirectory'); + * + * if($move) echo 'the directory has been moved to mynewdirectory'; + * + * + * + * @param string $old The current path of the directory + * @param string $new The desired path where the dir should be moved to + * @return boolean True: the directory has been moved, false: moving failed + */ + public static function move($old, $new) { + if(!is_dir($old)) return false; + return @rename($old, $new); + } + + /** + * Deletes a directory + * + * + * + * $remove = dir::remove('mydirectory'); + * + * if($remove) echo 'the directory has been removed'; + * + * + * + * @param string $dir The path of the directory + * @param boolean $keep If set to true, the directory will flushed but not removed. + * @return boolean True: the directory has been removed, false: removing failed + */ + public static function remove($dir, $keep = false) { + if(!is_dir($dir)) return false; + + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->remove($keep); + } + + /** + * Flushes a directory + * + * @param string $dir The path of the directory + * @return boolean True: the directory has been flushed, false: flushing failed + */ + public static function clean($dir) { + return static::remove($dir, true); + } + + /** + * Gets the size of the directory and all subfolders and files + * + * @param string $dir The path of the directory + * @return mixed + */ + public static function size($dir) { + + if(!file_exists($dir)) return false; + + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->size(); + + } + + /** + * Returns a nicely formatted size of all the contents of the folder + * + * @param string $dir The path of the directory + * @return mixed + */ + public static function niceSize($dir) { + return f::niceSize(static::size($dir)); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @param string $dir The path of the directory + * @param string $format + * @return int + */ + public static function modified($dir, $format = null, $handler = 'date') { + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->modified($format, $handler); + } + + /** + * Checks if the directory or any subdirectory has been + * modified after the given timestamp + * + * @param string $dir + * @param int $time + * @return boolean + */ + public static function wasModifiedAfter($dir, $time) { + + if(filemtime($dir) > $time) return true; + + $content = dir::read($dir); + + foreach($content as $item) { + $subdir = $dir . DS . $item; + if(filemtime($subdir) > $time) return true; + if(is_dir($subdir) && dir::wasModifiedAfter($subdir, $time)) return true; + } + + return false; + + } + + /** + * Checks if the dir is writable + * + * @param string $dir + * @return boolean + */ + public static function writable($dir) { + return is_writable($dir); + } + + /** + * Checks if the dir is readable + * + * @param string $dir + * @return boolean + */ + public static function readable($dir) { + return is_readable($dir); + } + + /** + * Copy a file, or recursively copy a folder and its contents + * + * @param string $dir Source path + * @param string $to Destination path + */ + public static function copy($dir, $to) { + // It's easier to handle this with the Folder class + $object = new Folder($dir); + return $object->copy($to); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/email.php b/kirby/toolkit/lib/email.php new file mode 100644 index 0000000..b20cdc7 --- /dev/null +++ b/kirby/toolkit/lib/email.php @@ -0,0 +1,291 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Email extends Obj { + + const ERROR_INVALID_RECIPIENT = 0; + const ERROR_INVALID_SENDER = 1; + const ERROR_INVALID_REPLY_TO = 2; + const ERROR_INVALID_SUBJECT = 3; + const ERROR_INVALID_BODY = 4; + const ERROR_INVALID_SERVICE = 5; + const ERROR_DISABLED = 6; + + public static $defaults = array( + 'service' => 'mail', + 'options' => array(), + 'to' => null, + 'from' => null, + 'replyTo' => null, + 'subject' => null, + 'body' => null + ); + + public static $services = array(); + public static $disabled = false; + + public $error; + public $service; + public $options; + public $to; + public $from; + public $replyTo; + public $subject; + public $body; + + public function __construct($params = array()) { + $options = a::merge(static::$defaults, $params); + parent::__construct($options); + } + + public function __set($key, $value) { + $this->$key = $value; + } + + /** + * Validates the constructed email + * to make sure it can be sent at all + */ + public function validate() { + if(!v::email($this->extractAddress($this->to))) throw new Error('Invalid recipient', static::ERROR_INVALID_RECIPIENT); + if(!v::email($this->extractAddress($this->from))) throw new Error('Invalid sender', static::ERROR_INVALID_SENDER); + if(!v::email($this->extractAddress($this->replyTo))) throw new Error('Invalid reply address', static::ERROR_INVALID_REPLY_TO); + if(empty($this->subject)) throw new Error('Missing subject', static::ERROR_INVALID_SUBJECT); + if(empty($this->body)) throw new Error('Missing body', static::ERROR_INVALID_BODY); + } + + /** + * Public getter for the error exception + * + * @return Exception + */ + public function error() { + return $this->error; + } + + /** + * Extracts the email address from an address string + * + * @return string + */ + protected function extractAddress($string) { + if(v::email($string)) return $string; + preg_match('/<(.*?)>/i', $string, $array); + return (empty($array[1])) ? $string : $array[1]; + } + + /** + * Sends the constructed email + * + * @param array $params Optional way to set values for the email + * @return boolean + */ + public function send($params = null) { + + try { + + // fail silently if sending emails is disabled + if(static::$disabled) throw new Error('Sending emails is disabled', static::ERROR_DISABLED); + + // overwrite already set values + if(is_array($params) && !empty($params)) { + foreach(a::merge($this->toArray(), $params) as $key => $val) { + $this->set($key, $val); + } + } + + // reset all errors + $this->error = null; + + // default service + if(empty($this->service)) $this->service = 'mail'; + + // if there's no dedicated reply to address, use the from address + if(empty($this->replyTo)) $this->replyTo = $this->from; + + // validate the email + $this->validate(); + + // check if the email service is available + if(!isset(static::$services[$this->service])) { + throw new Error('The email service is not available: ' . $this->service, static::ERROR_INVALID_SERVICE); + } + + // run the service + call(static::$services[$this->service], $this); + + // reset the error + $this->error = null; + return true; + + } catch(Exception $e) { + $this->error = $e; + return false; + } + + } + +} + + +/** + * Default mail driver + */ +email::$services['mail'] = function($email) { + + $headers = array( + 'From: ' . $email->from, + 'Reply-To: ' . $email->replyTo, + 'Return-Path: ' . $email->replyTo, + 'Message-ID: <' . time() . '-' . $email->from . '>', + 'X-Mailer: PHP v' . phpversion(), + 'Content-Type: text/plain; charset=utf-8', + 'Content-Transfer-Encoding: 8bit', + ); + + ini_set('sendmail_from', $email->from); + $send = mail($email->to, str::utf8($email->subject), str::utf8($email->body), implode(PHP_EOL, $headers)); + ini_restore('sendmail_from'); + + if(!$send) { + throw new Error('The email could not be sent'); + } + +}; + +/** + * Amazon mail driver + */ +email::$services['amazon'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Missing Amazon API key'); + if(empty($email->options['secret'])) throw new Error('Missing Amazon API secret'); + + $setup = array( + 'Action' => 'SendEmail', + 'Destination.ToAddresses.member.1' => $email->to, + 'ReplyToAddresses.member.1' => $email->replyTo, + 'ReturnPath' => $email->replyTo, + 'Source' => $email->from, + 'Message.Subject.Data' => $email->subject, + 'Message.Body.Text.Data' => $email->body + ); + + $params = array(); + + foreach($setup as $key => $value) { + $params[] = $key . '=' . str_replace('%7E', '~', rawurlencode($value)); + } + + sort($params, SORT_STRING); + + $host = a::get($email->options, 'host', 'email.us-east-1.amazonaws.com'); + $url = 'https://' . $host . '/'; + $date = gmdate('D, d M Y H:i:s e'); + $signature = base64_encode(hash_hmac('sha256', $date, $email->options['secret'], true)); + $query = implode('&', $params); + $headers = array(); + $auth = 'AWS3-HTTPS AWSAccessKeyId=' . $email->options['key']; + $auth .= ',Algorithm=HmacSHA256,Signature=' . $signature; + + $headers[] = 'Date: ' . $date; + $headers[] = 'Host: ' . $host; + $headers[] = 'X-Amzn-Authorization: '. $auth; + $headers[] = 'Content-Type: application/x-www-form-urlencoded'; + + $email->response = remote::post($url, array( + 'data' => $query, + 'headers' => $headers + )); + + if(!in_array($email->response->code(), array(200, 201, 202, 204))) { + throw new Error('The mail could not be sent!', $email->response->code()); + } + +}; + +/** + * Mailgun mail driver + */ +email::$services['mailgun'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Missing Mailgun API key'); + if(empty($email->options['domain'])) throw new Error('Missing Mailgun API domain'); + + $url = 'https://api.mailgun.net/v2/' . $email->options['domain'] . '/messages'; + $auth = base64_encode('api:' . $email->options['key']); + + $headers = array( + 'Accept: application/json', + 'Authorization: Basic ' . $auth + ); + + $data = array( + 'from' => $email->from, + 'to' => $email->to, + 'subject' => $email->subject, + 'text' => $email->body, + 'h:Reply-To' => $email->replyTo, + ); + + $email->response = remote::post($url, array( + 'data' => $data, + 'headers' => $headers + )); + + if($email->response->code() != 200) { + throw new Error('The mail could not be sent!'); + } + +}; + +/** + * Postmark mail driver + */ +email::$services['postmark'] = function($email) { + + if(empty($email->options['key'])) throw new Error('Invalid Postmark API Key'); + + // reset the api key if we are in test mode + if(a::get($email->options, 'test')) $email->options['key'] = 'POSTMARK_API_TEST'; + + // the url for postmarks api + $url = 'https://api.postmarkapp.com/email'; + + $headers = array( + 'Accept: application/json', + 'Content-Type: application/json', + 'X-Postmark-Server-Token: ' . $email->options['key'] + ); + + $data = array( + 'From' => $email->from, + 'To' => $email->to, + 'ReplyTo' => $email->replyTo, + 'Subject' => $email->subject, + 'TextBody' => $email->body + ); + + // fetch the response + $email->response = remote::post($url, array( + 'data' => json_encode($data), + 'headers' => $headers + )); + + if($email->response->code() != 200) { + throw new Error('The mail could not be sent'); + } + +}; diff --git a/kirby/toolkit/lib/embed.php b/kirby/toolkit/lib/embed.php new file mode 100644 index 0000000..34a9365 --- /dev/null +++ b/kirby/toolkit/lib/embed.php @@ -0,0 +1,124 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Embed { + + /** + * Embeds a youtube video by passing the Youtube url + * + * @param string $url Youtube url i.e. http://www.youtube.com/watch?v=d9NF2edxy-M + * @param array $attr Additional attributes for the iframe + * @return string + */ + public static function youtube($url, $attr = array()) { + + // http://www.youtube.com/embed/d9NF2edxy-M + if(preg_match('!youtube.com\/embed\/([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + // http://www.youtube.com/watch?feature=player_embedded&v=d9NF2edxy-M#! + } elseif(preg_match('!v=([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + // http://youtu.be/d9NF2edxy-M + } elseif(preg_match('!youtu.be\/([a-z0-9_-]+)!i', $url, $array)) { + $id = $array[1]; + } + + // no id no result! + if(empty($id)) return false; + + // default options + if(!empty($attr['options'])) { + $options = '?' . http_build_query($attr['options']); + // options should not propagate to the attr list + unset($attr['options']); + } else { + $options = ''; + } + + // default attributes + $attr = array_merge(array( + 'src' => '//youtube.com/embed/' . $id . $options, + 'frameborder' => '0', + 'webkitAllowFullScreen' => 'true', + 'mozAllowFullScreen' => 'true', + 'allowFullScreen' => 'true', + 'width' => '100%', + 'height' => '100%', + ), $attr); + + return html::tag('iframe', '', $attr); + + } + + /** + * Embeds a vimeo video by passing the vimeo url + * + * @param string $url vimeo url i.e. http://vimeo.com/52345557 + * @param array $attr Additional attributes for the iframe + * @return string + */ + public static function vimeo($url, $attr = array()) { + + // get the uid from the url + if(preg_match('!vimeo.com\/([0-9]+)!i', $url, $array)) { + $id = $array[1]; + } else { + $id = null; + } + + // no id no result! + if(empty($id)) return false; + + // default options + if(!empty($attr['options'])) { + $options = '?' . http_build_query($attr['options']); + // options should not propagate to the attr list + unset($attr['options']); + } else { + $options = ''; + } + + // default attributes + $attr = array_merge(array( + 'src' => '//player.vimeo.com/video/' . $id . $options, + 'frameborder' => '0', + 'webkitAllowFullScreen' => 'true', + 'mozAllowFullScreen' => 'true', + 'allowFullScreen' => 'true', + 'width' => '100%', + 'height' => '100%', + ), $attr); + + return html::tag('iframe', '', $attr); + + } + + /** + * Embeds a github gist + * + * @param string $url Gist url: i.e. https://gist.github.com/2924148 + * @param string $file The name of a particular file from the gist, which should displayed only. + * @return string + */ + public static function gist($url, $file = null) { + + // url for the script file + $url = $url . '.js' . r(!is_null($file), '?file=' . $file); + + // load the gist + return html::tag('script', '', array('src' => $url)); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/error.php b/kirby/toolkit/lib/error.php new file mode 100644 index 0000000..6ded65b --- /dev/null +++ b/kirby/toolkit/lib/error.php @@ -0,0 +1,26 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Error extends Exception { + + public function message() { + return $this->message; + } + + public function code() { + return $this->code; + } + + public function __toString() { + return $this->message; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/errorreporting.php b/kirby/toolkit/lib/errorreporting.php new file mode 100644 index 0000000..118231e --- /dev/null +++ b/kirby/toolkit/lib/errorreporting.php @@ -0,0 +1,94 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class ErrorReporting { + + /** + * Returns the current raw value + * + * @return int The current value + */ + public static function get() { + return error_reporting(); + } + + /** + * Sets a new raw error reporting value + * + * @param int $level The new level to set + * @return int The new value + */ + public static function set($level) { + if(static::get() !== error_reporting($level)) { + throw new Exception('Internal error: error_reporting() did not return the old value.'); + } + return static::get(); + } + + /** + * Check if the current error reporting includes an error level + * + * @param mixed $level The level to check for + * @param int $current A custom current level + * @return boolean + */ + public static function includes($level, $current = null) { + // also allow strings + if(is_string($level)) { + if(defined($level)) { + $level = constant($level); + } else if(defined('E_' . strtoupper($level))) { + $level = constant('E_' . strtoupper($level)); + } else { + throw new Exception('The level "' . $level . '" does not exist.'); + } + } + + $value = ($current)? $current : static::get(); + return bitmask::includes($level, $value); + } + + /** + * Adds a level to the current error reporting + * + * @param int $level The level to add + * @return boolean + */ + public static function add($level) { + // check if it is already added + if(static::includes($level)) return false; + + $old = static::get(); + $newExpected = bitmask::add($level, $old); + $newActual = static::set($newExpected); + + return $newActual === $newExpected; + } + + /** + * Removes a level from the current error reporting + * + * @param int $level The level to remove + * @return boolean + */ + public static function remove($level) { + // check if it is already removed + if(!static::includes($level)) return false; + + $old = static::get(); + $newExpected = bitmask::remove($level, $old); + $newActual = static::set($newExpected); + + return $newActual === $newExpected; + } +} diff --git a/kirby/toolkit/lib/escape.php b/kirby/toolkit/lib/escape.php new file mode 100644 index 0000000..db748a0 --- /dev/null +++ b/kirby/toolkit/lib/escape.php @@ -0,0 +1,266 @@ + + * @link https://github.com/ezraverheijen/escape + * @copyright Ezra Verheijen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Escape { + + /** + * Check if a string needs to be escaped or not + * + * @param string $string + * @return boolean + */ + public static function noNeedToEscape($string) { + return $string === '' || ctype_digit($string); + } + + /** + * Convert a character from UTF-8 to UTF-16BE + * + * @param string $char + * @return string + */ + public static function convertEncoding($char) { + return str::convert($char, 'UTF-16BE', 'UTF-8'); + } + + /** + * Check if a character is undefined in HTML + * + * @param string $char + * @return boolean + */ + public static function charIsUndefined($char) { + $ascii = ord($char); + return ($ascii <= 0x1f && $char != "\t" && $char != "\n" && $char != "\r") + || ($ascii >= 0x7f && $ascii <= 0x9f); + } + + /** + * Escape HTML element content + * + * This can be used to put untrusted data directly into the HTML body somewhere. + * This includes inside normal tags like div, p, b, td, etc. + * + * Escapes &, <, >, ", and ' with HTML entity encoding to prevent switching + * into any execution context, such as script, style, or event handlers. + * + * ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE... + *
...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
+ * + * @uses ENT_SUBSTITUE if available (PHP >= 5.4) + * + * @param string $string + * @return string + */ + public static function html($string) { + $flags = ENT_QUOTES; + if(defined('ENT_SUBSTITUTE')) { + $flags |= ENT_SUBSTITUTE; + } + return htmlspecialchars($string, $flags, 'UTF-8'); + } + + /** + * Escape XML element content + * + * Removes offending characters that could be wrongfully interpreted as XML markup. + * + * The following characters are reserved in XML and will be replaced with their + * corresponding XML entities: + * + * ' is replaced with ' + * " is replaced with " + * & is replaced with & + * < is replaced with < + * > is replaced with > + * + * @uses ENT_XML1 if available (PHP >= 5.4) + * + * @param string $string + * @return string + */ + public static function xml($string) { + if (defined('ENT_XML1')) { + return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8'); + } else { + return str_replace(''', ''', htmlspecialchars($string, ENT_QUOTES, 'UTF-8')); + } + } + + /** + * Escape common HTML attributes data + * + * This can be used to put untrusted data into typical attribute values + * like width, name, value, etc. + * + * This should not be used for complex attributes like href, src, style, + * or any of the event handlers like onmouseover. + * Use esc($string, 'js') for event handler attributes, esc($string, 'url') + * for src attributes and esc($string, 'css') for style attributes. + * + *
content
+ *
content
+ *
content
+ * + * @param string $string + * @param string $strict Whether to escape characters like [space] % * + , - / ; < = > ^ and | + * which is necessary in case of unquoted HTML attributes. + * @return string + */ + public static function attr($string, $strict = false) { + if(static::noNeedToEscape($string)) return $string; + if($strict !== true) { + return preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', 'static::escapeAttrChar', $string); + } + return static::html($string); + } + + /** + * Escape JavaScript data values + * + * This can be used to put dynamically generated JavaScript code + * into both script blocks and event-handler attributes. + * + * + * + *
+ * + * @param string $string + * @return string + */ + public static function js($string) { + if(static::noNeedToEscape($string)) return $string; + return preg_replace_callback('/[^a-z0-9,\._]/iSu', 'static::escapeJSChar', $string); + } + + /** + * Escape HTML style property values + * + * This can be used to put untrusted data into a stylesheet or a style tag. + * + * Stay away from putting untrusted data into complex properties like url, + * behavior, and custom (-moz-binding). You should also not put untrusted data + * into IE’s expression property value which allows JavaScript. + * + * + * + * text + * + * @param string $string + * @return string + */ + public static function css($string) { + if(static::noNeedToEscape($string)) return $string; + return preg_replace_callback('/[^a-z0-9]/iSu', 'static::escapeCSSChar', $string); + } + + /** + * Escape URL parameter values + * + * This can be used to put untrusted data into HTTP GET parameter values. + * This should not be used to escape an entire URI. + * + * link + * + * @param string $string + * @return string + */ + public static function url($string) { + return rawurlencode($string); + } + + /** + * Escape character for HTML attribute + * + * Callback function for preg_replace_callback() that applies HTML attribute + * escaping to all matches. + * + * @param array $matches + * @return mixed Unicode replacement if character is undefined in HTML, + * named HTML entity if available (only those that XML supports), + * upper hex entity if a named entity does not exist or + * entity with the &#xHH; format if ASCII value is less than 256. + */ + protected static function escapeAttrChar($matches) { + $char = $matches[0]; + + if(static::charIsUndefined($char)) { + return '�'; + } + + $dec = hexdec(bin2hex($char)); + + $namedEntities = array( + 34 => '"', // " + 38 => '&', // & + 60 => '<', // < + 62 => '>' // > + ); + + if(isset($namedEntities[$dec])) { + return $namedEntities[$dec]; + } + + if($dec > 255) { + return sprintf('&#x%04X;', $dec); + } + + return sprintf('&#x%02X;', $dec); + } + + /** + * Escape character for JavaScript + * + * Callback function for preg_replace_callback() that applies Javascript + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected static function escapeJSChar($matches) { + $char = $matches[0]; + if(str::length($char) == 1) { + return sprintf('\\x%02X', ord($char)); + } + $char = static::convertEncoding($char); + return sprintf('\\u%04s', str::upper(bin2hex($char))); + } + + /** + * Escape character for CSS + * + * Callback function for preg_replace_callback() that applies CSS + * escaping to all matches. + * + * @param array $matches + * @return string + */ + protected static function escapeCSSChar($matches) { + $char = $matches[0]; + if(str::length($char) == 1) { + $ord = ord($char); + } else { + $char = static::convertEncoding($char); + $ord = hexdec(bin2hex($char)); + } + return sprintf('\\%X ', $ord); + } + +} diff --git a/kirby/toolkit/lib/exif.php b/kirby/toolkit/lib/exif.php new file mode 100644 index 0000000..02cd717 --- /dev/null +++ b/kirby/toolkit/lib/exif.php @@ -0,0 +1,221 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Exif { + + // the parent media object + protected $media = null; + + // the raw exif array + protected $data = null; + + // the camera object with model and make + protected $camera = null; + + // the location object + protected $location = null; + + // the timestamp + protected $timestamp = null; + + // the exposure value + protected $exposure = null; + + // the aperture value + protected $aperture = null; + + // iso value + protected $iso = null; + + // focal length + protected $focalLength = null; + + // color or black/white + protected $isColor = null; + + /** + * Constructor + * + * @param Media $media + */ + public function __construct(Media $media) { + $this->media = $media; + $this->parse(); + } + + /** + * Returns the raw data array from the parser + * + * @return array + */ + public function data() { + return $this->data; + } + + /** + * Returns the Camera object + * + * @return object KirbyExifCamera + */ + public function camera() { + + if(!is_null($this->camera)) return $this->camera; + + // check for valid exif data + if(!is_array($this->data)) return null; + + // initialize and return it + return $this->camera = new Exif\Camera($this->data); + + } + + /** + * Returns the location object + * + * @return object ExifLocation + */ + public function location() { + + if(!is_null($this->location)) return $this->location; + + // check for valid exif data + if(!is_array($this->data)) return null; + + // initialize and return it + return $this->location = new Exif\Location($this->data); + + } + + /** + * Returns the timestamp + * + * @return string + */ + public function timestamp() { + return $this->timestamp; + } + + /** + * Returns the exposure + * + * @return string + */ + public function exposure() { + return $this->exposure; + } + + /** + * Returns the aperture + * + * @return string + */ + public function aperture() { + return $this->aperture; + } + + /** + * Returns the iso value + * + * @return int + */ + public function iso() { + return $this->iso; + } + + /** + * Checks if this is a color picture + * + * @return boolean + */ + public function isColor() { + return $this->isColor; + } + + /** + * Checks if this is a bw picture + * + * @return boolean + */ + public function isBW() { + return !$this->isColor; + } + + /** + * Returns the focal length + * + * @return string + */ + public function focalLength() { + return $this->focalLength; + } + + /** + * Pareses and stores all relevant exif data + */ + protected function parse() { + + // read the exif data of the media object if possible + $this->data = @read_exif_data($this->media->root()); + + // stop on invalid exif data + if(!is_array($this->data)) return false; + + // store the timestamp when the picture has been taken + if(isset($this->data['DateTimeOriginal'])) { + $this->timestamp = strtotime($this->data['DateTimeOriginal']); + } else { + $this->timestamp = a::get($this->data, 'FileDateTime', $this->media->modified()); + } + + // exposure + $this->exposure = a::get($this->data, 'ExposureTime'); + + // iso + $this->iso = a::get($this->data, 'ISOSpeedRatings'); + + // focal length + if(isset($this->data['FocalLength'])) { + $this->focalLength = $this->data['FocalLength']; + } else if(isset($this->data['FocalLengthIn35mmFilm'])) { + $this->focalLength = $this->data['FocalLengthIn35mmFilm']; + } + + // aperture + $this->aperture = @$this->data['COMPUTED']['ApertureFNumber']; + + // color or bw + $this->isColor = @$this->data['COMPUTED']['IsColor'] == true; + + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray() { + + return array( + 'camera' => $this->camera()->toArray(), + 'location' => $this->location()->toArray(), + 'timestamp' => $this->timestamp(), + 'exposure' => $this->exposure(), + 'aperture' => $this->aperture(), + 'iso' => $this->iso(), + 'focalLength' => $this->focalLength(), + 'isColor' => $this->isColor() + ); + + } + +} diff --git a/kirby/toolkit/lib/exif/camera.php b/kirby/toolkit/lib/exif/camera.php new file mode 100644 index 0000000..7bfeff0 --- /dev/null +++ b/kirby/toolkit/lib/exif/camera.php @@ -0,0 +1,68 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Camera { + + protected $make; + protected $model; + + /** + * Constructor + * + * @param array $exif + */ + public function __construct($exif) { + $this->make = @$exif['Make']; + $this->model = @$exif['Model']; + } + + /** + * Returns the make of the camera + * + * @return string + */ + public function make() { + return $this->make; + } + + /** + * Returns the camera model + * + * @return string + */ + public function model() { + return $this->model; + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray() { + return array( + 'make' => $this->make, + 'model' => $this->model + ); + } + + /** + * Returns the full make + model name + * + * @return string + */ + public function __toString() { + return trim($this->make . ' ' . $this->model); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/exif/location.php b/kirby/toolkit/lib/exif/location.php new file mode 100644 index 0000000..a62ab66 --- /dev/null +++ b/kirby/toolkit/lib/exif/location.php @@ -0,0 +1,118 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Location { + + // latitude + protected $lat; + + // longitude + protected $lng; + + /** + * Constructor + * + * @param array $exif The entire exif array + */ + public function __construct($exif) { + + if( + isset($exif['GPSLatitude']) && + isset($exif['GPSLatitudeRef']) && + isset($exif['GPSLongitude']) && + isset($exif['GPSLongitudeRef']) + ) { + $this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']); + $this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']); + } + + } + + /** + * Returns the latitude + * + * @return float + */ + public function lat() { + return $this->lat; + } + + /** + * Returns the longitude + * + * @return float + */ + public function lng() { + return $this->lng; + } + + /** + * Converts the gps coordinates + * + * @param string $coord + * @param string $hemi + * @return float + */ + protected function gps($coord, $hemi) { + + $degrees = count($coord) > 0 ? $this->num($coord[0]) : 0; + $minutes = count($coord) > 1 ? $this->num($coord[1]) : 0; + $seconds = count($coord) > 2 ? $this->num($coord[2]) : 0; + + $hemi = strtoupper($hemi); + $flip = ($hemi == 'W' || $hemi == 'S') ? -1 : 1; + + return $flip * ($degrees + $minutes / 60 + $seconds / 3600); + + } + + /** + * Converts coordinates to floats + * + * @param string $part + * @return float + */ + protected function num($part) { + + $parts = explode('/', $part); + + if(count($parts) <= 0) return 0; + if(count($parts) == 1) return $parts[0]; + + return floatval($parts[0]) / floatval($parts[1]); + + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray() { + return array( + 'lat' => $this->lat(), + 'lng' => $this->lng() + ); + } + + /** + * Echos the entire location as lat, lng + * + * @return string + */ + public function __toString() { + return trim(trim($this->lat() . ', ' . $this->lng(), ',')); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/f.php b/kirby/toolkit/lib/f.php new file mode 100644 index 0000000..dd27d6c --- /dev/null +++ b/kirby/toolkit/lib/f.php @@ -0,0 +1,794 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class F { + + public static $mimes = array( + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'csv' => array('text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'), + 'bin' => 'application/macbinary', + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => array('application/octet-stream', 'application/x-msdownload'), + 'class' => 'application/octet-stream', + 'psd' => 'application/x-photoshop', + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => array('application/pdf', 'application/x-download'), + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'php' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'php3' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'phtml' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'phps' => array('text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'), + 'js' => 'application/x-javascript', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => array('application/x-tar', 'application/x-gzip-compressed'), + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed'), + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => array('audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'), + 'aif' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'ico' => 'image/x-icon', + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => array('text/plain', 'text/x-log'), + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xls' => array('application/excel', 'application/vnd.ms-excel', 'application/msexcel'), + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'ppt' => array('application/powerpoint', 'application/vnd.ms-powerpoint'), + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'word' => array('application/msword', 'application/octet-stream'), + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => array('application/json', 'text/json'), + 'odt' => 'application/vnd.oasis.opendocument.text', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + ); + + public static $types = array( + + 'image' => array( + 'jpeg', + 'jpg', + 'jpe', + 'gif', + 'png', + 'svg', + 'ico', + 'tif', + 'tiff', + 'bmp', + 'psd', + 'ai', + 'eps', + 'ps' + ), + + 'document' => array( + 'txt', + 'text', + 'mdown', + 'md', + 'markdown', + 'pdf', + 'doc', + 'docx', + 'dotx', + 'word', + 'xl', + 'xls', + 'xlsx', + 'xltx', + 'ppt', + 'pptx', + 'potx', + 'csv', + 'rtf', + 'rtx', + 'log', + 'odt', + 'odp', + 'odc', + ), + + 'archive' => array( + 'zip', + 'tar', + 'gz', + 'gzip', + 'tgz', + ), + + 'code' => array( + 'js', + 'css', + 'scss', + 'htm', + 'html', + 'shtml', + 'xhtml', + 'php', + 'php3', + 'php4', + 'rb', + 'xml', + 'json', + 'java', + 'py' + ), + + 'video' => array( + 'mov', + 'movie', + 'avi', + 'ogg', + 'ogv', + 'webm', + 'flv', + 'swf', + 'mp4', + 'm4v', + 'mpg', + 'mpe' + ), + + 'audio' => array( + 'mp3', + 'm4a', + 'wav', + 'aif', + 'aiff', + 'midi', + ), + + ); + + public static $units = array('B','kB','MB','GB','TB','PB', 'EB', 'ZB', 'YB'); + + /** + * Checks if a file exists + * + * @param string $file + * @return boolean + */ + public static function exists($file) { + return file_exists($file); + } + + /** + * Safely requires a file if it exists + */ + public static function load($file, $data = array()) { + if(file_exists($file)) { + extract($data); + require($file); + } + } + + /** + * Creates a new file + * + * + * + * f::write('test.txt', 'hello'); + * // creates a new text file with hello as content + * + * // create a new file + * f::write('text.txt', array('test' => 'hello')); + * // creates a new file and encodes the array as json + * + * + * + * @param string $file The path for the new file + * @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized. + * @param boolean $append true: append the content to an exisiting file if available. false: overwrite. + * @return boolean + */ + public static function write($file, $content, $append = false) { + if(is_array($content) || is_object($content)) $content = serialize($content); + $mode = ($append) ? FILE_APPEND | LOCK_EX : LOCK_EX; + // if the parent directory does not exist, create it + if(!is_dir(dirname($file))) { + if(!dir::make(dirname($file))) return false; + } + return (@file_put_contents($file, $content, $mode) !== false) ? true : false; + } + + /** + * Appends new content to an existing file + * + * @param string $file The path for the file + * @param mixed $content Either a string or an array. Arrays will be converted to JSON. + * @return boolean + */ + public static function append($file, $content) { + return static::write($file,$content,true); + } + + /** + * Reads the content of a file + * + * + * + * $content = f::read('test.txt'); + * // i.e. content is hello + * + * $content = f::read('text.txt', 'json'); + * // returns an array with the parsed content + * + * + * + * @param string $file The path for the file + * @return mixed + */ + public static function read($file) { + return @file_get_contents($file); + } + + /** + * Returns the file content as base64 encoded string + * + * @param string $file The path for the file + * @return string + */ + public static function base64($file) { + return base64_encode(f::read($file)); + } + + /** + * Returns the file as data uri + * + * @param string $file The path for the file + * @return string + */ + public static function uri($file) { + $mime = static::mime($file); + return ($mime) ? 'data:' . $mime . ';base64,' . static::base64($file) : false; + } + + /** + * Moves a file to a new location + * + * + * + * $move = f::move('test.txt', 'super.txt'); + * + * if($move) echo 'The file has been moved'; + * + * + * + * @param string $old The current path for the file + * @param string $new The path to the new location + * @return boolean + */ + public static function move($old, $new) { + if(!file_exists($old) || file_exists($new)) return false; + return @rename($old, $new); + } + + /** + * Copy a file to a new location. + * + * @param string $file + * @param string $target + * @return boolean + */ + public static function copy($file, $target) { + if(!file_exists($file) || file_exists($target)) return false; + return @copy($file, $target); + } + + /** + * Deletes a file + * + * + * + * $remove = f::remove('test.txt'); + * if($remove) echo 'The file has been removed'; + * + * + * + * @param string $file The path for the file + * @return boolean + */ + public static function remove($file) { + return file_exists($file) && is_file($file) && !empty($file) ? @unlink($file) : false; + } + + /** + * Gets the extension of a file + * + * + * + * $extension = f::extension('test.txt'); + * // extension is txt + * + * + * + * @param string $file The filename or path + * @param string $extension Set an optional extension to overwrite the current one + * @return string + */ + public static function extension($file, $extension = false) { + + // overwrite the current extension + if($extension !== false) { + return static::name($file) . '.' . $extension; + } + + // return the current extension + return strtolower(pathinfo($file, PATHINFO_EXTENSION)); + + } + + /** + * Returns all extensions for a certain file type + * + * @param string $type + * @return array + */ + public static function extensions($type = null) { + if(is_null($type)) return array_keys(static::$mimes); + return isset(static::$types[$type]) ? static::$types[$type] : array(); + } + + /** + * Extracts the filename from a file path + * + * + * + * $filename = f::filename('/var/www/test.txt'); + * // filename is test.txt + * + * + * + * @param string $name The path + * @return string + */ + public static function filename($name) { + return pathinfo($name, PATHINFO_BASENAME); + } + + /** + * Extracts the name from a file path or filename without extension + * + * + * + * $name = f::name('/var/www/test.txt'); + * + * // name is test + * + * + * + * @param string $name The path or filename + * @return string + */ + public static function name($name) { + return pathinfo($name, PATHINFO_FILENAME); + } + + /** + * Just an alternative for dirname() to stay consistent + * + * + * + * $dirname = f::dirname('/var/www/test.txt'); + * // dirname is /var/www + * + * + * + * @param string $file The path + * @return string + */ + public static function dirname($file) { + return dirname($file); + } + + /** + * Returns the size of a file. + * + * + * + * $size = f::size('/var/www/test.txt'); + * // size is ie: 1231939 + * + * + * + * @param mixed $file The path + * @return mixed + */ + public static function size($file) { + return filesize($file); + } + + /** + * Converts an integer size into a human readable format + * + * + * + * $niceSize = f::niceSize('/path/to/a/file.txt'); + * // nice size is i.e: 212 kb + * + * $niceSize = f::niceSize(1231939); + * // nice size is: 1,2 mb + * + * + * + * @param mixed $size The file size or a file path + * @return string + */ + public static function niceSize($size) { + + // file mode + if(is_string($size) && file_exists($size)) { + $size = static::size($size); + } + + // make sure it's an int + $size = (int)$size; + + // avoid errors for invalid sizes + if($size <= 0) return '0 kB'; + + // the math magic + return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . static::$units[$i]; + + } + + /** + * Get the file's last modification time. + * + * @param string $file + * @param string $format + * @param string $handler date or strftime + * @return int + */ + public static function modified($file, $format = null, $handler = 'date') { + if(file_exists($file)) { + return !is_null($format) ? $handler($format, filemtime($file)) : filemtime($file); + } else { + return false; + } + } + + /** + * Returns the mime type of a file + * + * @param string $file + * @return mixed + */ + public static function mime($file) { + + // stop for invalid files + if(!file_exists($file)) return null; + + // Fileinfo is prefered if available + if(function_exists('finfo_file')) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file); + finfo_close($finfo); + return $mime; + } + + // for older versions with mime_content_type go for that. + if(function_exists('mime_content_type') && $mime = @mime_content_type($file) !== false) { + return $mime; + } + + // shell check + try { + $mime = system::execute('file', [$file, '-z', '-b', '--mime'], 'output'); + $mime = trim(str::split($mime, ';')[0]); + if(f::mimeToExtension($mime)) return $mime; + } catch(Exception $e) { + // no mime type detectable with shell + $mime = null; + } + + // Mime Sniffing + $reader = new MimeReader($file); + $mime = $reader->get_type(); + + if(!empty($mime) && f::mimeToExtension($mime)) { + return $mime; + } + + // guess the matching mime type by extension + return f::extensionToMime(f::extension($file)); + + } + + /** + * Returns all detectable mime types + * + * @return array + */ + public static function mimes() { + return static::$mimes; + } + + /** + * Categorize the file + * + * @param string $file Either the file path or extension + * @return string + */ + public static function type($file) { + + $length = strlen($file); + + if($length >= 2 && $length <= 4) { + // use the file name as extension + $extension = $file; + } else { + // get the extension from the filename + $extension = pathinfo($file, PATHINFO_EXTENSION); + } + + if(empty($extension)) { + // detect the mime type first to get the most reliable extension + $mime = static::mime($file); + $extension = static::mimeToExtension($mime); + } + + // sanitize extension + $extension = strtolower($extension); + + foreach(static::$types as $type => $extensions) { + if(in_array($extension, $extensions)) return $type; + } + + return null; + + } + + /** + * Returns an array of all available file types + * + * @return array + */ + public static function types() { + return static::$types; + } + + /** + * Checks if a file is of a certain type + * + * @param string $file Full path to the file + * @param string $value An extension or mime type + * @return boolean + */ + public static function is($file, $value) { + + if(in_array($value, static::extensions())) { + // check for the extension + return static::extension($file) == $value; + } else if(strpos($value, '/') !== false) { + // check for the mime type + return static::mime($file) == $value; + } + + return false; + + } + + /** + * Converts a mime type to a file extension + * + * @param string $mime + * @return string + */ + public static function mimeToExtension($mime) { + foreach(static::$mimes as $key => $value) { + if(is_array($value) && in_array($mime, $value)) return $key; + if($value == $mime) return $key; + } + return null; + } + + /** + * Returns the type for a given mime + * + * @param string $mime + * @return string + */ + public static function mimeToType($mime) { + return static::extensionToType(static::mimeToExtension($mime)); + } + + /** + * Converts a file extension to a mime type + * + * @param string $extension + * @return string + */ + public static function extensionToMime($extension) { + $mime = isset(static::$mimes[$extension]) ? static::$mimes[$extension] : null; + return is_array($mime) ? array_shift($mime) : $mime; + } + + /** + * Returns the file type for a passed extension + * + * @param string $extension + * @return string + */ + public static function extensionToType($extension) { + + // get all categorized types + foreach(static::$types as $type => $extensions) { + if(in_array($extension, $extensions)) return $type; + } + + return null; + + } + + /** + * Sanitize a filename to strip unwanted special characters + * + * + * + * $safe = f::safeName('über genious.txt'); + * // safe will be ueber-genious.txt + * + * + * + * @param string $string The file name + * @return string + */ + public static function safeName($string) { + $name = static::name($string); + $extension = static::extension($string); + $end = !empty($extension) ? '.' . str::slug($extension) : ''; + return str::slug($name, '-', 'a-z0-9@._-') . $end; + } + + /** + * Checks if the file is writable + * + * @param string $file + * @return boolean + */ + public static function isWritable($file) { + return is_writable($file); + } + + /** + * Checks if the file is readable + * + * @param string $file + * @return boolean + */ + public static function isReadable($file) { + return is_readable($file); + } + + /** + * Read and send the file with the correct headers + * + * @param string $file + */ + public static function show($file) { + + // stop the download if the file does not exist or is not readable + if(!is_file($file) || !is_readable($file)) return false; + + // send the browser headers + header::type(f::mime($file)); + + // send the file + die(f::read($file)); + + } + + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string $file The root to the file + * @param string $name Optional filename for the download + */ + public static function download($file, $name = null) { + + // stop the download if the file does not exist or is not readable + if(!is_file($file) || !is_readable($file)) return false; + + header::download(array( + 'name' => $name ? $name : f::filename($file), + 'size' => f::size($file), + 'mime' => f::mime($file), + 'modified' => f::modified($file) + )); + + die(f::read($file)); + + } + + /** + * Tries to find a file by various extensions + * + * @param string $base + * @param array $extensions + * @return string|false + */ + public static function resolve($base, $extensions) { + foreach($extensions as $ext) { + $file = $base . '.' . $ext; + if(file_exists($file)) return $file; + } + return false; + } + +} diff --git a/kirby/toolkit/lib/folder.php b/kirby/toolkit/lib/folder.php new file mode 100644 index 0000000..39e9d76 --- /dev/null +++ b/kirby/toolkit/lib/folder.php @@ -0,0 +1,406 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Folder { + + // the root for the directory + protected $root = null; + + // a cache for the scanned inventory + protected $inventory = null; + + /** + * Constructor + */ + public function __construct($root) { + if(file_exists($root) && is_file($root)) throw new Exception('Invalid folder: ' . $root); + $this->root = $root; + } + + /** + * Returns the root of the directory + */ + public function root() { + return $this->root; + } + + /** + * Returns a md5 hash of the root + */ + public function hash() { + return md5($this->root); + } + + /** + * Returns the name of the directory without the full path + * + * @return string + */ + public function name() { + return basename($this->root); + } + + /** + * Returns the parent directory object + * + * @return Directory + */ + public function parent() { + return new static(dirname($this->root)); + } + + /** + * Checks if the dir exists + * + * @return boolean + */ + public function exists() { + return is_dir($this->root); + } + + /** + * Creates the directory if it does not exist yet + * + * @param boolean $recursive + * @return boolean + */ + public function make($recursive = true) { + return dir::make($this->root, $recursive); + } + + /** + * Alternative for make + * + * @param boolean $recursive + * @return boolean + */ + public function create($recursive = true) { + return $this->make($recursive); + } + + /** + * Returns the entire content of the directory + * + * @return array + */ + public function inventory() { + if(!is_dir($this->root)) return array(); + return $this->inventory = is_null($this->inventory) ? scandir($this->root) : $this->inventory; + } + + /** + * Reads the directory content and returns an array with file objects + * + * @param array $ignore + * @return array + */ + public function scan($ignore = null) { + $skip = is_array($ignore) ? $ignore : a::get(dir::$defaults, 'ignore', array()); + return empty($skip) ? $this->inventory() : (array)array_diff($this->inventory(), $skip); + } + + /** + * Alternative for scan + * + * @param array $ignore + * @return array + */ + public function read($ignore = null) { + return $this->scan($ignore); + } + + /** + * Returns a collection with full File and Directory objects + * for each item in the directory + * + * @param array $ignore + * @return Collection + */ + public function content($ignore = null) { + $raw = $this->scan($ignore); + $root = $this->root; + $content = new Collection(); + + foreach($raw as $file) { + + if(is_dir($root . DS . $file)) { + $content->append($file, new static($root . DS . $file)); + } else { + $content->append($file, new Media($root . DS . $file)); + } + + } + + return $content; + + } + + /** + * Return a collection of all files within the directory + * + * @param array $ignore + * @param boolean $plain + * @return mixed When $plain is true an array will be returned. Otherwise a Collection + */ + public function files($ignore = null, $plain = false) { + + $raw = $this->scan($ignore); + + if($plain) { + + $content = array(); + + foreach($raw as $file) { + if(is_file($this->root . DS . $file)) $content[] = $file; + } + + } else { + + $content = new Collection(); + + foreach($raw as $file) { + if(is_file($this->root . DS . $file)) { + $content->append($file, new Media($this->root . DS . $file)); + } + } + + } + + return $content; + + } + + /** + * Return a collection of subfolders + * + * @param array $ignore + * @param boolean $plain + * @return mixed If $plain is true an array will be returned. Otherwise a Collection + */ + public function children($ignore = null, $plain = false) { + + $raw = $this->scan($ignore); + + if($plain) { + + $content = array(); + + foreach($raw as $file) { + if(is_dir($this->root . DS . $file)) $content[] = $file; + } + + } else { + + $content = new Collection(); + + foreach($raw as $file) { + if(is_dir($this->root . DS . $file)) { + $content->append($file, new static($this->root . DS . $file)); + } + } + + } + + return $content; + + } + + /** + * Returns a subfolder object by path + * + * @return mixed Directory + */ + public function child($path) { + $root = $this->root . DS . str_replace('/', DS, $path); + if(!is_dir($root)) return false; + return new static($root); + } + + /** + * Corresponding method to File::type() + * which makes it possible to filter a collection + * of files and directories by type. + * + * @return string + */ + public function type() { + return 'directory'; + } + + /** + * Moves the directory to a new location + * + * @param string $to + * @return boolean + */ + public function move($to) { + if(!dir::move($this->root, $to)) { + return false; + } else { + $this->root = true; + return true; + } + } + + /** + * Copies the directory to a new location + * + * @param string $to + * @return boolean + */ + public function copy($to) { + + // Make destination directory + $copy = new static($to); + if(!$copy->make()) return false; + + // Loop through all subfiles and folders + foreach($this->content() as $item) { + if(is_a($item, 'Folder')) { + $dest = $to . DS . $item->name(); + } else { + $dest = $to . DS . $item->filename(); + } + if(!$item->copy($dest)) return false; + } + + return $copy; + + } + + /** + * Deletes the directory + * + * @param boolean $keep Set this to true to keep the directory but delete all its content + * @return boolean + */ + public function delete($keep = false) { + $items = $this->content(array('.', '..')); + foreach($items as $item) $item->delete(); + return $keep ? true : @rmdir($this->root); + } + + /** + * Alternative for delete + * + * @param boolean $keep Set this to true to keep the directory but delete all its content + * @return boolean + */ + public function remove($keep = false) { + return $this->delete($keep); + } + + /** + * Deletes all contents of the directory + * + * @return boolean + */ + public function flush() { + return $this->delete(true); + } + + /** + * Alternative for flush + * + * @return boolean + */ + public function clean() { + return $this->delete(true); + } + + /** + * Returns the entire size of the directory and all its contents + * + * @return int + */ + public function size() { + + $size = 0; + $items = $this->content(array('.', '..')); + + foreach($items AS $item) $size += $item->size(); + return $size; + + } + + /** + * Returns the size as a human-readable string + * + * @return string + */ + public function niceSize() { + return f::niceSize($this->size()); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @return int + */ + public function modified($format = null, $handler = 'date') { + + $modified = filemtime($this->root); + $items = $this->scan(array('.', '..')); + + foreach($items AS $item) { + + if(is_file($this->root . DS . $item)) { + $newModified = filemtime($this->root . DS . $item); + } else { + $object = new static($this->root . DS . $item); + $newModified = $object->modified(); + } + + $modified = ($newModified > $modified) ? $newModified : $modified; + + } + + return !is_null($format) ? $handler($format, $modified) : $modified; + + } + + /** + * Checks if the directory is writable + * + * @param boolean $recursive + * @return boolean + */ + public function isWritable($recursive = false) { + if($recursive) { + if(!$this->isWritable()) return false; + foreach($this->content() as $f) { + if(!$f->isWritable(true)) return false; + } + return true; + } + return is_writable($this->root); + } + + /** + * Checks if the directory is readable + * + * @return boolean + */ + public function isReadable() { + return is_readable($this->root); + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString() { + return $this->root; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/header.php b/kirby/toolkit/lib/header.php new file mode 100644 index 0000000..7c87220 --- /dev/null +++ b/kirby/toolkit/lib/header.php @@ -0,0 +1,236 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Header { + + // configuration + public static $codes = array( + + // successful + '_200' => 'OK', + '_201' => 'Created', + '_202' => 'Accepted', + + // redirection + '_301' => 'Moved Permanently', + '_302' => 'Found', + '_303' => 'See Other', + '_304' => 'Not Modified', + '_307' => 'Temporary Redirect', + + // client error + '_400' => 'Bad Request', + '_401' => 'Unauthorized', + '_402' => 'Payment Required', + '_403' => 'Forbidden', + '_404' => 'Not Found', + '_405' => 'Method Not Allowed', + + // server error + '_500' => 'Internal Server Error', + '_501' => 'Not Implemented', + '_502' => 'Bad Gateway', + '_503' => 'Service Unavailable' + ); + + /** + * Sends a content type header + * + * @param string $mime + * @param string $charset + * @param boolean $send + * @return mixed + */ + public static function contentType($mime, $charset = 'UTF-8', $send = true) { + if(f::extensionToMime($mime)) $mime = f::extensionToMime($mime); + $header = 'Content-type: ' . $mime; + if($charset) $header .= '; charset=' . $charset; + if(!$send) return $header; + header($header); + } + + /** + * Shortcut for static::contentType() + * + * @param string $mime + * @param string $charset + * @param boolean $send + * @return mixed + */ + public static function type($mime, $charset = 'UTF-8', $send = true) { + return static::contentType($mime, $charset, $send); + } + + /** + * Sends a status header + * + * @param int $code The HTTP status code + * @param boolean $send If set to false the header will be returned instead + * @return mixed + */ + public static function status($code, $send = true) { + + $codes = static::$codes; + $code = !array_key_exists('_' . $code, $codes) ? 400 : $code; + $message = isset($codes['_' . $code]) ? $codes['_' . $code] : 'Something went wrong'; + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; + $header = $protocol . ' ' . $code . ' ' . $message; + + if(!$send) return $header; + + // try to send the header + header($header); + + } + + /** + * Sends a 200 header + * + * @param boolean $send + * @return mixed + */ + public static function success($send = true) { + return static::status(200, $send); + } + + /** + * Sends a 201 header + * + * @param boolean $send + * @return mixed + */ + public static function created($send = true) { + return static::status(201, $send); + } + + /** + * Sends a 202 header + * + * @param boolean $send + * @return mixed + */ + public static function accepted($send = true) { + return static::status(202, $send); + } + + /** + * Sends a 400 header + * + * @param boolean $send + * @return mixed + */ + public static function error($send = true) { + return static::status(400, $send); + } + + /** + * Sends a 403 header + * + * @param boolean $send + * @return mixed + */ + public static function forbidden($send = true) { + return static::status(403, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return mixed + */ + public static function notfound($send = true) { + return static::status(404, $send); + } + + /** + * Sends a 404 header + * + * @param boolean $send + * @return mixed + */ + public static function missing($send = true) { + return static::status(404, $send); + } + + /** + * Sends a 500 header + * + * @param boolean $send + * @return mixed + */ + public static function panic($send = true) { + return static::status(500, $send); + } + + /** + * Sends a 503 header + * + * @param boolean $send + * @return mixed + */ + public static function unavailable($send = true) { + return static::status(503, $send); + } + + /** + * Sends a redirect header + * + * @param boolean $send + * @return mixed + */ + public static function redirect($url, $code = 301, $send = true) { + + $status = static::status($code, false); + $location = 'Location:' . $url; + + if(!$send) { + return $status . PHP_EOL . $location; + } + + header($status); + header($location); + exit(); + + } + + /** + * Sends download headers for anything that is downloadable + * + * @param array $params Check out the defaults array for available parameters + */ + public static function download($params = array()) { + + $defaults = array( + 'name' => 'download', + 'size' => false, + 'mime' => 'application/force-download', + 'modified' => time() + ); + + $options = array_merge($defaults, $params); + + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Last-Modified: '. gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT'); + header('Cache-Control: private', false); + static::contentType($options['mime']); + header('Content-Disposition: attachment; filename="' . $options['name'] . '"'); + header('Content-Transfer-Encoding: binary'); + if($options['size']) header('Content-Length: ' . $options['size']); + header('Connection: close'); + + } + +} diff --git a/kirby/toolkit/lib/html.php b/kirby/toolkit/lib/html.php new file mode 100644 index 0000000..9db770e --- /dev/null +++ b/kirby/toolkit/lib/html.php @@ -0,0 +1,255 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Html { + + /** + * An internal store for a html entities translation table + * + * @return array + */ + public static $entities = array( + ' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§', + '¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯', + '°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·', + '¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿', + 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Æ' => 'Æ', 'Ç' => 'Ç', + 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', + 'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×', + 'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß', + 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç', + 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', + 'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷', + 'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ', + 'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', + 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', + 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', + 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', + 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', + 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', + 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′', + '″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', + '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', + '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', + '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', + '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', + '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', + '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', + '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', + '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦', '"' => '"', '&' => '&', '<' => '<', '>' => '>', 'Œ' => 'Œ', + 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ', + ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', + '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰', + '‹' => '‹', '›' => '›', '€' => '€' + ); + + /** + * Checks if a tag is self-closing + * + * @param string $tag + * @return param + */ + public static function isVoid($tag) { + + $void = array( + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + ); + + return in_array(strtolower($tag), $void); + + } + + /** + * Returns the full array with all HTML entities + * + * @return array + */ + public static function entities() { + return static::$entities; + } + + /** + * Converts a string to a html-safe string + * + * @param string $string + * @param boolean $keepTags True: lets stuff inside html tags untouched. + * @return string The html string + */ + public static function encode($string, $keepTags = true) { + if($keepTags) { + return stripslashes(implode('', preg_replace_callback('/^([^<].+[^>])$/', function($match) { + return htmlentities($match[1], ENT_COMPAT, 'utf-8'); + }, preg_split('/(<.+?>)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE)))); + } else { + return htmlentities($string, ENT_COMPAT, 'utf-8'); + } + } + + /** + * Removes all html tags and encoded chars from a string + * + * + * + * echo html::decode('some crazy stuff'); + * // output: some uber crazy stuff + * + * + * + * @param string $string + * @return string The html string + */ + public static function decode($string) { + $string = strip_tags($string); + return html_entity_decode($string, ENT_COMPAT, 'utf-8'); + } + + /** + * Converts lines in a string into html breaks + * + * @param string $string + * @return string + */ + public static function breaks($string) { + return nl2br($string); + } + + /** + * Generates an Html tag with optional content and attributes + * + * @param string $name The name of the tag, i.e. "a" + * @param mixed $content The content if availble. Pass null to generate a self-closing tag, Pass an empty string to generate empty content + * @param array $attr An associative array with additional attributes for the tag + * @return string The generated Html + */ + public static function tag($name, $content = null, $attr = array()) { + + if(is_array($content)) { + $attr = $content; + $content = null; + } + + $html = '<' . $name; + $attr = static::attr($attr); + + if(!empty($attr)) $html .= ' ' . $attr; + + if(static::isVoid($name)) { + $html .= '>'; + } else { + $html .= '>' . $content . ''; + } + + return $html; + + } + + /** + * Generates a single attribute or a list of attributes + * + * @param string $name mixed string: a single attribute with that name will be generated. array: a list of attributes will be generated. Don't pass a second argument in that case. + * @param string $value if used for a single attribute, pass the content for the attribute here + * @return string the generated html + */ + public static function attr($name, $value = null) { + if(is_array($name)) { + $attributes = array(); + foreach($name as $key => $val) { + $a = static::attr($key, $val); + if($a) $attributes[] = $a; + } + return implode(' ', $attributes); + } + + if(empty($value) && $value !== '0' && $value !== 0) { + return false; + } else if($value === ' ') { + return strtolower($name) . '=""'; + } else if(is_bool($value)) { + return $value === true ? strtolower($name) : ''; + } else { + return strtolower($name) . '="' . ( is_array($value) ? implode(' ', $value) : $value ) . '"'; + } + + } + + /** + * Generates an a tag + * + * @param string $href The url for the a tag + * @param mixed $text The optional text. If null, the url will be used as text + * @param array $attr Additional attributes for the tag + * @return string the generated html + */ + public static function a($href, $text = null, $attr = array()) { + $attr = array_merge(array('href' => $href), $attr); + if(empty($text)) $text = $href; + return static::tag('a', $text, $attr); + } + + /** + * Generates an "a mailto" tag + * + * @param string $email The url for the a tag + * @param mixed $text The optional text. If null, the url will be used as text + * @param array $attr Additional attributes for the tag + * @return string the generated html + */ + public static function email($email, $text = null, $attr = array()) { + if(empty($text)) { + // show only the eMail address without additional parameters (if the 'text' argument is empty) + $text = str::encode(a::first(str::split($email, '?'))); + } + $email = str::encode($email); + $attr = array_merge(array('href' => 'mailto:' . $email), $attr); + return static::tag('a', $text, $attr); + } + + /** + * Generates an img tag + * + * @param string $src The url of the image + * @param array $attr Additional attributes for the image tag + * @return string the generated html + */ + public static function img($src, $attr = array()) { + $attr = array_merge(array('src' => $src, 'alt' => pathinfo($src, PATHINFO_FILENAME)), $attr); + return static::tag('img', null, $attr); + } + + /** + * Generates a HTML5 shiv script tag with additional comments for older IEs + * + * @return string the generated html + */ + public static function shiv() { + return '' . PHP_EOL; + } + +} diff --git a/kirby/toolkit/lib/i.php b/kirby/toolkit/lib/i.php new file mode 100644 index 0000000..59de3fe --- /dev/null +++ b/kirby/toolkit/lib/i.php @@ -0,0 +1,105 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class I implements Iterator { + + public $data = array(); + + /** + * Constructor + * + * @param array $data + */ + public function __construct($data = array()) { + if(is_array($data)) $this->data = $data; + } + + /** + * Checks if the current key is set + * + * `isset($mycollection->mykey)` + * + * @param string $key the key to check + * @return boolean + */ + public function __isset($key) { + return isset($this->data[$key]); + } + + /** + * Removes an element from the array by key + * + * `unset($mycollection->mykey)` + * + * @param string $key the name of the key + */ + public function __unset($key) { + unset($this->data[$key]); + } + + /** + * Moves the cusor to the first element of the array + */ + public function rewind() { + reset($this->data); + } + + /** + * Returns the current element of the array + * + * @return mixed + */ + public function current() { + return current($this->data); + } + + /** + * Returns the current key from the array + * + * @return string + */ + public function key() { + return key($this->data); + } + + /** + * Moves the cursor to the previous element in the array + * and returns it + * + * @return mixed + */ + public function prev() { + return prev($this->data); + } + + /** + * Moves the cursor to the next element in the array + * and returns it + * + * @return mixed + */ + public function next() { + return next($this->data); + } + + /** + * Checks if an element is valid + * This is needed for the Iterator implementation + * + * @return boolean + */ + public function valid() { + return $this->current() !== false; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/l.php b/kirby/toolkit/lib/l.php new file mode 100644 index 0000000..7c607eb --- /dev/null +++ b/kirby/toolkit/lib/l.php @@ -0,0 +1,17 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class L extends Silo { + public static $data = array(); +} \ No newline at end of file diff --git a/kirby/toolkit/lib/media.php b/kirby/toolkit/lib/media.php new file mode 100644 index 0000000..b5c4e9c --- /dev/null +++ b/kirby/toolkit/lib/media.php @@ -0,0 +1,639 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Media { + + // optional url where the file is reachable + public $url = null; + + // the full path for the file + protected $root = null; + + // the filename including the extension + protected $filename = null; + + // the name excluding the extension + protected $name = null; + + // the extension of the file + protected $extension = null; + + // the content of the file + protected $content = null; + + // cache for various data + protected $cache = array(); + + /** + * Constructor + * + * @param string $root + */ + public function __construct($root, $url = null) { + $this->url = $url; + $this->root = $root === null ? $root : realpath($root); + $this->filename = basename($root); + $this->name = pathinfo($root, PATHINFO_FILENAME); + $this->extension = strtolower(pathinfo($root, PATHINFO_EXTENSION)); + } + + /** + * Resets the internal cache + */ + public function reset() { + $this->cache = array(); + } + + /** + * Returns the full root of the asset + * + * @return string + */ + public function root() { + return $this->root; + } + + /** + * Returns the url + * + * @return string + */ + public function url() { + return $this->url; + } + + /** + * Returns a md5 hash of the root + */ + public function hash() { + return md5($this->root); + } + + /** + * Returns the parent directory path + * + * @return string + */ + public function dir() { + return dirname($this->root); + } + + /** + * Returns the filename of the file + * i.e. somefile.jpg + * + * @return string + */ + public function filename() { + return $this->filename; + } + + /** + * Returns the name of the file without extension + * + * @return string + */ + public function name() { + return $this->name; + } + + /** + * Returns the filename as safe name + * + * @return string + */ + public function safeName() { + return f::safeName($this->filename()); + } + + /** + * Returns the extension of the file + * i.e. jpg + * + * @return string + */ + public function extension() { + // return the current extension + return $this->extension; + } + + /** + * Reads the file content and parses it + * + * @param string $format + * @return mixed + */ + public function read($format = null) { + return str::parse($this->content(), $format); + } + + /** + * Setter and getter for the file content + * + * @param string $content + * @return string + */ + public function content($content = null, $format = null) { + + if(!is_null($content)) { + if(is_array($content)) { + switch($format) { + case 'json': + $content = json_encode($content); + break; + case 'yaml': + $content = yaml::encode($content); + break; + default: + $content = serialize($content); + break; + } + } else if(is_object($content)) { + $content = serialize($content); + } + return $this->content = $content; + } + + if(is_null($this->content)) { + $this->content = file_get_contents($this->root); + } + + return $this->content; + + } + + /** + * Saves the file + * + * @param string $content + * @return boolean + */ + public function save($content = null, $format = null) { + $content = $this->content($content, $format); + return f::write($this->root, $content); + } + + /** + * Alternative for save + * + * @param string $content + * @return boolean + */ + public function write($content = null, $format = null) { + return $this->save($content, $format); + } + + /** + * Change the file's modification date to now + * and create it with an empty content if it is not there yet + * + * @return boolean + */ + public function touch() { + return touch($this->root); + } + + /** + * Appends the content and saves the file + * + * @param string $content + * @return boolean + */ + public function append($content) { + $this->content = $this->content() . $content; + return $this->save(); + } + + /** + * Deletes the file + * + * @return boolean + */ + public function delete() { + return f::remove($this->root); + } + + /** + * Alternative for delete + * + * @return boolean + */ + public function remove() { + return f::remove($this->root); + } + + /** + * Moves the file to a new location + * + * @param string $to + * @return boolean + */ + public function move($to) { + if(!f::move($this->root, $to)) { + return false; + } else { + $this->root = $to; + return true; + } + } + + /** + * Copies the file to a new location + * + * @param string $to + * @return boolean + */ + public function copy($to) { + return f::copy($this->root, $to); + } + + /** + * Returns the file size as integer + * + * @return int + */ + public function size() { + return f::size($this->root); + } + + /** + * Returns the human readable version of the file size + * + * @return string + */ + public function niceSize() { + return f::niceSize($this->size()); + } + + /** + * Get the file's last modification time. + * + * @return int + */ + public function modified($format = null, $handler = 'date') { + return f::modified($this->root, $format, $handler); + } + + /** + * Returns the mime type of a file + * + * @return string + */ + public function mime() { + return f::mime($this->root); + } + + /** + * Categorize the file + * + * @return string + */ + public function type() { + return f::type($this->root); + } + + /** + * Checks if a file is of a certain type + * + * @param string $value An extension or mime type + * @return boolean + */ + public function is($value) { + return f::is($this->root, $value); + } + + /** + * Returns the file content as base64 encoded string + * + * @return string + */ + public function base64() { + return base64_encode($this->content()); + } + + /** + * Returns the file as data uri + * + * @return string + */ + public function dataUri() { + return 'data:' . $this->mime() . ';base64,' . $this->base64(); + } + + /** + * Checks if the file exists + * + * @return boolean + */ + public function exists() { + return file_exists($this->root); + } + + /** + * Checks if the file is writable + * + * @return boolean + */ + public function isWritable() { + return is_writable($this->root); + } + + /** + * Checks if the file is readable + * + * @return boolean + */ + public function isReadable() { + return is_readable($this->root); + } + + /** + * Checks if the file is executable + * + * @return boolean + */ + public function isExecutable() { + return is_executable($this->root); + } + + /** + * Sends an appropriate header for the asset + * + * @param boolean $send + * @return mixed + */ + public function header($send = true) { + return header::type($this->mime(), false, $send); + } + + /** + * Safely requires a file if it exists + * + * @param array $data Optional variables, which will be made available to the file + */ + public function load($data = array()) { + return f::load($this->root, $data); + } + + /** + * Read and send the file with the correct headers + */ + public function show() { + f::show($this->root); + } + + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string $filename Optional filename for the download + */ + public function download($filename = null) { + f::download($this->root, $filename); + } + + /** + * Returns the exif object for this file (if image) + * + * @return Exif + */ + public function exif() { + if(isset($this->cache['exif'])) return $this->cache['exif']; + return $this->cache['exif'] = new Exif($this); + } + + /** + * Returns the PHP imagesize array + * + * @return array + */ + public function imagesize() { + return (array)getimagesize($this->root); + } + + /** + * Returns the dimensions of the file if possible + * + * @return Dimensions + */ + public function dimensions() { + + if(isset($this->cache['dimensions'])) return $this->cache['dimensions']; + + if(in_array($this->mime(), array('image/jpeg', 'image/png', 'image/gif'))) { + $size = (array)getimagesize($this->root); + $width = a::get($size, 0, 0); + $height = a::get($size, 1, 0); + } else if($this->extension() == 'svg') { + $content = $this->read(); + $xml = simplexml_load_string($content); + $attr = $xml->attributes(); + $width = floatval($attr->width); + $height = floatval($attr->height); + if($width == 0 or $height == 0 and !empty($attr->viewBox)) { + $box = str::split($attr->viewBox, ' '); + $width = floatval(a::get($box, 2, 0)); + $height = floatval(a::get($box, 3, 0)); + } + } else { + $width = 0; + $height = 0; + } + + return $this->cache['dimensions'] = new Dimensions($width, $height); + + } + + /** + * Returns the width of the asset + * + * @return int + */ + public function width() { + return $this->dimensions()->width(); + } + + /** + * Returns the height of the asset + * + * @return int + */ + public function height() { + return $this->dimensions()->height(); + } + + /** + * Returns the ratio of the asset + * + * @return int + */ + public function ratio() { + return $this->dimensions()->ratio(); + } + + /** + * Checks if the dimensions of the asset are portrait + * + * @return boolean + */ + public function isPortrait() { + return $this->dimensions()->portrait(); + } + + /** + * Checks if the dimensions of the asset are landscape + * + * @return boolean + */ + public function isLandscape() { + return $this->dimensions()->landscape(); + } + + /** + * Checks if the dimensions of the asset are square + * + * @return boolean + */ + public function isSquare() { + return $this->dimensions()->square(); + } + + /** + * Returns the orientation as string + * landscape | portrait | square + * + * @return string + */ + public function orientation() { + return $this->dimensions()->orientation(); + } + + /** + * @param array $attr + * @return string + */ + public function html($attr = array()) { + + if($this->type() != 'image') return false; + + $img = new Brick('img'); + $img->attr('src', $this->url()); + $img->attr('alt', ' '); + + if(is_string($attr) || (is_object($attr) && method_exists($attr, '__toString'))) { + $img->attr('alt', (string)$attr); + } else if(is_array($attr)) { + $img->attr($attr); + } + + return $img; + + } + + /** + * Scales the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Media + */ + public function resize($width, $height = null, $quality = null) { + + if($this->type() != 'image') return $this; + + $params = array('width' => $width); + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return new Thumb($this, $params); + + } + + /** + * Scales and crops the image if possible + * + * @param int $width + * @param mixed $height + * @param mixed $quality + * @return Media + */ + public function crop($width, $height = null, $quality = null) { + + if($this->type() != 'image') return $this; + + $params = array('width' => $width, 'crop' => true); + + if($height) $params['height'] = $height; + if($quality) $params['quality'] = $quality; + + return new Thumb($this, $params); + + } + + /** + * Converts the media object to a + * plain PHP array + * + * @param closure $callback + * @return array + */ + public function toArray($callback = null) { + + $data = array( + 'root' => $this->root(), + 'url' => $this->url(), + 'hash' => $this->hash(), + 'dir' => $this->dir(), + 'filename' => $this->filename(), + 'name' => $this->name(), + 'safeName' => $this->safeName(), + 'extension' => $this->extension(), + 'size' => $this->size(), + 'niceSize' => $this->niceSize(), + 'modified' => $this->modified(), + 'mime' => $this->mime(), + 'type' => $this->type(), + 'dimensions' => $this->dimensions()->toArray() + ); + + + if(is_null($callback)) { + return $data; + } else { + return array_map($callback, $data); + } + + } + + /** + * Converts the entire file array into + * a json string + * + * @param closure $callback Filter callback + * @return string + */ + public function toJson($callback = null) { + return json_encode($this->toArray($callback)); + } + + /** + * Returns a full link to this file + * Perfect for debugging in connection with echo + * + * @return string + */ + public function __toString() { + return $this->root; + } + +} diff --git a/kirby/toolkit/lib/obj.php b/kirby/toolkit/lib/obj.php new file mode 100644 index 0000000..2ed253c --- /dev/null +++ b/kirby/toolkit/lib/obj.php @@ -0,0 +1,38 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Obj extends stdClass { + + public function __construct($data = array()) { + foreach($data as $key => $val) { + $this->{$key} = $val; + } + } + + public function __call($method, $arguments) { + return isset($this->$method) ? $this->$method : null; + } + + public function set($key, $value) { + $this->$key = $value; + } + + public function get($key, $default = null) { + return isset($this->$key) ? $this->$key : $default; + } + + public function toArray() { + return (array)$this; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/pagination.php b/kirby/toolkit/lib/pagination.php new file mode 100644 index 0000000..be94b2f --- /dev/null +++ b/kirby/toolkit/lib/pagination.php @@ -0,0 +1,417 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Pagination { + + // configuration + public static $defaults = array( + 'variable' => 'page', + 'method' => 'param', + 'omitFirstPage' => true, + 'page' => false, + 'url' => null, + 'redirect' => false, + ); + + // options + protected $options = array(); + + // the current page + protected $page = null; + + // total count of items + protected $count = 0; + + // the number of displayed rows + protected $limit = 0; + + // the total number of pages + protected $pages = 0; + + // the offset for the slice function + protected $offset = 0; + + // the range start for ranged pagination + protected $rangeStart = 0; + + // the range end for ranged pagination + protected $rangeEnd = 0; + + /** + * Constructor + * + * @param mixed $count Either an integer or a Collection + * @param int $limit The number of items per page + * @param array $params Additional parameters to control the pagination object + */ + public function __construct($count, $limit, $params = array()) { + + // You can still pass an entire collection + if($count instanceof Collection) { + $count = $count->count(); + } + + $this->options = array_merge(static::$defaults, $params); + $this->count = (int)$count; + $this->limit = (int)$limit; + $this->pages = (int)ceil($this->count / $this->limit); + $this->offset = (int)(($this->page()-1) * $this->limit); + + } + + /** + * Returns the current page number + * + * @return int + */ + public function page() { + + if(!is_null($this->page)) return $this->page; + + if($this->options['page']) { + $this->page = $this->options['page']; + } else { + $this->page = ($this->options['method'] == 'query') ? get($this->options['variable']) : param($this->options['variable']); + } + + // make sure the page is an int + $this->page = intval($this->page); + + // set the first page correctly + if($this->page == 0) { + $this->page = 1; + } + + // sanitize the page if too low + if($this->page < 1) { + $this->redirect(); + $this->page = 1; + } + + // sanitize the page if too high + if($this->page > $this->pages && $this->count > 0) { + $this->redirect(); + $this->page = $this->lastPage(); + } + + // return the sanitized page number + return $this->page; + + } + + /** + * Redirects to an error page if the redirect option is set + * and the pagination is beyond the allowed boundaries + */ + public function redirect() { + if($redirect = $this->options['redirect']) { + go($redirect); + } + } + + /** + * Returns the total number of pages + * + * @return int + */ + public function countPages() { + return $this->pages; + } + + /** + * Alternative for countPages() + * + * @return int + */ + public function pages() { + return $this->pages; + } + + /** + * Returns the current offset + * This is used for the slice() method together with + * the limit to get the correct items from collections + * + * @return int + */ + public function offset() { + return $this->offset; + } + + /** + * Returns the chosen limit + * This is used for the slice() method together with + * the offset to get the correct items from collections + * + * @return int + */ + public function limit() { + return $this->limit; + } + + /** + * Checks if multiple pages are needed + * or if the collection can be displayed on a single page + * + * @return boolean + */ + public function hasPages() { + return $this->countPages() > 1; + } + + /** + * Returns the total number of items in the collection + * + * @return int + */ + public function countItems() { + return $this->count; + } + + /** + * Alternative for countItems() + * + * @return int + */ + public function items() { + return $this->count; + } + + /** + * Returns a page url for any given page number + * + * @param int $page The page number + * @return string The url + */ + public function pageURL($page) { + + if($this->options['method'] == 'query') { + + $query = url::query($this->options['url']); + + if($page == 1 && $this->options['omitFirstPage']) { + unset($query[$this->options['variable']]); + } else { + $query[$this->options['variable']] = $page; + } + + return url::build(array('query' => $query), $this->options['url']); + + } else { + + $params = url::params($this->options['url']); + + if($page == 1 && $this->options['omitFirstPage']) { + unset($params[$this->options['variable']]); + } else { + $params[$this->options['variable']] = $page; + } + + return url::build(array('params' => $params), $this->options['url']); + + } + + } + + /** + * Returns the number of the first page + * + * @return int + */ + public function firstPage() { + return 1; + } + + /** + * Checks if the current page is the first page + * + * @return boolean + */ + public function isFirstPage() { + return ($this->page == $this->firstPage()) ? true : false; + } + + /** + * Returns the url for the first page + * + * @return string + */ + public function firstPageURL() { + return $this->pageURL(1); + } + + /** + * Returns the number of the last page + * + * @return int + */ + public function lastPage() { + return $this->pages; + } + + /** + * Checks if the current page is the last page + * + * @return boolean + */ + public function isLastPage() { + return $this->page == $this->lastPage(); + } + + /** + * Returns the url for the last page + * + * @return string + */ + public function lastPageURL() { + return $this->pageURL($this->lastPage()); + } + + /** + * Returns the number of the previous page + * + * @return int + */ + public function prevPage() { + return $this->hasPrevPage() ? $this->page-1 : $this->page; + } + + /** + * Returns the url for the previous page + * + * @return string + */ + public function prevPageURL() { + return $this->pageURL($this->prevPage()); + } + + /** + * Checks if there's a previous page + * + * @return boolean + */ + public function hasPrevPage() { + return $this->page > 1; + } + + /** + * Returns the number of the next page + * + * @return int + */ + public function nextPage() { + return $this->hasNextPage() ? $this->page+1 : $this->page; + } + + /** + * Returns the url for the next page + * + * @return string + */ + public function nextPageURL() { + return $this->pageURL($this->nextPage()); + } + + /** + * Checks if there's a next page + * + * @return boolean + */ + public function hasNextPage() { + return $this->page < $this->pages; + } + + /** + * Returns the index number of the first item on the current page + * Can be used to display something like + * "Currently showing 1 - 10 of 123 items" + * + * @return int + */ + public function numStart() { + return $this->offset+1; + } + + /** + * Returns the index number of the last item on the current page + * Can be used to display something like + * "Currently showing 1 - 10 of 123 items" + * + * @return int + */ + public function numEnd() { + $end = $this->offset+$this->limit; + if($end > $this->items()) $end = $this->items(); + return $end; + } + + /** + * Creates a range of page numbers for Google-like pagination + * + * @return array + */ + public function range($range=5) { + + if($this->countPages() <= $range) { + $this->rangeStart = 1; + $this->rangeEnd = $this->countPages(); + return range($this->rangeStart, $this->rangeEnd); + } + + $this->rangeStart = $this->page - (int)floor($range/2); + $this->rangeEnd = $this->page + (int)floor($range/2); + + if($this->rangeStart <= 0) { + $this->rangeEnd += abs($this->rangeStart)+1; + $this->rangeStart = 1; + } + + if($this->rangeEnd > $this->countPages()) { + $this->rangeStart -= $this->rangeEnd-$this->countPages(); + $this->rangeEnd = $this->countPages(); + } + + return range($this->rangeStart,$this->rangeEnd); + + } + + /** + * Returns the first page of the created range + * + * @return int + */ + public function rangeStart() { + return $this->rangeStart; + } + + /** + * Returns the last page of the created range + * + * @return int + */ + public function rangeEnd() { + return $this->rangeEnd; + } + + /** + * Returns the most important pagination data into a readable array + * + * @return array + */ + public function toArray() { + return array( + 'page' => $this->page(), + 'pages' => $this->pages(), + 'items' => $this->countItems(), + ); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/password.php b/kirby/toolkit/lib/password.php new file mode 100644 index 0000000..2fd1bb0 --- /dev/null +++ b/kirby/toolkit/lib/password.php @@ -0,0 +1,48 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Password { + + /** + * Generates a salted hash for a plaintext password + * + * @param string $plaintext + * @return string + */ + public static function hash($plaintext) { + $salt = substr(str_replace('+', '.', base64_encode(sha1(str::random(), true))), 0, 22); + return crypt($plaintext, '$2a$10$' . $salt); + } + + /** + * Checks if a given string is already a hash + * + * @param string + * @return boolean + */ + public static function isHash($hash) { + return preg_match('!^\$2a\$10\$!', $hash); + } + + /** + * Checks if a password matches the encrypted hash + * + * @param string $plaintext + * @param string $hash + * @return boolean + */ + public static function match($plaintext, $hash) { + return crypt($plaintext, $hash) === $hash; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/r.php b/kirby/toolkit/lib/r.php new file mode 100644 index 0000000..f5e4c9b --- /dev/null +++ b/kirby/toolkit/lib/r.php @@ -0,0 +1,346 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class R { + + // Stores the raw request data + protected static $raw = null; + + // Stores all sanitized request data + protected static $data = null; + + // the request body + protected static $body = null; + + /** + * Returns the raw request data + * + * @return array + */ + public static function raw() { + if(!is_null(static::$raw)) return static::$raw; + return static::$raw = array_merge($_GET, $_POST); + } + + /** + * Returns either the entire data array or parts of it + * + * + * + * echo r::data('username'); + * // sample output 'bastian' + * + * echo r::data('username', 'peter'); + * // if no username is found in the request peter will be echoed + * + * + * + * @param string $key An optional key to receive only parts of the data array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @param mixed + */ + public static function data($key = null, $default = null) { + + if(is_null(static::$data)) { + static::$data = static::sanitize(static::raw()); + + if(!static::is('GET')) { + $body = static::body(); + parse_str($body, $parsed); + + if(!is_array($parsed)) { + $parsed = json_decode($body, false); + if(!is_array($parsed)) $parsed = array(); + } + + static::$data = array_merge($parsed, static::$data); + + } + + } + + if(is_null($key)) { + return static::$data; + } else if(isset(static::$data[$key])) { + return static::$data[$key]; + } else { + return $default; + } + + } + + /** + * Only returns get data + * + * @return array + */ + public static function getData($key = null, $default = null) { + return a::get((array)static::sanitize($_GET), $key, $default); + } + + /** + * Only returns post data + * + * @return array + */ + public static function postData($key = null, $default = null) { + return a::get((array)static::sanitize($_POST), $key, $default); + } + + /** + * Private method to sanitize incoming request data + * + * @param array $data + * @return array + */ + protected static function sanitize($data) { + + if(!is_array($data)) { + return trim(str::stripslashes($data)); + } + + foreach($data as $key => $value) { + $data[$key] = static::sanitize($value); + } + + return $data; + + } + + /** + * Sets or overwrites a variable in the data array + * + * + * + * r::set('username', 'bastian'); + * + * dump($request); + * + * // sample output: array( + * // 'username' => 'bastian' + * // ... other stuff from the request + * // ); + * + * + * + * @param mixed $key The key to set/replace. Use an array to set multiple values at once + * @param mixed $value The value + * @return array + */ + public static function set($key, $value = null) { + + // set multiple values at once + if(is_array($key)) { + foreach($key as $k => $v) static::set($k, $v); + // return this for chaining + return; + } + + // make sure the data array is actually an array + if(is_null(static::$data)) static::$data = array(); + + // sanitize the + static::$data[$key] = static::sanitize($value); + + // return the new data array + return static::$data; + + } + + /** + * Alternative for r::data($key, $default) + * + * + * + * echo r::get('username'); + * // sample output 'bastian' + * + * echo r::get('username', 'peter'); + * // if no username is found in the request peter will be echoed + * + * + * + * @param string $key An optional key to receive only parts of the data array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @param mixed + */ + public static function get($key = null, $default = null) { + return static::data($key, $default); + } + + /** + * Removes a variable from the request array + * + * @param string $key + */ + public static function remove($key) { + unset(static::$data[$key]); + } + + /** + * Returns the current request method + * + * @return string POST, GET, DELETE, PUT, HEAD, PATCH, etc. + */ + public static function method() { + return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + } + + /** + * Returns the request body from POST requests for example + * + * @return mixed + */ + public static function body() { + if(!is_null(static::$body)) return static::$body; + return static::$body = file_get_contents('php://input'); + } + + /** + * Returns the files array + * + * @param string $key An optional key to receive only parts of the files array + * @param mixed $default A default value, which will be returned if nothing can be found for a given key + * @return array + */ + public static function files($key = null, $default = null) { + return a::get($_FILES, $key, $default); + } + + /** + * Checks if the request is of a specific type: + * + * - GET + * - POST + * - PUT + * - PATCH + * - DELETE + * - AJAX + * + * @return boolean + */ + public static function is($method) { + if($method == 'ajax') { + return static::ajax(); + } else { + return strtoupper($method) == static::method() ? true : false; + } + } + + /** + * Checks for a specific key in the data array + * + * @return boolean + */ + public static function has($key) { + $data = static::data(); + return isset($data[$key]); + } + + /** + * Returns the referer if available + * + * + * + * echo r::referer(); + * // sample result: http://someurl.com + * + * + * + * @param string $default Pass an optional URL to use as default referer if no referer is being found + * @return string + */ + public static function referer($default = null) { + return a::get($_SERVER, 'HTTP_REFERER', $default); + } + + /** + * Nobody remembers how to spell it + * so this is a shortcut + * + * + * + * echo $request->referrer(); + * // sample result: http://someurl.com + * + * + * + * @param string $default Pass an optional URL to use as default referer if no referer is being found + * @return string + */ + public static function referrer($default = null) { + return static::referer($default); + } + + /** + * Returns the IP address from the + * request user if available + * + * @param mixed + */ + public static function ip() { + return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : false; + } + + /** + * Checks if the request has been made from the command line + * + * @return boolean + */ + public static function cli() { + return defined('STDIN') || (substr(PHP_SAPI, 0, 3) == 'cgi' && $term = getenv('TERM') && $term !== 'unknown'); + } + + /** + * Checks if the request is an AJAX request + * + * + * + * if($request->ajax()) echo 'ajax rulez'; + * + * + * + * @return boolean + */ + public static function ajax() { + return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); + } + + /** + * Returns the request scheme + * + * @return string + */ + public static function scheme() { + return url::scheme(); + } + + /** + * Checks if the request is encrypted + * + * @return boolean + */ + public static function ssl() { + return static::scheme() == 'https'; + } + + /** + * Alternative for r::ssl() + * + * @return boolean + */ + public static function secure() { + return static::ssl(); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/redirect.php b/kirby/toolkit/lib/redirect.php new file mode 100644 index 0000000..6a43ebd --- /dev/null +++ b/kirby/toolkit/lib/redirect.php @@ -0,0 +1,56 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Redirect { + + /** + * Redirects the user to a new URL + * + * @param string $url The URL to redirect to + * @param boolean $code The HTTP status code, which should be sent (301, 302 or 303) + * @param boolean $send If true, headers will be sent and redirection will take effect + */ + public static function send($url = false, $code = false, $send = true) { + return header::redirect($url, $code, $send); + } + + /** + * Redirects to a specific URL. You can pass either a normal URI + * a controller path or simply nothing (which redirects home) + */ + public static function to() { + static::send(call_user_func_array(array('url', 'to'), func_get_args())); + } + + /** + * Redirects to the home page of the app + */ + public static function home() { + static::send(url::home()); + } + + /** + * Redirects to the last location of the user + * + * @param string $fallback + */ + public static function back($fallback = null) { + // get the last url + $last = url::last(); + // make sure there's a proper fallback + if(empty($last)) $last = $fallback ? $fallback : url::home(); + static::send($last); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/remote.php b/kirby/toolkit/lib/remote.php new file mode 100644 index 0000000..aeae2cd --- /dev/null +++ b/kirby/toolkit/lib/remote.php @@ -0,0 +1,327 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Remote { + + // configuration + public static $defaults = array( + 'method' => 'GET', + 'data' => array(), + 'file' => null, + 'timeout' => 10, + 'headers' => array(), + 'encoding' => 'utf-8', + 'agent' => null, + 'body' => true, + ); + + // store for the response object + protected $response = null; + + // all options for the request + protected $options = array(); + + // all received headers + protected $headers = array(); + + /** + * Constructor + * + * @param string $url + * @param array $options + */ + public function __construct($url, $options = array()) { + + // set all options + $this->options = array_merge(static::$defaults, $options); + + // add the url + $this->options['url'] = $url; + + // send the request + $this->send(); + + } + + /** + * Sets up all curl options and sends the request + * + * @return object Response + */ + protected function send() { + + // start a curl request + $curl = curl_init(); + + // curl options + $params = array( + CURLOPT_URL => $this->options['url'], + CURLOPT_ENCODING => $this->options['encoding'], + CURLOPT_CONNECTTIMEOUT => $this->options['timeout'], + CURLOPT_TIMEOUT => $this->options['timeout'], + CURLOPT_AUTOREFERER => true, + CURLOPT_RETURNTRANSFER => $this->options['body'], + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 10, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_HEADER => false, + CURLOPT_HEADERFUNCTION => array($this, 'header'), + ); + + // add all headers + if(!empty($this->options['headers'])) $params[CURLOPT_HTTPHEADER] = $this->options['headers']; + + // add the user agent + if(!empty($this->options['agent'])) $params[CURLOPT_USERAGENT] = $this->options['agent']; + + // do some request specific stuff + switch(strtolower($this->options['method'])) { + case 'post': + $params[CURLOPT_POST] = true; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'put': + + $params[CURLOPT_CUSTOMREQUEST] = 'PUT'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + + // put a file + if($this->options['file']) { + $params[CURLOPT_INFILE] = fopen($this->options['file'], 'r'); + $params[CURLOPT_INFILESIZE] = f::size($this->options['file']); + } + + break; + case 'delete': + $params[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'head': + $params[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + $params[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + $params[CURLOPT_NOBODY] = true; + break; + } + + curl_setopt_array($curl, $params); + + $content = curl_exec($curl); + $error = curl_errno($curl); + $message = curl_error($curl); + $info = curl_getinfo($curl); + + curl_close($curl); + + $this->response = new RemoteResponse(); + $this->response->headers = $this->headers; + $this->response->error = $error; + $this->response->message = $message; + $this->response->content = $content; + $this->response->code = $info['http_code']; + $this->response->info = $info; + + return $this->response; + + } + + /** + * Used by curl to parse incoming headers + * + * @param object $curl the curl connection + * @param string $header the header line + * @return int the length of the heade + */ + protected function header($curl, $header) { + + $parts = str::split($header, ':'); + + if(!empty($parts[0]) && !empty($parts[1])) { + $this->headers[$parts[0]] = $parts[1]; + } + + return strlen($header); + + } + + /** + * Returns all options which have been + * set for the current request + * + * @return array + */ + public function options() { + return $this->options; + } + + /** + * Returns the response object for + * the current request + * + * @return object Response + */ + public function response() { + return $this->response; + } + + /** + * Static method to init this class and send a request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function request($url, $params = array()) { + $request = new self($url, $params); + return $request->response(); + } + + /** + * Static method to send a GET request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function get($url, $params = array()) { + + $defaults = array( + 'method' => 'GET', + 'data' => array(), + ); + + $options = array_merge($defaults, $params); + $query = http_build_query($options['data']); + + if(!empty($query)) { + $url = (url::hasQuery($url)) ? $url . '&' . $query : $url . '?' . $query; + } + + // remove the data array from the options + unset($options['data']); + + $request = new self($url, $options); + return $request->response(); + + } + + /** + * Static method to send a POST request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function post($url, $params = array()) { + + $defaults = array( + 'method' => 'POST' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a PUT request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function put($url, $params = array()) { + + $defaults = array( + 'method' => 'PUT' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a DELETE request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function delete($url, $params = array()) { + + $defaults = array( + 'method' => 'DELETE' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a HEAD request + * + * @param string $url + * @param array $params + * @return object Response + */ + public static function head($url, $params = array()) { + + $defaults = array( + 'method' => 'HEAD' + ); + + $request = new self($url, array_merge($defaults, $params)); + return $request->response(); + + } + + /** + * Static method to send a HEAD request + * which only returns an array of headers + * + * @param string $url + * @param array $params + * @return array + */ + public static function headers($url, $params = array()) { + $request = static::head($url, $params); + return array_merge($request->headers(), $request->info()); + } + + /** + * Internal method to handle post field data + * + * @param mixed $data + * @return mixed + */ + protected function postfields($data) { + + if(is_object($data) || is_array($data)) { + return http_build_query($data); + } else { + return $data; + } + + } + +} + +class RemoteResponse extends Obj { + + public function __toString() { + return (string)$this->content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/response.php b/kirby/toolkit/lib/response.php new file mode 100644 index 0000000..8ffca3d --- /dev/null +++ b/kirby/toolkit/lib/response.php @@ -0,0 +1,142 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Response { + + // the response content + protected $content; + + // the format type + protected $format; + + // the HTTP code + protected $code; + + /** + * Constructor + * + * @param string $content + * @param string $format + * @param int $code Optional HTTP code + */ + public function __construct($content, $format = 'html', $code = 200) { + + $this->content = $content; + $this->format = strtolower($format); + $this->code = $code; + + // convert arrays to json + if(is_array($this->content) && $this->format == 'json') { + + if(defined('JSON_PRETTY_PRINT') && get('pretty')) { + $this->content = json_encode($this->content, JSON_PRETTY_PRINT); + } else { + $this->content = json_encode($this->content); + } + + } + + } + + /** + * Sends the correct header for the response + * + * @param boolean $send If set to false, the header will be returned + * @return mixed + */ + public function header($send = true) { + + $status = header::status($this->code, false); + $type = header::type($this->format, 'utf-8', false); + + if(!$send) return $status . PHP_EOL . $type; + + header($status); + header($type); + + } + + /** + * Returns the content of this response + * + * @return string + */ + public function content() { + return $this->content; + } + + /** + * Returns the content format + * + * @return string + */ + public function format() { + return $this->format; + } + + /** + * Returns a success response + * + * @param string $message + * @param mixed $data + * @param mixed $code + * @return object + */ + static public function success($message = 'Everything went fine', $data = array(), $code = 200) { + return new static(array( + 'status' => 'success', + 'code' => $code, + 'message' => $message, + 'data' => $data + ), 'json', $code); + } + + /** + * Returns an error response + * + * @param mixed $message Either a message string or an error or errors object + * @param mixed $code + * @param mixed $data + * @return object + */ + static public function error($message = 'Something went wrong', $code = 400, $data = array()) { + return new static(array( + 'status' => 'error', + 'code' => $code, + 'message' => $message, + 'data' => $data + ), 'json', $code); + } + + /** + * Converts an array to json and returns it properly + * + * @param array $data + * @return object + */ + static public function json($data, $code = 200) { + return new static($data, 'json', $code); + } + + /** + * Echos the content + * and sends the appropriate header + * + * @return string + */ + public function __toString() { + $this->header(); + return (string)$this->content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/router.php b/kirby/toolkit/lib/router.php new file mode 100644 index 0000000..a286a2e --- /dev/null +++ b/kirby/toolkit/lib/router.php @@ -0,0 +1,284 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Router { + + // request instance + protected $request = null; + + // the matched route if found + protected $route = null; + + // all registered routes + protected $routes = array( + 'GET' => array(), + 'POST' => array(), + 'HEAD' => array(), + 'PUT' => array(), + 'PATCH' => array(), + 'DELETE' => array() + ); + + // The wildcard patterns supported by the router. + protected $patterns = array( + '(:num)' => '([0-9]+)', + '(:alpha)' => '([a-zA-Z]+)', + '(:any)' => '([a-zA-Z0-9\.\-_%=]+)', + '(:all)' => '(.*)', + ); + + // The optional wildcard patterns supported by the router. + protected $optional = array( + '/(:num?)' => '(?:/([0-9]+)', + '/(:alpha?)' => '(?:/([a-zA-Z]+)', + '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%=]+)', + '/(:all?)' => '(?:/(.*)', + ); + + // additional events, which can be triggered by routes + protected $filters = array(); + + /** + * Constructor + * + * @param array $routes + */ + public function __construct($routes = array()) { + $this->register($routes); + } + + /** + * Returns the found route + * + * @return mixed + */ + public function route() { + return $this->route; + } + + /** + * Returns the arguments array from the current route + * + * @return array + */ + public function arguments() { + if($route = $this->route()) return $route->arguments(); + } + + /** + * Adds a new route + * + * @param mixed $pattern + * @param mixed $params + * @param mixed $optional + * @return Obj + */ + public function register($pattern, $params = array(), $optional = array()) { + + if($pattern === false) { + return false; + } else if(is_array($pattern)) { + foreach($pattern as $v) { + if($v === false || empty($v['pattern'])) { + continue; + } else if(is_array($v['pattern'])) { + foreach($v['pattern'] as $p) { + $v['pattern'] = $p; + $this->register($p, $v); + } + } else { + $this->register($v['pattern'], $v); + } + } + return $this; + } + + $defaults = array( + 'pattern' => $pattern, + 'https' => false, + 'ajax' => false, + 'filter' => null, + 'method' => 'GET', + 'arguments' => array(), + ); + + $route = new Obj(array_merge($defaults, $params, $optional)); + + // convert single methods or methods separated by | to arrays + if(is_string($route->method)) { + + if(strpos($route->method, '|') !== false) { + $route->method = str::split($route->method, '|'); + } else if($route->method == 'ALL') { + $route->method = array_keys($this->routes); + } else { + $route->method = array($route->method); + } + + } + + if(is_string($route->filter)) { + if(strpos($route->filter, '|') !== false) { + $route->filter = str::split($route->filter, '|'); + } else { + $route->filter = array($route->filter); + } + } + + foreach($route->method as $method) { + $this->routes[strtoupper($method)][$route->pattern] = $route; + } + + return $route; + + } + + /** + * Add a new router filter + * + * @param string $name A simple name for the filter, which can be used by routes later + * @param closure $function A filter function, which should be called before routes + */ + public function filter($name, $function) { + $this->filters[$name] = $function; + } + + /** + * Return all registered filters + * + * @return array + */ + public function filters() { + return $this->filters; + } + + /** + * Call all matching filters + * + * @param mixed $filters + */ + protected function filterer($filters) { + foreach((array)$filters as $filter) { + if(array_key_exists($filter, $this->filters) && is_callable($this->filters[$filter])) { + call_user_func($this->filters[$filter]); + } + } + } + + /** + * Returns all added routes + * + * @param string $method + * @return array + */ + public function routes($method = null) { + return is_null($method) ? $this->routes : $this->routes[strtoupper($method)]; + } + + /** + * Iterate through every route to find a matching route. + * + * @param string $path Optional path to match against + * @return Route + */ + public function run($path = null) { + + $method = r::method(); + $ajax = r::ajax(); + $https = r::ssl(); + $routes = a::get($this->routes, $method, array()); + + // detect path if not set manually + if($path === null) $path = implode('/', (array)url::fragments(detect::path())); + + // empty urls should never happen + if(empty($path)) $path = '/'; + + foreach($routes as $route) { + + if($route->https && !$https) continue; + if($route->ajax && !$ajax) continue; + + // handle exact matches + if($route->pattern == $path) { + $this->route = $route; + break; + } + + // We only need to check routes with regular expression since all others + // would have been able to be matched by the search for literal matches + // we just did before we started searching. + if(strpos($route->pattern, '(') === false) continue; + + $preg = '#^'. $this->wildcards($route->pattern) . '$#u'; + + // If we get a match we'll return the route and slice off the first + // parameter match, as preg_match sets the first array item to the + // full-text match of the pattern. + if(preg_match($preg, $path, $parameters)) { + $this->route = $route; + $this->route->arguments = array_slice($parameters, 1); + break; + } + + } + + if($this->route && $this->filterer($this->route->filter) !== false) { + return $this->route; + } else { + return null; + } + + } + + /** + * Translate route URI wildcards into regular expressions. + * + * @param string $pattern + * @return string + */ + protected function wildcards($pattern) { + + $search = array_keys($this->optional); + $replace = array_values($this->optional); + + // For optional parameters, first translate the wildcards to their + // regex equivalent, sans the ")?" ending. We'll add the endings + // back on when we know the replacement count. + $pattern = str_replace($search, $replace, $pattern, $count); + + if($count > 0) $pattern .= str_repeat(')?', $count); + + return strtr($pattern, $this->patterns); + + } + + /** + * Find a registered route by a field and value + * + * @param string $field + * @param string $value + * @return object + */ + public function findRouteBy($field, $value) { + foreach($this->routes as $method => $routes) { + foreach($routes as $route) { + if($route->$field() == $value) return $route; + } + } + } + +} diff --git a/kirby/toolkit/lib/s.php b/kirby/toolkit/lib/s.php new file mode 100644 index 0000000..cac1749 --- /dev/null +++ b/kirby/toolkit/lib/s.php @@ -0,0 +1,291 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class S { + + public static $started = false; + public static $name = 'kirby_session'; + public static $timeout = 30; + public static $cookie = array(); + + /** + * Starts a new session + * + * + * + * s::start(); + * // do whatever you want with the session now + * + * + * + */ + public static function start() { + + if(session_status() === PHP_SESSION_ACTIVE) return true; + + // store the session name + static::$cookie += array( + 'lifetime' => 0, + 'path' => ini_get('session.cookie_path'), + 'domain' => ini_get('session.cookie_domain'), + 'secure' => r::secure(), + 'httponly' => true + ); + + // set the custom session name + session_name(static::$name); + + // make sure to use cookies only + ini_set('session.use_cookies', 1); + ini_set('session.use_only_cookies', 1); + + // try to start the session + if(!session_start()) return false; + + if(!setcookie( + static::$name, + session_id(), + cookie::lifetime(static::$cookie['lifetime']), + static::$cookie['path'], + static::$cookie['domain'], + static::$cookie['secure'], + static::$cookie['httponly'] + )) { + return false; + } + + // mark it as started + static::$started = true; + + // check if the session is still valid + if(!static::check()) { + return static::destroy(); + } + + return true; + + } + + /** + * Checks if the session is still valid + * and not expired + * + * @return boolean + */ + public static function check() { + + // check for the last activity and compare it with the session timeout + if(isset($_SESSION['kirby_session_activity']) && time() - $_SESSION['kirby_session_activity'] > static::$timeout * 60) { + return false; + } + + // check for an existing fingerprint and compare it + if(isset($_SESSION['kirby_session_fingerprint']) and $_SESSION['kirby_session_fingerprint'] !== static::fingerprint()) { + return false; + } + + // store a new fingerprint and the last activity + $_SESSION['kirby_session_fingerprint'] = static::fingerprint(); + $_SESSION['kirby_session_activity'] = time(); + + return true; + + } + + /** + * Generates a fingerprint from the user agent string + * + * @return string + */ + public static function fingerprint() { + if(!r::cli()) { + return sha1(Visitor::ua() . (ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))); + } else { + return ''; + } + } + + /** + * Returns the current session id + * + * @return string + */ + public static function id() { + static::start(); + return session_id(); + } + + /** + * Sets a session value by key + * + * + * + * s::set('username', 'bastian'); + * // saves the username in the session + * + * s::set(array( + * 'key1' => 'val1', + * 'key2' => 'val2', + * 'key3' => 'val3' + * )); + * // setting multiple variables at once + * + * + * + * @param mixed $key The key to define + * @param mixed $value The value for the passed key + */ + public static function set($key, $value = false) { + + static::start(); + + if(!isset($_SESSION)) return false; + if(is_array($key)) { + $_SESSION = array_merge($_SESSION, $key); + } else { + $_SESSION[$key] = $value; + } + + } + + /** + * Gets a session value by key + * + * + * + * s::get('username', 'bastian'); + * // saves the username in the session + * + * echo s::get('username'); + * // output: 'bastian' + * + * + * + * @param mixed $key The key to look for. Pass false or null to return the entire session array. + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + public static function get($key = false, $default = null) { + + static::start(static::$name, static::$timeout, static::$cookie); + + if(!isset($_SESSION)) return false; + if(empty($key)) return $_SESSION; + return isset($_SESSION[$key]) ? $_SESSION[$key] : $default; + + } + + /** + * Retrieves an item and removes it afterwards + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function pull($key, $default = null) { + $value = s::get($key, $default); + s::remove($key); + return $value; + } + + /** + * Removes a value from the session by key + * + * + * + * $_SESSION = array( + * 'username' => 'bastian', + * 'id' => 1, + * ); + * + * s::remove('username'); + * // $_SESSION = array( + * // 'id' => 1 + * // ) + * + * + * + * @param mixed $key The key to remove by + * @return array The session array without the value + */ + public static function remove($key) { + + static::start(); + + unset($_SESSION[$key]); + return $_SESSION; + + } + + /** + * Checks if the session has already been started + * + * @return boolean + */ + public static function started() { + return static::$started; + } + + /** + * Destroys a session + * + * + * + * s::start(); + * // do whatever you want with the session now + * + * s::destroy(); + * // everything stored in the session will be deleted + * + * + * + */ + public static function destroy() { + + if(!static::$started) return false; + + $_SESSION = array(); + + cookie::remove(static::$name); + + static::$started = false; + + return session_destroy(); + + } + + /** + * Alternative for s::destroy() + */ + public static function stop() { + s::destroy(); + } + + /** + * Destroys a session first and then starts it again + */ + public static function restart() { + static::destroy(); + static::start(); + } + + /** + * Create a new session Id + */ + public static function regenerateId() { + static::start(static::$name, static::$timeout, static::$cookie); + session_regenerate_id(true); + } + +} diff --git a/kirby/toolkit/lib/server.php b/kirby/toolkit/lib/server.php new file mode 100644 index 0000000..dca04fe --- /dev/null +++ b/kirby/toolkit/lib/server.php @@ -0,0 +1,62 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Server { + + /** + * Gets a value from the _SERVER array + * + * + * + * server::get('document_root'); + * // sample output: /var/www/kirby + * + * server::get(); + * // returns the whole server array + * + * + * + * @param mixed $key The key to look for. Pass false or null to return the entire server array. + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + public static function get($key = false, $default = null) { + if(empty($key)) return $_SERVER; + $key = str::upper($key); + $value = a::get($_SERVER, $key, $default); + return static::sanitize($key, $value); + } + + public static function sanitize($key, $value) { + + switch($key) { + case 'SERVER_ADDR': + case 'SERVER_NAME': + case 'HTTP_HOST': + $value = strip_tags($value); + $value = preg_replace('![^\w.:-]+!iu', '', $value); + $value = trim($value, '-'); + $value = htmlspecialchars($value); + break; + case 'SERVER_PORT': + $value = preg_replace('![^0-9]+!', '', $value); + break; + } + + return $value; + + } + +} diff --git a/kirby/toolkit/lib/silo.php b/kirby/toolkit/lib/silo.php new file mode 100644 index 0000000..ec8da16 --- /dev/null +++ b/kirby/toolkit/lib/silo.php @@ -0,0 +1,42 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Silo { + + public static $data = array(); + + public static function set($key, $value = null) { + if(is_array($key)) { + return static::$data = array_merge(static::$data, $key); + } else { + return static::$data[$key] = $value; + } + } + + public static function get($key = null, $default = null) { + if(empty($key)) return static::$data; + return isset(static::$data[$key]) ? static::$data[$key] : $default; + } + + public static function remove($key = null) { + // reset the entire array + if(is_null($key)) return static::$data = array(); + // unset a single key + unset(static::$data[$key]); + // return the array without the removed key + return static::$data; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/sql.php b/kirby/toolkit/lib/sql.php new file mode 100644 index 0000000..4c1af07 --- /dev/null +++ b/kirby/toolkit/lib/sql.php @@ -0,0 +1,806 @@ +, Lukas Bestle + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Sql { + + // list of literals which should not be escaped in queries + public static $literals = array('NOW()', null); + + // sql formatting methods, defined below + public static $methods = array(); + + // the parent database connection and database query + public $database; + public $dbquery; + + // list of bindings by sql query string that defines them + protected $bindings = array(); + + /** + * Constructor + * + * @param Database $database + * @param Database\Query $dbquery Database query that is used to set the bindings directly + */ + public function __construct($database, $dbquery = null) { + $this->database = $database; + $this->dbquery = $dbquery; + } + + /** + * Sets and returns query-specific bindings + * + * @param string $query SQL query string that contains the bindings + * @param array $values Array of bindings to set (null to get the bindings) + * @return array + */ + public function bindings($query, $values = null) { + if(is_null($values)) { + return a::get($this->bindings, $query, array()); + } else { + if(!is_null($query)) $this->bindings[$query] = $values; + + // directly register bindings if possible + if($this->dbquery) $this->dbquery->bindings($values); + } + } + + /** + * Calls an SQL method using the correct database type + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call($method, $arguments) { + $type = $this->database->type(); + + if(isset(static::$methods[$type][$method])) { + $method = static::$methods[$type][$method]; + } else { + // fallback to shared method + if(!isset(static::$methods['_shared'][$method])) { + throw new Error('SQL method ' . $method . ' is not defined for database type ' . $type); + } + + $method = static::$methods['_shared'][$method]; + } + + // pass the sql object as first argument + array_unshift($arguments, $this); + return call($method, $arguments); + } + + /** + * Registers a method for a specified database type + * The function must take this SQL object as first parameter and set bindings on it + * + * @param string $name + * @param callable $function + * @param string $type 'mysql', 'sqlite' or '_shared' + */ + public static function registerMethod($name, $function, $type = '_shared') { + if(!isset(static::$methods[$type])) static::$methods[$type] = array(); + static::$methods[$type][$name] = $function; + } + + /** + * Returns a randomly generated binding name + * + * @param string $label String that contains lowercase letters and numbers to use as a readable identifier + * @return string + */ + public static function generateBindingName($label) { + // make sure that the binding name is valid to prevent injections + if(!preg_match('/^[a-z0-9]+$/', $label)) $label = 'invalid'; + + return ':' . $label . '_' . uniqid(); + } + +} + +/** + * Builds a select clause + * + * @param array $params List of parameters for the select clause. Check out the defaults for more info. + * @return string + */ +sql::registerMethod('select', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'columns' => '*', + 'join' => false, + 'distinct' => false, + 'where' => false, + 'group' => false, + 'having' => false, + 'order' => false, + 'offset' => 0, + 'limit' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + $query[] = 'SELECT'; + + // select distinct values + if($options['distinct']) $query[] = 'DISTINCT'; + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + // columns + if(empty($options['columns'])) { + $query[] = '*'; + } else if(is_array($options['columns'])) { + // validate columns + $columns = array(); + foreach($options['columns'] as $column) { + list($table, $columnPart) = $sql->splitIdentifier($options['table'], $column); + if(!$sql->database->validateColumn($table, $columnPart)) { + throw new Error('Invalid column ' . $column); + } + + $columns[] = $sql->combineIdentifier($table, $columnPart); + } + + $query[] = implode(', ', $columns); + } else { + $query[] = $options['columns']; + } + + // table + $query[] = 'FROM ' . $sql->quoteIdentifier($options['table']); + + // join + if(!empty($options['join'])) { + foreach($options['join'] as $join) { + $joinType = ltrim(strtoupper(a::get($join, 'type', '')) . ' JOIN'); + if(!in_array($joinType, array( + 'JOIN', 'INNER JOIN', + 'OUTER JOIN', + 'LEFT OUTER JOIN', 'LEFT JOIN', + 'RIGHT OUTER JOIN', 'RIGHT JOIN', + 'FULL OUTER JOIN', 'FULL JOIN', + 'NATURAL JOIN', + 'CROSS JOIN', + 'SELF JOIN' + ))) throw new Error('Invalid join type ' . $joinType); + + // validate table + if(!$sql->database->validateTable($join['table'])) throw new Error('Invalid table ' . $join['table']); + + // ON can't be escaped here + $query[] = $joinType . ' ' . $sql->quoteIdentifier($join['table']) . ' ON ' . $join['on']; + } + } + + // where + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + // group + if(!empty($options['group'])) { + // GROUP BY can't be escaped here + $query[] = 'GROUP BY ' . $options['group']; + } + + // having + if(!empty($options['having'])) { + // HAVING can't be escaped here + $query[] = 'HAVING ' . $options['having']; + } + + // order + if(!empty($options['order'])) { + // ORDER BY can't be escaped here + $query[] = 'ORDER BY ' . $options['order']; + } + + // offset and limit + if($options['offset'] > 0 || $options['limit']) { + if(!$options['limit']) $options['limit'] = '18446744073709551615'; + + $offsetBinding = sql::generateBindingName('offset'); + $bindings[$offsetBinding] = $options['offset']; + $limitBinding = sql::generateBindingName('limit'); + $bindings[$limitBinding] = $options['limit']; + + $query[] = 'LIMIT ' . $offsetBinding . ', ' . $limitBinding; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds an insert clause + * + * @param array $params List of parameters for the insert clause. See defaults for more info. + * @return string + */ +sql::registerMethod('insert', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'values' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'INSERT INTO ' . $sql->quoteIdentifier($options['table']); + $query[] = $sql->values($options['table'], $options['values'], ', ', false); + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds an update clause + * + * @param array $params List of parameters for the update clause. See defaults for more info. + * @return string + */ +sql::registerMethod('update', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'values' => false, + 'where' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'UPDATE ' . $sql->quoteIdentifier($options['table']) . ' SET'; + $query[] = $sql->values($options['table'], $options['values']); + + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds a delete clause + * + * @param array $params List of parameters for the delete clause. See defaults for more info. + * @return string + */ +sql::registerMethod('delete', function($sql, $params = array()) { + + $defaults = array( + 'table' => '', + 'where' => false, + ); + + $options = array_merge($defaults, $params); + $query = array(); + $bindings = array(); + + // validate table + if(!$sql->database->validateTable($options['table'])) throw new Error('Invalid table ' . $options['table']); + + $query[] = 'DELETE FROM ' . $sql->quoteIdentifier($options['table']); + + if(!empty($options['where'])) { + // WHERE can't be escaped here + $query[] = 'WHERE ' . $options['where']; + } + + $query = implode(' ', $query); + + $sql->bindings($query, $bindings); + return $query; + +}); + +/** + * Builds a safe list of values for insert, select or update queries + * + * @param string $table Table name + * @param mixed $values A value string or array of values + * @param string $separator A separator which should be used to join values + * @param boolean $set If true builds a set list of values for update clauses + * @param boolean $enforceQualified Always use fully qualified column names + * @return string + */ +sql::registerMethod('values', function($sql, $table, $values, $separator = ', ', $set = true, $enforceQualified = false) { + + if(!is_array($values)) return $values; + + if($set) { + + $output = array(); + $bindings = array(); + + foreach($values as $key => $value) { + // validate column + list($table, $column) = $sql->splitIdentifier($table, $key); + if(!$sql->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $key); + } + $key = $sql->combineIdentifier($table, $column, $enforceQualified !== true); + + if(in_array($value, sql::$literals, true)) { + $output[] = $key . ' = ' . (($value === null)? 'null' : $value); + continue; + } elseif(is_array($value)) { + $value = json_encode($value); + } + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $value; + + $output[] = $key . ' = ' . $valueBinding; + } + + $sql->bindings(null, $bindings); + return implode($separator, $output); + + } else { + + $fields = array(); + $output = array(); + $bindings = array(); + + foreach($values as $key => $value) { + // validate column + list($table, $column) = $sql->splitIdentifier($table, $key); + if(!$sql->database->validateColumn($table, $column)) { + throw new Error('Invalid column ' . $key); + } + $key = $sql->combineIdentifier($table, $column, $enforceQualified !== true); + + $fields[] = $key; + + if(in_array($value, sql::$literals, true)) { + $output[] = ($value === null)? 'null' : $value; + continue; + } elseif(is_array($value)) { + $value = json_encode($value); + } + + $valueBinding = sql::generateBindingName('value'); + $bindings[$valueBinding] = $value; + + $output[] = $valueBinding; + } + + $sql->bindings(null, $bindings); + return '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $output) . ')'; + + } + +}); + +/** + * Creates the sql for dropping a single table + * + * @param string $table + * @return string + */ +sql::registerMethod('dropTable', function($sql, $table) { + + // validate table + if(!$sql->database->validateTable($table)) throw new Error('Invalid table ' . $table); + + return 'DROP TABLE ' . $sql->quoteIdentifier($table); + +}); + +/** + * Creates a table with a simple scheme array for columns + * MySQL version + * + * @todo add more options per column + * @param string $table The table name + * @param array $columns + * @return string + */ +sql::registerMethod('createTable', function($sql, $table, $columns = array()) { + + $output = array(); + $keys = array(); + $bindings = array(); + + foreach($columns as $name => $column) { + // column type + if(!isset($column['type'])) throw new Error('No column type given for column ' . $name); + switch($column['type']) { + case 'id': + $template = '{column.name} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT'; + $column['key'] = 'PRIMARY'; + break; + case 'varchar': + $template = '{column.name} varchar(255) {column.null} {column.default}'; + break; + case 'text': + $template = '{column.name} TEXT'; + break; + case 'int': + $template = '{column.name} INT(11) UNSIGNED {column.null} {column.default}'; + break; + case 'timestamp': + $template = '{column.name} TIMESTAMP {column.null} {column.default}'; + break; + default: + throw new Error('Unsupported column type: ' . $column['type']); + } + + // null + if(a::get($column, 'null') === false) { + $null = 'NOT NULL'; + } else { + $null = 'NULL'; + } + + // indexes/keys + $key = false; + if(isset($column['key'])) { + $column['key'] = strtoupper($column['key']); + + // backwards compatibility + if($column['key'] === 'PRIMARY') $column['key'] = 'PRIMARY KEY'; + + if(in_array($column['key'], array('PRIMARY KEY', 'INDEX'))) { + $key = $column['key']; + $keys[$name] = $key; + } + } + + // default value + $defaultBinding = null; + if(isset($column['default'])) { + $defaultBinding = sql::generateBindingName('default'); + $bindings[$defaultBinding] = $column['default']; + } + + $output[] = trim(str::template($template, array( + 'column.name' => $sql->quoteIdentifier($name), + 'column.null' => $null, + 'column.default' => r(!is_null($defaultBinding), 'DEFAULT ' . $defaultBinding), + ))); + + } + + // combine columns + $inner = implode(',' . PHP_EOL, $output); + + // add keys + foreach($keys as $name => $key) { + $inner .= ',' . PHP_EOL . $key . ' (' . $sql->quoteIdentifier($name) . ')'; + } + + // make it a string + $query = 'CREATE TABLE ' . $sql->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')'; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Creates a table with a simple scheme array for columns + * SQLite version + * + * @todo add more options per column + * @param string $table The table name + * @param array $columns + * @return string + */ +sql::registerMethod('createTable', function($sql, $table, $columns = array()) { + + $output = array(); + $keys = array(); + $bindings = array(); + + foreach($columns as $name => $column) { + // column type + if(!isset($column['type'])) throw new Error('No column type given for column ' . $name); + switch($column['type']) { + case 'id': + $template = '{column.name} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE'; + break; + case 'varchar': + $template = '{column.name} TEXT {column.null} {column.key} {column.default}'; + break; + case 'text': + $template = '{column.name} TEXT {column.null} {column.key} {column.default}'; + break; + case 'int': + $template = '{column.name} INTEGER {column.null} {column.key} {column.default}'; + break; + case 'timestamp': + $template = '{column.name} INTEGER {column.null} {column.key} {column.default}'; + break; + default: + throw new Error('Unsupported column type: ' . $column['type']); + } + + // null + if(a::get($column, 'null') === false) { + $null = 'NOT NULL'; + } else { + $null = 'NULL'; + } + + // indexes/keys + $key = false; + if(isset($column['key'])) { + $column['key'] = strtoupper($column['key']); + + // backwards compatibility + if($column['key'] === 'PRIMARY') $column['key'] = 'PRIMARY KEY'; + + if(in_array($column['key'], array('PRIMARY KEY', 'INDEX'))) { + $key = $column['key']; + $keys[$name] = $key; + } + } + + // default value + $default = null; + if(isset($column['default'])) { + // Apparently SQLite doesn't support bindings for default values + $default = "'" . $sql->database->escape($column['default']) . "'"; + } + + $output[] = trim(str::template($template, array( + 'column.name' => $sql->quoteIdentifier($name), + 'column.null' => $null, + 'column.key' => r($key && $key != 'INDEX', $key), + 'column.default' => r(!is_null($default), 'DEFAULT ' . $default), + ))); + + } + + // combine columns + $inner = implode(',' . PHP_EOL, $output); + + // make it a string + $query = 'CREATE TABLE ' . $sql->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')'; + + // set bindings for our first query + $sql->bindings($query, $bindings); + + // add index keys + foreach($keys as $name => $key) { + if($key != 'INDEX') continue; + + $indexQuery = 'CREATE INDEX ' . $sql->quoteIdentifier($table . '_' . $name) . ' ON ' . $sql->quoteIdentifier($table) . ' (' . $sql->quoteIdentifier($name) . ')'; + $query .= ';' . PHP_EOL . $indexQuery; + } + + return $query; + +}, 'sqlite'); + +/** + * Splits a (qualified) identifier into table and column + * + * @param $table string Default table if the identifier is not qualified + * @param $identifier string + * @return array + */ +sql::registerMethod('splitIdentifier', function($sql, $table, $identifier) { + + // split by dot, but only outside of quotes + $parts = preg_split('/(?:`[^`]*`|"[^"]*")(*SKIP)(*F)|\./', $identifier); + + switch(count($parts)) { + // non-qualified identifier + case 1: + return array($table, $sql->unquoteIdentifier($parts[0])); + + // qualified identifier + case 2: + return array($sql->unquoteIdentifier($parts[0]), $sql->unquoteIdentifier($parts[1])); + + // every other number is an error + default: + throw new Error('Invalid identifier ' . $identifier); + } + +}); + +/** + * Unquotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ +sql::registerMethod('unquoteIdentifier', function($sql, $identifier) { + + // remove quotes around the identifier + if(in_array(str::substr($identifier, 0, 1), array('"', '`'))) $identifier = str::substr($identifier, 1); + if(in_array(str::substr($identifier, -1), array('"', '`'))) $identifier = str::substr($identifier, 0, -1); + + // unescape duplicated quotes + return str_replace(array('""', '``'), array('"', '`'), $identifier); + +}); + +/** + * Combines an identifier (table and column) + * MySQL version + * + * @param $table string + * @param $column string + * @param $values boolean Whether the identifier is going to be used for a values clause + * Only relevant for SQLite + * @return string + */ +sql::registerMethod('combineIdentifier', function($sql, $table, $column, $values = false) { + + return $sql->quoteIdentifier($table) . '.' . $sql->quoteIdentifier($column); + +}, 'mysql'); + +/** + * Combines an identifier (table and column) + * SQLite version + * + * @param $table string + * @param $column string + * @param $values boolean Whether the identifier is going to be used for a values clause + * Only relevant for SQLite + * @return string + */ +sql::registerMethod('combineIdentifier', function($sql, $table, $column, $values = false) { + + // SQLite doesn't support qualified column names for VALUES clauses + if($values) return $sql->quoteIdentifier($column); + return $sql->quoteIdentifier($table) . '.' . $sql->quoteIdentifier($column); + +}, 'sqlite'); + +/** + * Quotes an identifier (table *or* column) + * MySQL version + * + * @param $identifier string + * @return string + */ +sql::registerMethod('quoteIdentifier', function($sql, $identifier) { + + // * is special + if($identifier === '*') return $identifier; + + // replace every backtick with two backticks + $identifier = str_replace('`', '``', $identifier); + + // wrap in backticks + return '`' . $identifier . '`'; + +}, 'mysql'); + +/** + * Quotes an identifier (table *or* column) + * SQLite version + * + * @param $identifier string + * @return string + */ +sql::registerMethod('quoteIdentifier', function($sql, $identifier) { + + // * is special + if($identifier === '*') return $identifier; + + // replace every quote with two quotes + $identifier = str_replace('"', '""', $identifier); + + // wrap in quotes + return '"' . $identifier . '"'; + +}, 'sqlite'); + +/** + * Returns a list of tables for a specified database + * MySQL version + * + * @param string $database The database name + * @return string + */ +sql::registerMethod('tableList', function($sql, $database) { + + $bindings = array(); + $databaseBinding = sql::generateBindingName('database'); + $bindings[$databaseBinding] = $database; + + $query = 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $databaseBinding; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Returns a list of tables of the database + * SQLite version + * + * @param string $database The database name + * @return string + */ +sql::registerMethod('tableList', function($sql, $database) { + + return 'SELECT name FROM sqlite_master WHERE type = "table"'; + +}, 'sqlite'); + +/** + * Returns a list of columns for a specified table + * MySQL version + * + * @param string $database The database name + * @param string $table The table name + * @return string + */ +sql::registerMethod('columnList', function($sql, $database, $table) { + + $bindings = array(); + $databaseBinding = sql::generateBindingName('database'); + $bindings[$databaseBinding] = $database; + $tableBinding = sql::generateBindingName('table'); + $bindings[$tableBinding] = $table; + + $query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS '; + $query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding; + + $sql->bindings($query, $bindings); + return $query; + +}, 'mysql'); + +/** + * Returns a list of columns for a specified table + * SQLite version + * + * @param string $database The database name + * @param string $table The table name + * @return string + */ +sql::registerMethod('columnList', function($sql, $database, $table) { + + // validate table + if(!$sql->database->validateTable($table)) throw new Error('Invalid table ' . $table); + + return 'PRAGMA table_info(' . $sql->quoteIdentifier($table) . ')'; + +}, 'sqlite'); diff --git a/kirby/toolkit/lib/str.php b/kirby/toolkit/lib/str.php new file mode 100644 index 0000000..540c59e --- /dev/null +++ b/kirby/toolkit/lib/str.php @@ -0,0 +1,701 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Str { + + public static $ascii = array( + '/Ä/' => 'Ae', + '/æ|ǽ|ä/' => 'ae', + '/œ|ö/' => 'oe', + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|А/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C', + '/ç|ć|ĉ|ċ|č|ц/' => 'c', + '/Ð|Ď|Đ/' => 'Dj', + '/ð|ď|đ/' => 'dj', + '/Д/' => 'D', + '/д/' => 'd', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e', + '/Ф/' => 'F', + '/ƒ|ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G', + '/ĝ|ğ|ġ|ģ|г/' => 'g', + '/Ĥ|Ħ|Х/' => 'H', + '/ĥ|ħ|х/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и/' => 'i', + '/Ĵ|Й/' => 'J', + '/ĵ|й/' => 'j', + '/Ķ|К/' => 'K', + '/ķ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|н/' => 'n', + '/Ö/' => 'Oe', + '/ö/' => 'oe', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|О/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Р/' => 'R', + '/ŕ|ŗ|ř|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|с/' => 's', + '/Ţ|Ț|Ť|Ŧ|Т/' => 'T', + '/ţ|ț|ť|ŧ|т/' => 't', + '/Ü/' => 'Ue', + '/ü/' => 'ue', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у/' => 'u', + '/В/' => 'V', + '/в/' => 'v', + '/Ý|Ÿ|Ŷ|Ы/' => 'Y', + '/ý|ÿ|ŷ|ы/' => 'y', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Ź|Ż|Ž|З/' => 'Z', + '/ź|ż|ž|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/'=> 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ю/' => 'Ju', + '/ю/' => 'ju', + '/Я/' => 'Ja', + '/я/' => 'ja', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + ); + + /** + * Default options for string methods + * + * @var array + */ + public static $defaults = array( + 'slug' => array( + 'separator' => '-', + 'allowed' => 'a-z0-9' + ) + ); + + /** + * Converts a string to a html-safe string + * + * + * + * echo str::html('some über crazy stuff'); + * // output: some über crazy stuff + * + * echo str::html('some über crazy stuff', false); + * // output: some <em>über crazy</em> stuff + * + * + * + * @param string $string + * @param boolean $keepTags True: lets stuff inside html tags untouched. + * @return string The html string + */ + public static function html($string, $keepTags = true) { + return html::encode($string, $keepTags); + } + + /** + * Removes all html tags and encoded chars from a string + * + * + * + * echo str::unhtml('some crazy stuff'); + * // output: some uber crazy stuff + * + * + * + * @param string $string + * @return string The html string + */ + public static function unhtml($string) { + return html::decode($string); + } + + /** + * Converts a string to a xml-safe string + * Converts it to html-safe first and then it + * will replace html entities to xml entities + * + * + * + * echo str::xml('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $text + * @param boolean $html True: convert to html first + * @return string + */ + public static function xml($text, $html = true) { + return xml::encode($text, $html); + } + + /** + * Removes all xml entities from a string + * and convert them to html entities first + * and remove all html entities afterwards. + * + * + * + * echo str::unxml('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @return string + */ + public static function unxml($string) { + return xml::decode($string); + } + + /** + * Parses a string by a set of available methods + * + * Available methods: + * - json + * - xml + * - url + * - query + * - php + * + * + * + * str::parse('{"test":"cool","super":"genious"}'); + * // output: array( + * // 'test' => 'cool', + * // 'super' => 'genious' + * // ); + * + * str::parse('nice', 'xml'); + * // output: array( + * // 'entries' => array( + * // 'cool' => 'nice' + * // ) + * // ); + * + * + * + * @param string $string + * @param string $mode + * @return mixed + */ + public static function parse($string, $mode = 'json') { + + if(is_array($string) || is_object($string)) return $string; + + switch($mode) { + case 'json': + return (array)@json_decode($string, true); + case 'xml': + return xml::parse($string); + case 'url': + return (array)@parse_url($string); + case 'php': + return @unserialize($string); + default: + return $string; + } + + } + + /** + * Encode a string (used for email addresses) + * + * @param string $string + * @return string + */ + public static function encode($string) { + $string = (string)$string; + $encoded = ''; + for($i = 0; $i < static::length($string); $i++) { + $char = static::substr($string, $i, 1); + if(MB) { + list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8')); + } else { + $code = ord($char); + } + + $encoded .= rand(1, 2) == 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';'; + } + return $encoded; + } + + /** + * Generates an "a mailto" tag + * + * + * + * echo str::email('bastian@getkirby.com'); + * echo str::email('bastian@getkirby.com', 'mail me'); + * + * + * + * @param string $email The url for the a tag + * @param mixed $text The optional text. If null, the url will be used as text + * @param array $attr Additional attributes for the tag + * @return string the generated html + */ + public static function email($email, $text = false, $attr = array()) { + return html::email($email, $text, $attr); + } + + /** + * Generates an a tag + * + * @param string $href The url for the a tag + * @param mixed $text The optional text. If null, the url will be used as text + * @param array $attr Additional attributes for the tag + * @return string the generated html + */ + public static function link($href, $text = null, $attr = array()) { + return html::a($href, $text, $attr); + } + + /** + * Returns an array with all words in a string + * + * @param string $string + */ + public static function words($string) { + preg_match_all('/(\pL{4,})/iu', $string, $m); + return array_shift($m); + } + + /** + * Returns an array with all sentences in a string + * + * @param string $string + * @return string + */ + public static function sentences($string) { + return preg_split('/(?<=[.?!])\s+/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * Returns an array with all lines in a string + * + * @param string $string + * @return array + */ + public static function lines($string) { + return str::split($string, PHP_EOL); + } + + /** + * Checks if the given string is a URL + * + * @param string $string + * @return boolean + */ + public static function isURL($string) { + return filter_var($string, FILTER_VALIDATE_URL); + } + + /** + * Shortens a string and adds an ellipsis if the string is too long + * + * + * + * echo str::short('This is a very, very, very long string', 10); + * // output: This is a… + * + * echo str::short('This is a very, very, very long string', 10, '####'); + * // output: This i#### + * + * + * + * @param string $string The string to be shortened + * @param int $length The final number of characters the string should have + * @param string $rep The element, which should be added if the string is too long. Ellipsis is the default. + * @return string The shortened string + */ + public static function short($string, $length, $rep = '…') { + if(!$length) return $string; + if(static::length($string) <= $length) return $string; + $string = static::substr($string, 0, $length); + return $string . $rep; + } + + /** + * Creates an excerpt of a string + * It removes all html tags first and then uses str::short + * + * @param string $string The string to be shortened + * @param int $chars The final number of characters the string should have + * @param boolean $removehtml True: remove the HTML tags from the string first + * @param string $rep The element, which should be added if the string is too long. Ellipsis is the default. + * @return string The shortened string + */ + public static function excerpt($string, $chars = 140, $removehtml = true, $rep='…') { + if($removehtml) $string = strip_tags($string); + $string = str_replace(PHP_EOL, ' ', trim($string)); + if(static::length($string) <= $chars) return $string; + return $chars == 0 ? $string : static::substr($string, 0, strrpos(static::substr($string, 0, $chars), ' ')) . $rep; + } + + /** + * The widont function makes sure that there are no + * typographical widows at the end of a paragraph – + * that's a single word in the last line + * + * @param string $string + * @return string + */ + public static function widont($string = '') { + return preg_replace_callback('|([^\s])\s+([^\s]+)\s*$|', function($matches) { + if(str::contains($matches[2], '-')) { + return $matches[1] . ' ' . str_replace('-', '‑', $matches[2]); + } else { + return $matches[1] . ' ' . $matches[2]; + } + }, $string); + } + + /** + * An UTF-8 safe version of substr() + * + * @param string $str + * @param int $start + * @param int $length + * @return string + */ + public static function substr($str, $start, $length = null) { + $length = $length === null ? static::length($str) : $length; + return MB ? mb_substr($str, $start, $length, 'UTF-8') : substr($str, $start, $length); + } + + /** + * An UTF-8 safe version of strtolower() + * + * @param string $str + * @return string + */ + public static function lower($str) { + return MB ? mb_strtolower($str, 'UTF-8') : strtolower($str); + } + + /** + * An UTF-8 safe version of strotoupper() + * + * @param string $str + * @return string + */ + public static function upper($str) { + return MB ? mb_strtoupper($str, 'UTF-8') : strtoupper($str); + } + + /** + * An UTF-8 safe version of strlen() + * + * @param string $str + * @return string + */ + public static function length($str) { + return MB ? mb_strlen($str, 'UTF-8') : strlen($str); + } + + /** + * Checks if a str contains another string + * + * @param string $str + * @param string $needle + * @param boolean $i ignore upper/lowercase + * @return string + */ + public static function contains($str, $needle, $i = true) { + if($i) { + $str = static::lower($str); + $needle = static::lower($needle); + } + return strstr($str, $needle) ? true : false; + } + + /** + * Generates a random string + * + * @param int $length The length of the random string + * @return string + */ + public static function random($length = false, $type = 'alphaNum') { + $length = $length ? $length : rand(5,10); + $pool = static::pool($type); + shuffle($pool); + $size = count($pool) - 1; + $hash = ''; + for($x = 0; $x < $length; $x++) { + $hash .= $pool[rand(0, $size)]; + } + return $hash; + } + + /** + * Convert a string to a safe version to be used in a URL + * + * @param string $string The unsafe string + * @param string $separator To be used instead of space and other non-word characters. + * @return string The safe string + */ + public static function slug($string, $separator = null, $allowed = null) { + + $separator = $separator ?: static::$defaults['slug']['separator']; + $allowed = $allowed ?: static::$defaults['slug']['allowed']; + + $string = trim($string); + $string = static::lower($string); + $string = static::ascii($string); + + // replace spaces with simple dashes + $string = preg_replace('![^' . $allowed . ']!i', $separator, $string); + // remove double dashes + $string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string); + // trim trailing and leading dashes + $string = trim($string, $separator); + // replace slashes with dashes + $string = str_replace('/', $separator, $string); + + return $string; + + } + + /** + * Better alternative for explode() + * It takes care of removing empty values + * and it has a built-in way to skip values + * which are too short. + * + * @param string $string The string to split + * @param string $separator The string to split by + * @param int $length The min length of values. + * @return array An array of found values + */ + public static function split($string, $separator = ',', $length = 1) { + + if(is_array($string)) return $string; + + $string = trim($string, $separator); + $parts = explode($separator, $string); + $out = array(); + + foreach($parts AS $p) { + $p = trim($p); + if(static::length($p) > 0 && static::length($p) >= $length) $out[] = $p; + } + + return $out; + + } + + /** + * An UTF-8 safe version of ucwords() + * + * @param string $string + * @return string + */ + public static function ucwords($string) { + return MB ? mb_convert_case($string, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($string)); + } + + /** + * An UTF-8 safe version of ucfirst() + * + * @param string $string + * @return string + */ + public static function ucfirst($string) { + return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1)); + } + + /** + * Tries to detect the string encoding + * + * @param string $string + * @return string + */ + public static function encoding($string) { + + if(MB) { + return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251'); + } else { + foreach(array('utf-8', 'iso-8859-1', 'windows-1251') as $item) { + if(md5(iconv($item, $item, $string)) == md5($string)) return $item; + } + return false; + } + + } + + /** + * Converts a string to a different encoding + * + * @param string $string + * @param string $targetEncoding + * @param string $sourceEncoding (optional) + * @return string + */ + public static function convert($string, $targetEncoding, $sourceEncoding = null) { + // detect the source encoding if not passed as third argument + if(is_null($sourceEncoding)) $sourceEncoding = static::encoding($string); + return iconv($sourceEncoding, $targetEncoding, $string); + } + + /** + * Converts a string to UTF-8 + * + * @param string $string + * @return string + */ + public static function utf8($string) { + return static::convert($string, 'utf-8'); + } + + /** + * A better way to strip slashes + * + * @param string $string + * @return string + */ + public static function stripslashes($string) { + if(is_array($string)) return $string; + return get_magic_quotes_gpc() ? stripslashes($string) : $string; + } + + /** + * A super simple string template engine, + * which replaces tags like {mytag} with any other string + * + * @param string $string + * @param array $data An associative array with keys, which should be replaced and values. + * @return string + */ + public static function template($string, $data = array()) { + $replace = array(); + foreach($data as $key => $value) $replace['{' . $key . '}'] = $value; + return str_replace(array_keys($replace), array_values($replace), $string); + } + + /** + * Convert a string to 7-bit ASCII. + * + * @param string $string + * @return string + */ + public static function ascii($string) { + $foreign = static::$ascii; + $string = preg_replace(array_keys($foreign), array_values($foreign), $string); + return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string); + } + + /** + * Forces a download of the string as text file + * + * @param string $string + * @param string $name Optional name for the downloaded file + */ + public static function download($string, $name = null) { + + header::download(array( + 'name' => $name ? $name : 'text.txt', + 'size' => static::length($string), + 'mime' => 'text/plain', + )); + + die($string); + + } + + /** + * Checks if a string starts with the passed needle + * + * @param string $string + * @param string $needle + * @return boolean + */ + public static function startsWith($string, $needle) { + return $needle === '' || strpos($string, $needle) === 0; + } + + /** + * Checks if a string ends with the passed needle + * + * @param string $string + * @param string $needle + * @return boolean + */ + public static function endsWith($string, $needle) { + return $needle === '' || static::substr($string, -static::length($needle)) === $needle; + } + + /** + * Get a character pool with various possible combinations + * + * @param string $type + * @param boolean $array + * @return string + */ + public static function pool($type, $array = true) { + + $pool = array(); + + if(is_array($type)) { + foreach($type as $t) { + $pool = array_merge($pool, static::pool($t)); + } + } else { + + switch($type) { + case 'alphaLower': + $pool = range('a','z'); + break; + case 'alphaUpper': + $pool = range('A', 'Z'); + break; + case 'alpha': + $pool = static::pool(array('alphaLower', 'alphaUpper')); + break; + case 'num': + $pool = range(0, 9); + break; + case 'alphaNum': + $pool = static::pool(array('alpha', 'num')); + break; + } + + } + + return $array ? $pool : implode('', $pool); + + } + +} diff --git a/kirby/toolkit/lib/system.php b/kirby/toolkit/lib/system.php new file mode 100644 index 0000000..929d27d --- /dev/null +++ b/kirby/toolkit/lib/system.php @@ -0,0 +1,150 @@ + + * @link http://getkirby.com + * @copyright Lukas Bestle + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class System { + + /** + * Checks if the system() function is available + * + * @return boolean + */ + public static function available() { + return (!ini_get('safe_mode') && function_exists('exec')); + } + + /** + * Checks if a command is executable + * + * @param string $command Name or path of the command to check + * @return boolean + */ + public static function isExecutable($command) { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // only use the actual command + list($command) = explode(' ', $command); + + // get the path to the executable and check if it exists + $path = static::realpath($command); + return $path !== false; + } + + /** + * Returns the path to a specific executable + * + * @param string $command Name or path of the command + * @return mixed + */ + public static function realpath($command) { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // if this is actually a file, we don't need to search for it any longer + if(file_exists($command)) { + return is_executable($command) ? realpath($command) : false; + } + + // let the shell search for it + // depends on the operating system + if(strtolower(substr(PHP_OS, 0, 3)) === 'win') { + // Windows + // run the "where" command + $result = `where $command`; + // everything besides "Could not find files" would be OK + $exists = !preg_match('/Could not find files/', $result); + } else { + // Unix + // run the "which" command + $result = `which $command`; + // an empty output means there is no path + $exists = !empty($result); + } + + return $exists ? trim($result) : false; + + } + + /** + * Execute a given shell command + * + * @param string $command Name or path of the command + * @param string $arguments Additional arguments + * @param string $what What to return ('status', 'success', 'output' or 'all') + * @return mixed + */ + public static function execute($command, $arguments = array(), $what = 'all') { + // check if everything we need is available + if(!static::available()) { + throw new Exception('The exec() function is not available on this system. Probably, safe_mode is on (shame!).'); + } + + // other ways of calling this method + if(is_array($command)) { + // everything is given as one array + $what = (is_array($arguments))? 'all' : $arguments; + $arguments = array_slice($command, 1); + $command = $command[0]; + } else if(!is_array($arguments)) { + // each additional argument is given as a new method argument + $arguments = array_slice(func_get_args(), 1); + $what = 'all'; + } + + // check if the command exists + if(!static::isExecutable($command)) { + throw new Exception('The command "' . $command . '" is not executable.'); + } + + // escape command + $command = escapeshellcmd($command); + + // escape arguments + array_walk($arguments, function(&$argument) { + $argument = escapeshellarg($argument); + }); + + // execute the command + exec($command . ' ' . implode(' ', $arguments) . ' 2>&1', $output, $status); + + $result = array( + 'output' => implode("\n", $output), + 'status' => $status, + 'success' => $status === 0 + ); + + // return an appropriate result + if($what === 'all' || !array_key_exists($what, $result)) { + return $result; + } else { + return $result[$what]; + } + } + + /** + * Execute a given shell command + * Alias for System::execute() + * + * @param string $command Name or path of the command + * @param string $arguments Additional arguments + * @return array + */ + public static function __callStatic($command, $arguments) { + return static::execute($command, $arguments, 'all'); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/thumb.php b/kirby/toolkit/lib/thumb.php new file mode 100644 index 0000000..bc8b534 --- /dev/null +++ b/kirby/toolkit/lib/thumb.php @@ -0,0 +1,358 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Thumb extends Obj { + + const ERROR_INVALID_IMAGE = 0; + const ERROR_INVALID_DRIVER = 1; + + public static $drivers = array(); + + public static $defaults = array( + 'destination' => false, + 'filename' => '{safeName}-{hash}.{extension}', + 'url' => '/thumbs', + 'root' => '/thumbs', + 'driver' => 'im', + 'memory' => '128M', + 'quality' => 90, + 'blur' => false, + 'blurpx' => 10, + 'width' => null, + 'height' => null, + 'upscale' => false, + 'crop' => false, + 'grayscale' => false, + 'overwrite' => false, + 'autoOrient' => false, + 'interlace' => false + ); + + public $source = null; + public $result = null; + public $destination = null; + public $options = array(); + public $error = null; + + /** + * Constructor + * + * @param mixed $source + * @param array $params + */ + public function __construct($source, $params = array()) { + + $this->source = $this->result = is_a($source, 'Media') ? $source : new Media($source); + $this->options = array_merge(static::$defaults, $this->params($params)); + $this->destination = $this->destination(); + + // don't create the thumbnail if it's not necessary + if($this->isObsolete()) return; + + // don't create the thumbnail if it exists + if(!$this->isThere()) { + + // try to create the thumb folder if it is not there yet + dir::make(dirname($this->destination->root)); + + // check for a valid image + if(!$this->source->exists() || $this->source->type() != 'image') { + throw new Error('The given image is invalid', static::ERROR_INVALID_IMAGE); + } + + // check for a valid driver + if(!array_key_exists($this->options['driver'], static::$drivers)) { + throw new Error('Invalid thumbnail driver', static::ERROR_INVALID_DRIVER); + } + + // create the thumbnail + $this->create(); + + // check if creating the thumbnail failed + if(!file_exists($this->destination->root)) return; + + } + + // create the result object + $this->result = new Media($this->destination->root, $this->destination->url); + + } + + /** + * Build the destination object + * + * @return Obj + */ + public function destination() { + + if(is_callable($this->options['destination'])) { + return call($this->options['destination'], $this); + } else { + + $destination = new Obj(); + $safeName = f::safeName($this->source->name()); + + $destination->filename = str::template($this->options['filename'], array( + 'extension' => $this->source->extension(), + 'name' => $this->source->name(), + 'filename' => $this->source->filename(), + 'safeName' => $safeName, + 'safeFilename' => $safeName . '.' . $this->extension(), + 'width' => $this->options['width'], + 'height' => $this->options['height'], + 'hash' => md5($this->source->root() . $this->settingsIdentifier()), + )); + + $destination->url = $this->options['url'] . '/' . $destination->filename; + $destination->root = $this->options['root'] . DS . $destination->filename; + + return $destination; + + } + + } + + /** + * Returns the source media object + * + * @return Media + */ + public function source() { + return $this->source; + } + + /** + * Returns the exception if available + * + * @return Exception + */ + public function error() { + return $this->error; + } + + /** + * Makes it possible to pass a string of params + * which is shorter and more convenient than + * passing a full array of keys and values: + * width:300|height:200|crop:true + * + * @param array $params + * @return array + */ + public function params($params) { + if(is_array($params)) return $params; + $result = array(); + foreach(explode('|', $params) as $param) { + $pos = strpos($param, ':'); + $result[trim(substr($param, 0, $pos))] = trim(substr($param, $pos+1)); + } + return $result; + } + + /** + * Builds a hash for all relevant settings + * + * @return string + */ + public function settingsIdentifier() { + + // build the settings string + return implode('-', array( + ($this->options['width']) ? $this->options['width'] : 0, + ($this->options['height']) ? $this->options['height'] : 0, + ($this->options['upscale']) ? $this->options['upscale'] : 0, + ($this->options['crop']) ? $this->options['crop'] : 0, + $this->options['blur'], + $this->options['grayscale'], + $this->options['quality'] + )); + + } + + /** + * Checks if the thumbnail already exists + * and is newer than the original file + * + * @return boolean + */ + public function isThere() { + + if($this->options['overwrite'] === true) return false; + + // if the thumb already exists and the source hasn't been updated + // we don't need to generate a new thumbnail + if(file_exists($this->destination->root) && f::modified($this->destination->root) >= $this->source->modified()) return true; + + return false; + + } + + /** + * Checks if the thumbnail is not needed + * because the original image is small enough + * + * @return boolean + */ + public function isObsolete() { + + if($this->options['overwrite'] === true) return false; + + // try to use the original if resizing is not necessary + if($this->options['width'] >= $this->source->width() && + $this->options['height'] >= $this->source->height() && + $this->options['crop'] == false && + $this->options['blur'] == false && + $this->options['upscale'] == false) return true; + + return false; + + } + + /** + * Calls the driver function and + * creates the thumbnail + */ + protected function create() { + return call_user_func_array(static::$drivers[$this->options['driver']], array($this)); + } + + /** + * Makes all public methods of the result object + * available to the thumb class + * + * @param string $method + * @param mixed $arguments + * @return mixed + */ + public function __call($method, $arguments) { + + if(method_exists($this->result, $method)) { + return call_user_func_array(array($this->result, $method), $arguments); + } + + } + + /** + * Generates and returns the full html tag for the thumbnail + * + * @param array $attr An optional array of attributes, which should be added to the image tag + * @return string + */ + public function tag($attr = array()) { + + // don't return the tag if the url is not available + if(!$this->result->url()) return false; + + return html::img($this->result->url(), array_merge(array( + 'alt' => isset($this->options['alt']) ? $this->options['alt'] : ' ', + 'class' => isset($this->options['class']) ? $this->options['class'] : null, + ), $attr)); + + } + + /** + * Makes it possible to echo the entire object + */ + public function __toString() { + return $this->tag(); + } + +} + + + +/** + * ImageMagick Driver + */ +thumb::$drivers['im'] = function($thumb) { + + $command = array(); + + $command[] = isset($thumb->options['bin']) ? $thumb->options['bin'] : 'convert'; + $command[] = '"' . $thumb->source->root() . '"'; + $command[] = '-strip'; + + if($thumb->options['interlace']) { + $command[] = '-interlace line'; + } + + if($thumb->source->extension() === 'gif') { + $command[] = '-coalesce'; + } + + if($thumb->options['grayscale']) { + $command[] = '-colorspace gray'; + } + + if($thumb->options['autoOrient']) { + $command[] = '-auto-orient'; + } + + $command[] = '-resize'; + + if($thumb->options['crop']) { + $command[] = $thumb->options['width'] . 'x' . $thumb->options['height'] . '^'; + $command[] = '-gravity Center -crop ' . $thumb->options['width'] . 'x' . $thumb->options['height'] . '+0+0'; + } else { + $dimensions = clone $thumb->source->dimensions(); + $dimensions->fitWidthAndHeight($thumb->options['width'], $thumb->options['height'], $thumb->options['upscale']); + $command[] = $dimensions->width() . 'x' . $dimensions->height() . '!'; + } + + $command[] = '-quality ' . $thumb->options['quality']; + + if($thumb->options['blur']) { + $command[] = '-blur 0x' . $thumb->options['blurpx']; + } + + $command[] = '-limit thread 1'; + $command[] = '"' . $thumb->destination->root . '"'; + + exec(implode(' ', $command)); + +}; + + +/** + * GDLib Driver + */ +thumb::$drivers['gd'] = function($thumb) { + + try { + $img = new abeautifulsite\SimpleImage($thumb->root()); + $img->quality = $thumb->options['quality']; + + if($thumb->options['crop']) { + @$img->thumbnail($thumb->options['width'], $thumb->options['height']); + } else { + $dimensions = clone $thumb->source->dimensions(); + $dimensions->fitWidthAndHeight($thumb->options['width'], $thumb->options['height'], $thumb->options['upscale']); + @$img->resize($dimensions->width(), $dimensions->height()); + } + + if($thumb->options['grayscale']) { + $img->desaturate(); + } + + if($thumb->options['blur']) { + $img->blur('gaussian', $thumb->options['blurpx']); + } + + if($thumb->options['autoOrient']) { + $img->auto_orient(); + } + + @$img->save($thumb->destination->root); + } catch(Exception $e) { + $thumb->error = $e; + } + +}; diff --git a/kirby/toolkit/lib/timer.php b/kirby/toolkit/lib/timer.php new file mode 100644 index 0000000..0325205 --- /dev/null +++ b/kirby/toolkit/lib/timer.php @@ -0,0 +1,28 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Timer { + + public static $time = null; + + public static function start() { + $time = explode(' ', microtime()); + static::$time = (double)$time[1] + (double)$time[0]; + } + + public static function stop() { + $time = explode(' ', microtime()); + $time = (double)$time[1] + (double)$time[0]; + $timer = static::$time; + return round(($time-$timer), 5); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/toolkit.php b/kirby/toolkit/lib/toolkit.php new file mode 100644 index 0000000..2f2a77e --- /dev/null +++ b/kirby/toolkit/lib/toolkit.php @@ -0,0 +1,20 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Toolkit { + + public static $version = '2.3.0'; + + public static function version() { + return static::$version; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/tpl.php b/kirby/toolkit/lib/tpl.php new file mode 100644 index 0000000..cc06212 --- /dev/null +++ b/kirby/toolkit/lib/tpl.php @@ -0,0 +1,29 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Tpl extends Silo { + + public static $data = array(); + + public static function load($_file, $_data = array(), $_return = true) { + if(!file_exists($_file)) return false; + ob_start(); + extract(array_merge(static::$data, (array)$_data)); + require($_file); + $_content = ob_get_contents(); + ob_end_clean(); + if($_return) return $_content; + echo $_content; + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/upload.php b/kirby/toolkit/lib/upload.php new file mode 100644 index 0000000..6a306e0 --- /dev/null +++ b/kirby/toolkit/lib/upload.php @@ -0,0 +1,202 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Upload { + + const ERROR_FAILED_UPLOAD = 0; + const ERROR_MISSING_TMP_DIR = 1; + const ERROR_MISSING_FILE = 2; + const ERROR_UNALLOWED_OVERWRITE = 3; + const ERROR_PARTIAL_UPLOAD = 4; + const ERROR_MAX_SIZE = 5; + const ERROR_MOVE_FAILED = 6; + const ERROR_UNACCEPTED = 7; + + public $options = array(); + public $error = null; + public $file = null; + public $to = null; + + public function __construct($to, $params = array()) { + + $defaults = array( + 'input' => 'file', + 'index' => 0, + 'to' => $to, + 'overwrite' => true, + 'maxSize' => false, + 'accept' => null, + ); + + $this->options = array_merge($defaults, $params); + + try { + $this->move(); + $this->file = new Media($this->to()); + } catch(Exception $e) { + $this->error = $e; + } + + } + + public function error() { + return $this->error; + } + + public function source() { + + $source = isset($_FILES[$this->options['input']]) ? $_FILES[$this->options['input']] : null; + + // get the correct file out of multiple based on the "index" option + if($source && is_int($this->options['index']) && is_array($source['name'])) { + $allSources = $source; + $source = array(); + + // get the correct value out of the $values array with all files + foreach($allSources as $key => $values) { + $source[$key] = isset($values[$this->options['index']]) ? $values[$this->options['index']] : null; + } + } + + // prevent duplicate ios uploads + // ios automatically uploads all images as image.jpg, + // which will lead to overwritten duplicates. + // this dirty hack will simply add a uniqid between the + // name and the extension to avoid duplicates + if($source && f::name($source['name']) == 'image' && detect::ios()) { + $source['name'] = 'image-' . uniqid() . '.' . ltrim(f::extension($source['name']), '.'); + } + + return $source; + + } + + public function to() { + + if(!is_null($this->to)) return $this->to; + + $source = $this->source(); + $name = f::name($source['name']); + $extension = f::extension($source['name']); + $safeName = f::safeName($name); + $safeExtension = str_replace('jpeg', 'jpg', str::lower($extension)); + + if(empty($safeExtension)) { + $safeExtension = f::mimeToExtension(f::mime($source['tmp_name'])); + } + + return $this->to = str::template($this->options['to'], array( + 'name' => $name, + 'filename' => $source['name'], + 'safeName' => $safeName, + 'safeFilename' => $safeName . r(!empty($safeExtension), '.' . $safeExtension), + 'extension' => $extension, + 'safeExtension' => $safeExtension + )); + + } + + /** + * Returns the maximum accepted file size + * + * @return int + */ + public function maxSize() { + $sizes = array(detect::maxPostSize(), detect::maxUploadSize()); + if($this->options['maxSize']) { + $sizes[] = $this->options['maxSize']; + } + return min($sizes); + } + + public function file() { + return $this->file; + } + + protected function move() { + + $source = $this->source(); + + if(is_null($source['name']) || is_null($source['tmp_name'])) { + $this->fail(static::ERROR_MISSING_FILE); + } + + if($source['error'] !== 0) { + + switch($source['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->fail(static::ERROR_MAX_SIZE); + case UPLOAD_ERR_PARTIAL: + $this->fail(static::ERROR_PARTIAL_UPLOAD); + case UPLOAD_ERR_NO_FILE: + $this->fail(static::ERROR_MISSING_FILE); + case UPLOAD_ERR_NO_TMP_DIR: + $this->fail(static::ERROR_MISSING_TMP_DIR); + case UPLOAD_ERR_CANT_WRITE: + $this->fail(static::ERROR_MOVE_FAILED); + case UPLOAD_ERR_EXTENSION: + $this->fail(static::ERROR_UNACCEPTED); + default: + $this->fail(static::ERROR_FAILED_UPLOAD); + } + + } + + if(file_exists($this->to()) && $this->options['overwrite'] === false) { + $this->fail(static::ERROR_UNALLOWED_OVERWRITE); + } + + if($this->options['maxSize'] && $source['size'] > $this->options['maxSize']) { + $this->fail(static::ERROR_MAX_SIZE); + } + + if(is_callable($this->options['accept'])) { + $accepted = call($this->options['accept'], new Media($source['tmp_name'])); + if($accepted === false) { + $this->fail(static::ERROR_UNACCEPTED); + } + } + + if(!@move_uploaded_file($source['tmp_name'], $this->to())) { + $this->fail(static::ERROR_MOVE_FAILED); + } + + } + + protected function messages() { + return array( + static::ERROR_MISSING_FILE => 'The file is missing', + static::ERROR_MISSING_TMP_DIR => 'The /tmp directory is missing on your server', + static::ERROR_FAILED_UPLOAD => 'The upload failed', + static::ERROR_PARTIAL_UPLOAD => 'The file has been only been partially uploaded', + static::ERROR_UNALLOWED_OVERWRITE => 'The file exists and cannot be overwritten', + static::ERROR_MAX_SIZE => 'The file is too big. The maximum size is ' . f::niceSize($this->maxSize()), + static::ERROR_MOVE_FAILED => 'The file could not be moved', + static::ERROR_UNACCEPTED => 'The file is not accepted by the server' + ); + } + + protected function fail($code) { + + $messages = $this->messages(); + + if(!isset($messages[$code])) { + $code = static::ERROR_FAILED_UPLOAD; + } + + throw new Error($messages[$code], $code); + + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/url.php b/kirby/toolkit/lib/url.php new file mode 100644 index 0000000..bedc9c6 --- /dev/null +++ b/kirby/toolkit/lib/url.php @@ -0,0 +1,391 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Url { + + public static $home = '/'; + public static $to = null; + public static $current = null; + + public static function scheme($url = null) { + if(is_null($url)) { + if( + (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') || + server::get('SERVER_PORT') == '443' || + server::get('HTTP_X_FORWARDED_PORT') == '443' || + server::get('HTTP_X_FORWARDED_PROTO') == 'https' || + server::get('HTTP_X_FORWARDED_PROTO') == 'https, http' + ) { + return 'https'; + } else { + return 'http'; + } + } + return parse_url($url, PHP_URL_SCHEME); + + } + + /** + * Returns the current url with all bells and whistles + * + * @return string + */ + public static function current() { + if(!is_null(static::$current)) return static::$current; + return static::$current = static::base() . server::get('REQUEST_URI'); + } + + /** + * Returns the url for the current directory + * + * @return string + */ + public static function currentDir() { + return dirname(static::current()); + } + + /** + */ + public static function host($url = null) { + if(is_null($url)) $url = static::current(); + return parse_url($url, PHP_URL_HOST); + } + + /** + * Returns the port for the given url + * + * @return mixed + */ + public static function port($url = null) { + if(is_null($url)) $url = static::current(); + $port = intval(parse_url($url, PHP_URL_PORT)); + return v::between($port, 1, 65535) ? $port : false; + } + + /** + * Returns only the cleaned path of the url + */ + public static function path($url = null) { + + if(is_null($url)) $url = static::current(); + + // if a path is passed, let's pretend this is an absolute url + // to trick the url parser. It's a bit hacky but it works + if(!static::isAbsolute($url)) $url = 'http://0.0.0.0/' . $url; + + return trim(parse_url($url, PHP_URL_PATH), '/'); + + } + + /** + * Returns the correct separator for parameters + * depending on the operating system + * + * @return string + */ + public static function paramSeparator() { + return detect::windows() ? ';' : ':'; + } + + /** + * Returns the params in the url + */ + public static function params($url = null) { + if(is_null($url)) $url = static::current(); + $path = static::path($url); + if(empty($path)) return array(); + $params = array(); + foreach(explode('/', $path) as $part) { + $pos = strpos($part, static::paramSeparator()); + if($pos === false) continue; + $params[substr($part, 0, $pos)] = urldecode(substr($part, $pos+1)); + } + return $params; + } + + /** + * Returns the path without params + */ + public static function fragments($url = null) { + if(is_null($url)) $url = static::current(); + $path = static::path($url); + if(empty($path)) return null; + $frag = array(); + foreach(explode('/', $path) as $part) { + if(strpos($part, static::paramSeparator()) === false) $frag[] = $part; + } + return $frag; + } + + /** + * Returns the query as array + */ + public static function query($url = null) { + if(is_null($url)) $url = static::current(); + parse_str(parse_url($url, PHP_URL_QUERY), $array); + return $array; + } + + /** + * Checks if the url contains a query string + */ + public static function hasQuery($url = null) { + if(is_null($url)) $url = static::current(); + return str::contains($url, '?'); + } + + /** + */ + public static function hash($url = null) { + if(is_null($url)) $url = static::current(); + return parse_url($url, PHP_URL_FRAGMENT); + } + + public static function build($parts = array(), $url = null) { + + if(is_null($url)) $url = static::current(); + + $defaults = array( + 'scheme' => static::scheme($url), + 'host' => static::host($url), + 'port' => static::port($url), + 'fragments' => static::fragments($url), + 'params' => static::params($url), + 'query' => static::query($url), + 'hash' => static::hash($url), + ); + + $parts = array_merge($defaults, $parts); + $result = array(r(!empty($parts['scheme']), $parts['scheme'] . '://') . $parts['host'] . r(!empty($parts['port']), ':' . $parts['port'])); + + if(!empty($parts['fragments'])) $result[] = implode('/', $parts['fragments']); + if(!empty($parts['params'])) $result[] = static::paramsToString($parts['params']); + if(!empty($parts['query'])) $result[] = '?' . static::queryToString($parts['query']); + + return implode('/', $result) . (!empty($parts['hash']) ? '#' . $parts['hash'] : ''); + + } + + public static function queryToString($query = null) { + if(is_null($query)) $query = url::query(); + return http_build_query($query); + } + + public static function paramsToString($params = null) { + if(is_null($params)) $params = url::params(); + $result = array(); + foreach($params as $key => $val) $result[] = $key . static::paramSeparator() . $val; + return implode('/', $result); + } + + public static function stripPath($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('fragments' => array(), 'params' => array()), $url); + } + + public static function stripFragments($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('fragments' => array()), $url); + } + + public static function stripParams($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('params' => array()), $url); + } + + /** + * Strips the query from the URL + * + * + * + * echo url::stripQuery('http://www.youtube.com/watch?v=9q_aXttJduk'); + * // output: http://www.youtube.com/watch + * + * + * + * @param string $url + * @return string + */ + public static function stripQuery($url = null) { + if(is_null($url)) $url = static::current(); + return static::build(array('query' => array()), $url); + } + + /** + * Strips a hash value from the URL + * + * + * + * echo url::stripHash('http://testurl.com/#somehash'); + * // output: http://testurl.com/ + * + * + * + * @param string $url + * @return string + */ + public static function stripHash($url) { + if(is_null($url)) $url = static::current(); + return static::build(array('hash' => ''), $url); + } + + /** + * Checks if an URL is absolute + * + * @return boolean + */ + public static function isAbsolute($url) { + // don't convert absolute urls + return (str::startsWith($url, 'http://') || str::startsWith($url, 'https://') || str::startsWith($url, '//')); + } + + /** + * Convert a relative path into an absolute URL + * + * @param string $path + * @param string $home + * @return string + */ + public static function makeAbsolute($path, $home = null) { + + if(static::isAbsolute($path)) return $path; + + // build the full url + $path = ltrim($path, '/'); + $home = is_null($home) ? static::$home : $home; + + if(empty($path)) return $home; + + return $home == '/' ? '/' . $path : $home . '/' . $path; + + } + + /** + * Tries to fix a broken url without protocol + * + * @param string $url + * @return string + */ + public static function fix($url) { + // make sure to not touch absolute urls + return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url)) ? 'http://' . $url : $url; + } + + /** + * Returns the home url if defined + * + * @return string + */ + public static function home() { + return static::$home; + } + + /** + * The url smart handler. Must be defined before + * + * @return string + */ + public static function to() { + return call_user_func_array(static::$to, func_get_args()); + } + + /** + * Return the last url the user has been on if detectable + * + * @return string + */ + public static function last() { + return r::referer(); + } + + /** + * Returns the base url + * + * @param string $url + * @return string + */ + public static function base($url = null) { + if(is_null($url)) { + $port = server::get('SERVER_PORT'); + $port = in_array($port, array(80, 443)) ? null : $port; + return static::scheme() . '://' . server::get('SERVER_NAME', server::get('SERVER_ADDR')) . r($port, ':' . $port); + } else { + $port = static::port($url); + $scheme = static::scheme($url); + $host = static::host($url) . r(is_int($port), ':' . $port); + return r($scheme, $scheme . '://') . $host; + } + } + + /** + * Shortens a URL + * It removes http:// or https:// and uses str::short afterwards + * + * + * + * echo url::short('http://veryveryverylongurl.com', 30); + * // output: veryveryverylongurl.com + * + * + * + * @param string $url The URL to be shortened + * @param int $length The final number of characters the URL should have + * @param boolean $base True: only take the base of the URL. + * @param string $rep The element, which should be added if the string is too long. Ellipsis is the default. + * @return string The shortened URL + */ + public static function short($url, $length = false, $base = false, $rep = '…') { + + if($base) $url = static::base($url); + + // replace all the nasty stuff from the url + $url = str_replace(array('http://', 'https://', 'ftp://', 'www.'), '', $url); + + // try to remove the last / after the url + $url = rtrim($url, '/'); + + return ($length) ? str::short($url, $length, $rep) : $url; + + } + + /** + * Returns the URL for document root no + * matter what the path is. + * + * @return string + */ + public static function index() { + if(r::cli()) { + return '/'; + } else { + return static::base() . preg_replace('!\/index\.php$!i', '', server::get('SCRIPT_NAME')); + } + } + +} + +// basic home url setup +url::$home = url::base(); + +// basic url generator setup +url::$to = function($path = '/') { + + if(url::isAbsolute($path)) return $path; + + $path = ltrim($path, '/'); + + if(empty($path)) return url::home(); + + return url::home() . '/' . $path; + +}; diff --git a/kirby/toolkit/lib/v.php b/kirby/toolkit/lib/v.php new file mode 100644 index 0000000..62a408d --- /dev/null +++ b/kirby/toolkit/lib/v.php @@ -0,0 +1,131 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class V { + + // an array with all installed validators + public static $validators = array(); + + /** + * Return the list of all validators + * + * @return array + */ + public static function validators() { + return static::$validators; + } + + /** + * Calls an installed validator and passes all arguments + * + * @param string $method + * @param array $arguments + * @return boolean + */ + public static function __callStatic($method, $arguments) { + + // check for missing validators + if(!isset(static::$validators[$method])) throw new Exception('The validator does not exist: ' . $method); + + return call_user_func_array(static::$validators[$method], $arguments); + + } + +} + + +/** + * Default set of validators + */ +v::$validators = array( + 'accepted' => function($value) { + return v::in($value, array(1, true, 'yes', 'true', '1', 'on')); + }, + 'denied' => function($value) { + return v::in($value, array(0, false, 'no', 'false', '0', 'off')); + }, + 'alpha' => function($value) { + return v::match($value, '/^([a-z])+$/i'); + }, + 'alphanum' => function($value) { + return v::match($value, '/^[a-z0-9]+$/i'); + }, + 'between' => function($value, $min, $max) { + return v::min($value, $min) && v::max($value, $max); + }, + 'date' => function($value) { + $time = strtotime($value); + if(!is_int($time)) return false; + + $year = date('Y', $time); + $month = date('m', $time); + $day = date('d', $time); + + return checkdate($month, $day, $year); + + }, + 'different' => function($value, $other) { + return $value !== $other; + }, + 'email' => function($value) { + return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; + }, + 'filename' => function($value) { + return v::match($value, '/^[a-z0-9@._-]+$/i') && v::min($value, 2); + }, + 'in' => function($value, $in) { + return in_array($value, $in, true); + }, + 'integer' => function($value) { + return filter_var($value, FILTER_VALIDATE_INT) !== false; + }, + 'ip' => function($value) { + return filter_var($value, FILTER_VALIDATE_IP) !== false; + }, + 'match' => function($value, $preg) { + return preg_match($preg, $value) > 0; + }, + 'max' => function($value, $max) { + return size($value) <= $max; + }, + 'min' => function($value, $min) { + return size($value) >= $min; + }, + 'maxWords' => function($value, $max) { + return v::max(explode(' ', $value), $max); + }, + 'minWords' => function($value, $min) { + return v::min(explode(' ', $value), $min); + }, + 'notIn' => function($value, $notIn) { + return !v::in($value, $notIn); + }, + 'num' => function($value) { + return is_numeric($value); + }, + 'required' => function($key, $array) { + return !empty($array[$key]); + }, + 'same' => function($value, $other) { + return $value === $other; + }, + 'size' => function($value, $size) { + return size($value) == $size; + }, + 'url' => function($value) { + // In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex + $regex = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iu'; + return preg_match($regex, $value) !== 0; + } +); diff --git a/kirby/toolkit/lib/visitor.php b/kirby/toolkit/lib/visitor.php new file mode 100644 index 0000000..b42ff31 --- /dev/null +++ b/kirby/toolkit/lib/visitor.php @@ -0,0 +1,97 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Visitor { + + // banned ips + public static $banned = array(); + + // cache for the detected language code + protected static $acceptedLanguageCode = null; + + /** + * Returns the ip address of the current visitor + * + * @return string + */ + public static function ip() { + return getenv('REMOTE_ADDR'); + } + + /** + * Returns the user agent string of the current visitor + * + * @return string + */ + public static function ua() { + return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + /** + * A more readable but longer alternative for ua() + * + * @return string + */ + public static function userAgent() { + return static::ua(); + } + + /** + * Returns the user's accepted language + * + * @return string + */ + public static function acceptedLanguage() { + return isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; + } + + /** + * Returns the user's accepted language code + * + * @return string + */ + public static function acceptedLanguageCode() { + if(!is_null(static::$acceptedLanguageCode)) return static::$acceptedLanguageCode; + $detected = explode(',', static::acceptedLanguage()); + $detected = explode('-', $detected[0]); + return static::$acceptedLanguageCode = strtolower($detected[0]); + } + + /** + * Returns the referrer if available + * + * @return string + */ + public static function referrer() { + return r::referer(); + } + + /** + * Nobody can remember if it is written with on or two r + * + * @return string + */ + public static function referer() { + return r::referer(); + } + + /** + * Checks if the ip of the current visitor is banned + * + * @return boolean + */ + public static function banned() { + return in_array(static::ip(), static::$banned); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/lib/xml.php b/kirby/toolkit/lib/xml.php new file mode 100644 index 0000000..d88dc87 --- /dev/null +++ b/kirby/toolkit/lib/xml.php @@ -0,0 +1,144 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Xml { + + /** + * Converts a string to a xml-safe string + * Converts it to html-safe first and then it + * will replace html entities to xml entities + * + * + * + * echo xml::encode('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @param boolean $html True: convert to html first + * @return string + */ + public static function encode($string, $html = true) { + + // convert raw text to html safe text + if($html) { + $string = html::encode($string, false); + } + + // convert html entities to xml entities + return strtr($string, html::entities()); + + } + + /** + * Removes all xml entities from a string + * and convert them to html entities first + * and remove all html entities afterwards. + * + * + * + * echo xml::decode('some über crazy stuff'); + * // output: some über crazy stuff + * + * + * + * @param string $string + * @return string + */ + public static function decode($string) { + // convert xml entities to html entities + $string = strtr($string, static::entities()); + return html::decode($string); + } + + /** + * Parses a XML string and returns an array + * + * @param string $xml + * @return mixed + */ + public static function parse($xml) { + + $xml = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $xml); + $xml = @simplexml_load_string($xml, null, LIBXML_NOENT | LIBXML_NOCDATA); + $xml = @json_encode($xml); + $xml = @json_decode($xml, true); + return (is_array($xml)) ? $xml : false; + + } + + /** + * Returns a translation table of xml entities to html entities + * + * @return array + */ + public static function entities() { + return array_flip(html::entities()); + } + + /** + * Creates an XML string from an array + * + * @param array $array The source array + * @param string $tag The name of the root element + * @param boolean $head Include the xml declaration head or not + * @param string $charset The charset, which should be used for the header + * @param int $level The indendation level + * @return string The XML string + */ + public static function create($array, $tag = 'root', $head = true, $charset = 'utf-8', $tab = ' ', $level = 0) { + $result = ($level == 0 && $head) ? '' . PHP_EOL : ''; + $nlevel = ($level + 1); + $attr = '@attributes'; + $attributes = html::attr(a::get($array, $attr)); + if(count($array) == 1 and $attributes) { + // return the self closed node + return str_repeat($tab, $level) . '<' . $tag . ($attributes ? ' ' . $attributes : '') . ' />' . PHP_EOL; + } else { + $result .= str_repeat($tab, $level) . '<' . $tag . ($attributes ? ' ' . $attributes . ' ' : '') . '>' . PHP_EOL; + } + foreach($array as $key => $value) { + $key = str::lower($key); + if($key == $attr) { + continue; + } + if(is_array($value)) { + $mtags = false; + foreach($value as $key2 => $value2) { + if($key2 == $attr) { + continue; + } + if(is_array($value2)) { + $result .= static::create($value2, $key2, $head, $charset, $tab, $nlevel); + } elseif(!is_numeric($key)) { + $result .= static::create($value, $key, $head, $charset, $tab, $nlevel); + } elseif(trim($value2) != '') { + $value2 = (!strstr($value2, '' : $value2; + $result .= str_repeat($tab, $nlevel) . '<' . $key2 . '>' . $value2 . '' . PHP_EOL; + } + $mtags = true; + } + if(!$mtags && count($value) > 0) { + $result .= static::create($value, $key, $head, $charset, $tab, $nlevel); + } + } elseif(trim($value) != '') { + $value = (!strstr($value, '' : $value; + $result .= str_repeat($tab, $nlevel) . (is_numeric($key) ? '' : '<' . $key . '>') . $value . (is_numeric($key) ? '' : '') . PHP_EOL; + } + } + return $result . str_repeat($tab, $level) . '' . PHP_EOL; + } + +} diff --git a/kirby/toolkit/lib/yaml.php b/kirby/toolkit/lib/yaml.php new file mode 100644 index 0000000..b1fc8d5 --- /dev/null +++ b/kirby/toolkit/lib/yaml.php @@ -0,0 +1,57 @@ + + * @link http://getkirby.com + * @copyright Bastian Allgeier + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Yaml { + + /** + * Creates a new yaml string from an array + * + * @param array $array + * @return string + */ + public static function encode($array) { + return preg_replace('!^---\n!', '', spyc::yamldump($array)); + } + + /** + * Creates a new yaml file from an array + * + * @param string $file + * @param array $array + * @return boolean + */ + public static function write($file, $array) { + return f::write($file, static::encode($array)); + } + + /** + * Parses a yaml string and returns the array + * + * @param string $yaml + * @return array + */ + public static function decode($yaml) { + return spyc::yamlload($yaml); + } + + /** + * Reads and parses a yaml file and returns the array + * + * @param string $file + * @return array + */ + public static function read($file) { + return spyc::yamlload($file); + } + +} \ No newline at end of file diff --git a/kirby/toolkit/readme.md b/kirby/toolkit/readme.md new file mode 100644 index 0000000..7a094e7 --- /dev/null +++ b/kirby/toolkit/readme.md @@ -0,0 +1,32 @@ +# Kirby Toolkit + +The Kirby 2 toolkit is a set of handy classes and helpers which make your life with PHP easier. + +[![Build Status](https://travis-ci.org/getkirby/toolkit.svg?branch=master)](https://travis-ci.org/getkirby/toolkit) + +## Installation + +```` +git pull https://github.com/getkirby/toolkit.git +```` + +Adding the toolkit to your app… + +```php + +``` + +## Requirements + +PHP 5.4+ + +## License + + + +## Author + +Bastian Allgeier + + + diff --git a/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php b/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php new file mode 100755 index 0000000..4af7f08 --- /dev/null +++ b/kirby/toolkit/vendors/abeautifulsite/SimpleImage.php @@ -0,0 +1,1285 @@ + - merging of forks, namespace support, PhpDoc editing, adaptive_resize() method, other fixes + * @license This software is licensed under the MIT license: http://opensource.org/licenses/MIT + * @copyright A Beautiful Site, LLC + * + */ + +namespace abeautifulsite; +use Exception; + +/** + * Class SimpleImage + * This class makes image manipulation in PHP as simple as possible. + * @package SimpleImage + * + */ +class SimpleImage { + + /** + * @var int Default output image quality + * + */ + public $quality = 80; + + protected $image, $filename, $original_info, $width, $height, $imagestring, $exif; + + /** + * Create instance and load an image, or create an image from scratch + * + * @param null|string $filename Path to image file (may be omitted to create image from scratch) + * @param int $width Image width (is used for creating image from scratch) + * @param int|null $height If omitted - assumed equal to $width (is used for creating image from scratch) + * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127
+ * (is used for creating image from scratch) + * + * @throws Exception + * + */ + public function __construct($filename = null, $width = null, $height = null, $color = null) { + if ($filename !== null) { + $this->load($filename); + } elseif ($width !== null) { + $this->create($width, $height, $color); + } + } + + /** + * Destroy image resource + * + */ + public function __destruct() { + if( get_resource_type($this->image) === 'gd' ) { + imagedestroy($this->image); + } + } + + /** + * Adaptive resize + * + * This function has been deprecated and will be removed in an upcoming release. Please + * update your code to use the `thumbnail()` method instead. The arguments for both + * methods are exactly the same. + * + * @param int $width + * @param int|null $height If omitted - assumed equal to $width + * + * @return SimpleImage + * + */ + public function adaptive_resize($width, $height = null) { + + return $this->thumbnail($width, $height); + + } + + /** + * Rotates and/or flips an image automatically so the orientation will be correct (based on exif 'Orientation') + * + * @return SimpleImage + * + */ + public function auto_orient() { + + // stop if there's no exif data + if(!isset($this->original_info['exif']['Orientation'])) { + return $this; + } + + switch ($this->original_info['exif']['Orientation']) { + case 1: + // Do nothing + break; + case 2: + // Flip horizontal + $this->flip('x'); + break; + case 3: + // Rotate 180 counterclockwise + $this->rotate(-180); + break; + case 4: + // vertical flip + $this->flip('y'); + break; + case 5: + // Rotate 90 clockwise and flip vertically + $this->flip('y'); + $this->rotate(90); + break; + case 6: + // Rotate 90 clockwise + $this->rotate(90); + break; + case 7: + // Rotate 90 clockwise and flip horizontally + $this->flip('x'); + $this->rotate(90); + break; + case 8: + // Rotate 90 counterclockwise + $this->rotate(-90); + break; + } + + return $this; + + } + + /** + * Best fit (proportionally resize to fit in specified width/height) + * + * Shrink the image proportionally to fit inside a $width x $height box + * + * @param int $max_width + * @param int $max_height + * + * @return SimpleImage + * + */ + public function best_fit($max_width, $max_height) { + + // If it already fits, there's nothing to do + if ($this->width <= $max_width && $this->height <= $max_height) { + return $this; + } + + // Determine aspect ratio + $aspect_ratio = $this->height / $this->width; + + // Make width fit into new dimensions + if ($this->width > $max_width) { + $width = $max_width; + $height = $width * $aspect_ratio; + } else { + $width = $this->width; + $height = $this->height; + } + + // Make height fit into new dimensions + if ($height > $max_height) { + $height = $max_height; + $width = $height / $aspect_ratio; + } + + return $this->resize($width, $height); + + } + + /** + * Blur + * + * @param string $type selective|gaussian + * @param int $passes Number of times to apply the filter + * + * @return SimpleImage + * + */ + public function blur($type = 'selective', $passes = 1) { + switch (strtolower($type)) { + case 'gaussian': + $type = IMG_FILTER_GAUSSIAN_BLUR; + break; + default: + $type = IMG_FILTER_SELECTIVE_BLUR; + break; + } + for ($i = 0; $i < $passes; $i++) { + imagefilter($this->image, $type); + } + return $this; + } + + /** + * Brightness + * + * @param int $level Darkest = -255, lightest = 255 + * + * @return SimpleImage + * + */ + public function brightness($level) { + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $this->keep_within($level, -255, 255)); + return $this; + } + + /** + * Contrast + * + * @param int $level Min = -100, max = 100 + * + * @return SimpleImage + * + * + */ + public function contrast($level) { + imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keep_within($level, -100, 100)); + return $this; + } + + /** + * Colorize + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * @param float|int $opacity 0-1 + * + * @return SimpleImage + * + */ + public function colorize($color, $opacity) { + $rgba = $this->normalize_color($color); + $alpha = $this->keep_within(127 - (127 * $opacity), 0, 127); + imagefilter($this->image, IMG_FILTER_COLORIZE, $this->keep_within($rgba['r'], 0, 255), $this->keep_within($rgba['g'], 0, 255), $this->keep_within($rgba['b'], 0, 255), $alpha); + return $this; + } + + /** + * Create an image from scratch + * + * @param int $width Image width + * @param int|null $height If omitted - assumed equal to $width + * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function create($width, $height = null, $color = null) { + + $height = $height ?: $width; + $this->width = $width; + $this->height = $height; + $this->image = imagecreatetruecolor($width, $height); + $this->original_info = array( + 'width' => $width, + 'height' => $height, + 'orientation' => $this->get_orientation(), + 'exif' => null, + 'format' => 'png', + 'mime' => 'image/png' + ); + + if ($color !== null) { + $this->fill($color); + } + + return $this; + + } + + /** + * Crop an image + * + * @param int $x1 Left + * @param int $y1 Top + * @param int $x2 Right + * @param int $y2 Bottom + * + * @return SimpleImage + * + */ + public function crop($x1, $y1, $x2, $y2) { + + // Determine crop size + if ($x2 < $x1) { + list($x1, $x2) = array($x2, $x1); + } + if ($y2 < $y1) { + list($y1, $y2) = array($y2, $y1); + } + $crop_width = $x2 - $x1; + $crop_height = $y2 - $y1; + + // Perform crop + $new = imagecreatetruecolor($crop_width, $crop_height); + imagealphablending($new, false); + imagesavealpha($new, true); + imagecopyresampled($new, $this->image, 0, 0, $x1, $y1, $crop_width, $crop_height, $crop_width, $crop_height); + + // Update meta data + $this->width = $crop_width; + $this->height = $crop_height; + $this->image = $new; + + return $this; + + } + + /** + * Desaturate + * + * @param int $percentage Level of desaturization. + * + * @return SimpleImage + * + */ + public function desaturate($percentage = 100) { + + // Determine percentage + $percentage = $this->keep_within($percentage, 0, 100); + + if( $percentage === 100 ) { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + } else { + // Make a desaturated copy of the image + $new = imagecreatetruecolor($this->width, $this->height); + imagealphablending($new, false); + imagesavealpha($new, true); + imagecopy($new, $this->image, 0, 0, 0, 0, $this->width, $this->height); + imagefilter($new, IMG_FILTER_GRAYSCALE); + + // Merge with specified percentage + $this->imagecopymerge_alpha($this->image, $new, 0, 0, 0, 0, $this->width, $this->height, $percentage); + imagedestroy($new); + + } + + return $this; + } + + /** + * Edge Detect + * + * @return SimpleImage + * + */ + public function edges() { + imagefilter($this->image, IMG_FILTER_EDGEDETECT); + return $this; + } + + /** + * Emboss + * + * @return SimpleImage + * + */ + public function emboss() { + imagefilter($this->image, IMG_FILTER_EMBOSS); + return $this; + } + + /** + * Fill image with color + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function fill($color = '#000000') { + + $rgba = $this->normalize_color($color); + $fill_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + imagealphablending($this->image, false); + imagesavealpha($this->image, true); + imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $fill_color); + + return $this; + + } + + /** + * Fit to height (proportionally resize to specified height) + * + * @param int $height + * + * @return SimpleImage + * + */ + public function fit_to_height($height) { + + $aspect_ratio = $this->height / $this->width; + $width = $height / $aspect_ratio; + + return $this->resize($width, $height); + + } + + /** + * Fit to width (proportionally resize to specified width) + * + * @param int $width + * + * @return SimpleImage + * + */ + public function fit_to_width($width) { + + $aspect_ratio = $this->height / $this->width; + $height = $width * $aspect_ratio; + + return $this->resize($width, $height); + + } + + /** + * Flip an image horizontally or vertically + * + * @param string $direction x|y + * + * @return SimpleImage + * + */ + public function flip($direction) { + + $new = imagecreatetruecolor($this->width, $this->height); + imagealphablending($new, false); + imagesavealpha($new, true); + + switch (strtolower($direction)) { + case 'y': + for ($y = 0; $y < $this->height; $y++) { + imagecopy($new, $this->image, 0, $y, 0, $this->height - $y - 1, $this->width, 1); + } + break; + default: + for ($x = 0; $x < $this->width; $x++) { + imagecopy($new, $this->image, $x, 0, $this->width - $x - 1, 0, 1, $this->height); + } + break; + } + + $this->image = $new; + + return $this; + + } + + /** + * Get the current height + * + * @return int + * + */ + public function get_height() { + return $this->height; + } + + /** + * Get the current orientation + * + * @return string portrait|landscape|square + * + */ + public function get_orientation() { + + if (imagesx($this->image) > imagesy($this->image)) { + return 'landscape'; + } + + if (imagesx($this->image) < imagesy($this->image)) { + return 'portrait'; + } + + return 'square'; + + } + + /** + * Get info about the original image + * + * @return array
 array(
+     *  width        => 320,
+     *  height       => 200,
+     *  orientation  => ['portrait', 'landscape', 'square'],
+     *  exif         => array(...),
+     *  mime         => ['image/jpeg', 'image/gif', 'image/png'],
+     *  format       => ['jpeg', 'gif', 'png']
+     * )
+ * + */ + public function get_original_info() { + return $this->original_info; + } + + /** + * Get the current width + * + * @return int + * + */ + public function get_width() { + return $this->width; + } + + /** + * Invert + * + * @return SimpleImage + * + */ + public function invert() { + imagefilter($this->image, IMG_FILTER_NEGATE); + return $this; + } + + /** + * Load an image + * + * @param string $filename Path to image file + * + * @return SimpleImage + * @throws Exception + * + */ + public function load($filename) { + + // Require GD library + if (!extension_loaded('gd')) { + throw new Exception('Required extension GD is not loaded.'); + } + $this->filename = $filename; + return $this->get_meta_data(); + } + + /** + * Load a base64 string as image + * + * @param string $base64string base64 string + * + * @return SimpleImage + * + */ + public function load_base64($base64string) { + if (!extension_loaded('gd')) { + throw new Exception('Required extension GD is not loaded.'); + } + //remove data URI scheme and spaces from base64 string then decode it + $this->imagestring = base64_decode(str_replace(' ', '+',preg_replace('#^data:image/[^;]+;base64,#', '', $base64string))); + $this->image = imagecreatefromstring($this->imagestring); + return $this->get_meta_data(); + } + + /** + * Mean Remove + * + * @return SimpleImage + * + */ + public function mean_remove() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + return $this; + } + + /** + * Changes the opacity level of the image + * + * @param float|int $opacity 0-1 + * + * @throws Exception + * + */ + public function opacity($opacity) { + + // Determine opacity + $opacity = $this->keep_within($opacity, 0, 1) * 100; + + // Make a copy of the image + $copy = imagecreatetruecolor($this->width, $this->height); + imagealphablending($copy, false); + imagesavealpha($copy, true); + imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height); + + // Create transparent layer + $this->create($this->width, $this->height, array(0, 0, 0, 127)); + + // Merge with specified opacity + $this->imagecopymerge_alpha($this->image, $copy, 0, 0, 0, 0, $this->width, $this->height, $opacity); + imagedestroy($copy); + + return $this; + + } + + /** + * Outputs image without saving + * + * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png + * @param int|null $quality Output image quality in percents 0-100 + * + * @throws Exception + * + */ + public function output($format = null, $quality = null) { + + // Determine quality + $quality = $quality ?: $this->quality; + + // Determine mimetype + switch (strtolower($format)) { + case 'gif': + $mimetype = 'image/gif'; + break; + case 'jpeg': + case 'jpg': + imageinterlace($this->image, true); + $mimetype = 'image/jpeg'; + break; + case 'png': + $mimetype = 'image/png'; + break; + default: + $info = (empty($this->imagestring)) ? getimagesize($this->filename) : getimagesizefromstring($this->imagestring); + $mimetype = $info['mime']; + unset($info); + break; + } + + // Output the image + header('Content-Type: '.$mimetype); + switch ($mimetype) { + case 'image/gif': + imagegif($this->image); + break; + case 'image/jpeg': + imagejpeg($this->image, null, round($quality)); + break; + case 'image/png': + imagepng($this->image, null, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported image format: '.$this->filename); + } + } + + /** + * Outputs image as data base64 to use as img src + * + * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png + * @param int|null $quality Output image quality in percents 0-100 + * + * @return string + * @throws Exception + * + */ + public function output_base64($format = null, $quality = null) { + + // Determine quality + $quality = $quality ?: $this->quality; + + // Determine mimetype + switch (strtolower($format)) { + case 'gif': + $mimetype = 'image/gif'; + break; + case 'jpeg': + case 'jpg': + imageinterlace($this->image, true); + $mimetype = 'image/jpeg'; + break; + case 'png': + $mimetype = 'image/png'; + break; + default: + $info = getimagesize($this->filename); + $mimetype = $info['mime']; + unset($info); + break; + } + + // Output the image + ob_start(); + switch ($mimetype) { + case 'image/gif': + imagegif($this->image); + break; + case 'image/jpeg': + imagejpeg($this->image, null, round($quality)); + break; + case 'image/png': + imagepng($this->image, null, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported image format: '.$this->filename); + } + $image_data = ob_get_contents(); + ob_end_clean(); + + // Returns formatted string for img src + return 'data:'.$mimetype.';base64,'.base64_encode($image_data); + + } + + /** + * Overlay + * + * Overlay an image on top of another, works with 24-bit PNG alpha-transparency + * + * @param string $overlay An image filename or a SimpleImage object + * @param string $position center|top|left|bottom|right|top left|top right|bottom left|bottom right + * @param float|int $opacity Overlay opacity 0-1 + * @param int $x_offset Horizontal offset in pixels + * @param int $y_offset Vertical offset in pixels + * + * @return SimpleImage + * + */ + public function overlay($overlay, $position = 'center', $opacity = 1, $x_offset = 0, $y_offset = 0) { + + // Load overlay image + if( !($overlay instanceof SimpleImage) ) { + $overlay = new SimpleImage($overlay); + } + + // Convert opacity + $opacity = $opacity * 100; + + // Determine position + switch (strtolower($position)) { + case 'top left': + $x = 0 + $x_offset; + $y = 0 + $y_offset; + break; + case 'top right': + $x = $this->width - $overlay->width + $x_offset; + $y = 0 + $y_offset; + break; + case 'top': + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = 0 + $y_offset; + break; + case 'bottom left': + $x = 0 + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'bottom right': + $x = $this->width - $overlay->width + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'bottom': + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = $this->height - $overlay->height + $y_offset; + break; + case 'left': + $x = 0 + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + case 'right': + $x = $this->width - $overlay->width + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + case 'center': + default: + $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; + $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; + break; + } + + // Perform the overlay + $this->imagecopymerge_alpha($this->image, $overlay->image, $x, $y, 0, 0, $overlay->width, $overlay->height, $opacity); + + return $this; + + } + + /** + * Pixelate + * + * @param int $block_size Size in pixels of each resulting block + * + * @return SimpleImage + * + */ + public function pixelate($block_size = 10) { + imagefilter($this->image, IMG_FILTER_PIXELATE, $block_size, true); + return $this; + } + + /** + * Resize an image to the specified dimensions + * + * @param int $width + * @param int $height + * + * @return SimpleImage + * + */ + public function resize($width, $height) { + + // Generate new GD image + $new = imagecreatetruecolor($width, $height); + + if( $this->original_info['format'] === 'gif' ) { + // Preserve transparency in GIFs + $transparent_index = imagecolortransparent($this->image); + $palletsize = imagecolorstotal($this->image); + if ($transparent_index >= 0 && $transparent_index < $palletsize) { + $transparent_color = imagecolorsforindex($this->image, $transparent_index); + $transparent_index = imagecolorallocate($new, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); + imagefill($new, 0, 0, $transparent_index); + imagecolortransparent($new, $transparent_index); + } + } else { + // Preserve transparency in PNGs (benign for JPEGs) + imagealphablending($new, false); + imagesavealpha($new, true); + } + + // Resize + imagecopyresampled($new, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height); + + // Update meta data + $this->width = $width; + $this->height = $height; + $this->image = $new; + + return $this; + + } + + /** + * Rotate an image + * + * @param int $angle 0-360 + * @param string $bg_color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return SimpleImage + * + */ + public function rotate($angle, $bg_color = '#000000') { + + // Perform the rotation + $rgba = $this->normalize_color($bg_color); + $bg_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + $new = imagerotate($this->image, -($this->keep_within($angle, -360, 360)), $bg_color); + imagesavealpha($new, true); + imagealphablending($new, true); + + // Update meta data + $this->width = imagesx($new); + $this->height = imagesy($new); + $this->image = $new; + + return $this; + + } + + /** + * Save an image + * + * The resulting format will be determined by the file extension. + * + * @param null|string $filename If omitted - original file will be overwritten + * @param null|int $quality Output image quality in percents 0-100 + * @param null|string $format The format to use; determined by file extension if null + * + * @return SimpleImage + * @throws Exception + * + */ + public function save($filename = null, $quality = null, $format = null) { + + // Determine quality, filename, and format + $quality = $quality ?: $this->quality; + $filename = $filename ?: $this->filename; + if( $format === null ) { + $format = $this->file_ext($filename) ?: $this->original_info['format']; + } + + // Create the image + switch (strtolower($format)) { + case 'gif': + $result = imagegif($this->image, $filename); + break; + case 'jpg': + case 'jpeg': + imageinterlace($this->image, true); + $result = imagejpeg($this->image, $filename, round($quality)); + break; + case 'png': + $result = imagepng($this->image, $filename, round(9 * $quality / 100)); + break; + default: + throw new Exception('Unsupported format'); + } + + if (!$result) { + throw new Exception('Unable to save image: ' . $filename); + } + + return $this; + + } + + /** + * Sepia + * + * @return SimpleImage + * + */ + public function sepia() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 50, 0); + return $this; + } + + /** + * Sketch + * + * @return SimpleImage + * + */ + public function sketch() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + return $this; + } + + /** + * Smooth + * + * @param int $level Min = -10, max = 10 + * + * @return SimpleImage + * + */ + public function smooth($level) { + imagefilter($this->image, IMG_FILTER_SMOOTH, $this->keep_within($level, -10, 10)); + return $this; + } + + /** + * Add text to an image + * + * @param string $text + * @param string $font_file + * @param float|int $font_size + * @param string $color + * @param string $position + * @param int $x_offset + * @param int $y_offset + * + * @return SimpleImage + * @throws Exception + * + */ + public function text($text, $font_file, $font_size = 12, $color = '#000000', $position = 'center', $x_offset = 0, $y_offset = 0) { + + // todo - this method could be improved to support the text angle + $angle = 0; + + // Determine text color + $rgba = $this->normalize_color($color); + $color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); + + // Determine textbox size + $box = imagettfbbox($font_size, $angle, $font_file, $text); + if (empty($box)) { + throw new Exception('Unable to load font: '.$font_file); + } + $box_width = abs($box[6] - $box[2]); + $box_height = abs($box[7] - $box[1]); + + // Determine position + switch (strtolower($position)) { + case 'top left': + $x = 0 + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'top right': + $x = $this->width - $box_width + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'top': + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = 0 + $y_offset + $box_height; + break; + case 'bottom left': + $x = 0 + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'bottom right': + $x = $this->width - $box_width + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'bottom': + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = $this->height - $box_height + $y_offset + $box_height; + break; + case 'left': + $x = 0 + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + case 'right'; + $x = $this->width - $box_width + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + case 'center': + default: + $x = ($this->width / 2) - ($box_width / 2) + $x_offset; + $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; + break; + } + + // Add the text + imagesavealpha($this->image, true); + imagealphablending($this->image, true); + imagettftext($this->image, $font_size, $angle, $x, $y, $color, $font_file, $text); + + return $this; + + } + + /** + * Thumbnail + * + * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the + * remaining overflow (from the center) to get the image to be the size specified. Useful for generating thumbnails. + * + * @param int $width + * @param int|null $height If omitted - assumed equal to $width + * + * @return SimpleImage + * + */ + public function thumbnail($width, $height = null) { + + // Determine height + $height = $height ?: $width; + + // Determine aspect ratios + $current_aspect_ratio = $this->height / $this->width; + $new_aspect_ratio = $height / $width; + + // Fit to height/width + if ($new_aspect_ratio > $current_aspect_ratio) { + $this->fit_to_height($height); + } else { + $this->fit_to_width($width); + } + $left = floor(($this->width / 2) - ($width / 2)); + $top = floor(($this->height / 2) - ($height / 2)); + + // Return trimmed image + return $this->crop($left, $top, $width + $left, $height + $top); + + } + + /** + * Returns the file extension of the specified file + * + * @param string $filename + * + * @return string + * + */ + protected function file_ext($filename) { + + if (!preg_match('/\./', $filename)) { + return ''; + } + + return preg_replace('/^.*\./', '', $filename); + + } + + /** + * Get meta data of image or base64 string + * + * @return SimpleImage + * @throws Exception + * + */ + protected function get_meta_data() { + //gather meta data + if(empty($this->imagestring)) { + $info = getimagesize($this->filename); + + switch ($info['mime']) { + case 'image/gif': + $this->image = imagecreatefromgif($this->filename); + break; + case 'image/jpeg': + $this->image = imagecreatefromjpeg($this->filename); + break; + case 'image/png': + $this->image = imagecreatefrompng($this->filename); + break; + default: + throw new Exception('Invalid image: '.$this->filename); + } + } elseif (function_exists('getimagesizefromstring')) { + $info = getimagesizefromstring($this->imagestring); + } else { + throw new Exception('PHP 5.4 is required to use method getimagesizefromstring'); + } + + $this->original_info = array( + 'width' => $info[0], + 'height' => $info[1], + 'orientation' => $this->get_orientation(), + 'exif' => function_exists('exif_read_data') && $info['mime'] === 'image/jpeg' && $this->imagestring === null ? $this->exif = @exif_read_data($this->filename) : null, + 'format' => preg_replace('/^image\//', '', $info['mime']), + 'mime' => $info['mime'] + ); + $this->width = $info[0]; + $this->height = $info[1]; + + imagesavealpha($this->image, true); + imagealphablending($this->image, true); + + return $this; + + } + + /** + * Same as PHP's imagecopymerge() function, except preserves alpha-transparency in 24-bit PNGs + * + * @param $dst_im + * @param $src_im + * @param $dst_x + * @param $dst_y + * @param $src_x + * @param $src_y + * @param $src_w + * @param $src_h + * @param $pct + * + * @link http://www.php.net/manual/en/function.imagecopymerge.php#88456 + * + */ + protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { + + // Get image width and height and percentage + $pct /= 100; + $w = imagesx($src_im); + $h = imagesy($src_im); + + // Turn alpha blending off + imagealphablending($src_im, false); + + // Find the most opaque pixel in the image (the one with the smallest alpha value) + $minalpha = 127; + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF; + if ($alpha < $minalpha) { + $minalpha = $alpha; + } + } + } + + // Loop through image pixels and modify alpha for each + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + // Get current alpha value (represents the TANSPARENCY!) + $colorxy = imagecolorat($src_im, $x, $y); + $alpha = ($colorxy >> 24) & 0xFF; + // Calculate new alpha + if ($minalpha !== 127) { + $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha); + } else { + $alpha += 127 * $pct; + } + // Get the color index with new alpha + $alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha); + // Set pixel with the new color + opacity + if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) { + return; + } + } + } + + // Copy it + imagesavealpha($dst_im, true); + imagealphablending($dst_im, true); + imagesavealpha($src_im, true); + imagealphablending($src_im, true); + imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); + + } + + /** + * Ensures $value is always within $min and $max range. + * + * If lower, $min is returned. If higher, $max is returned. + * + * @param int|float $value + * @param int|float $min + * @param int|float $max + * + * @return int|float + * + */ + protected function keep_within($value, $min, $max) { + + if ($value < $min) { + return $min; + } + + if ($value > $max) { + return $max; + } + + return $value; + + } + + /** + * Converts a hex color value to its RGB equivalent + * + * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). + * Where red, green, blue - integers 0-255, alpha - integer 0-127 + * + * @return array|bool + * + */ + protected function normalize_color($color) { + + if (is_string($color)) { + + $color = trim($color, '#'); + + if (strlen($color) == 6) { + list($r, $g, $b) = array( + $color[0].$color[1], + $color[2].$color[3], + $color[4].$color[5] + ); + } elseif (strlen($color) == 3) { + list($r, $g, $b) = array( + $color[0].$color[0], + $color[1].$color[1], + $color[2].$color[2] + ); + } else { + return false; + } + return array( + 'r' => hexdec($r), + 'g' => hexdec($g), + 'b' => hexdec($b), + 'a' => 0 + ); + + } elseif (is_array($color) && (count($color) == 3 || count($color) == 4)) { + + if (isset($color['r'], $color['g'], $color['b'])) { + return array( + 'r' => $this->keep_within($color['r'], 0, 255), + 'g' => $this->keep_within($color['g'], 0, 255), + 'b' => $this->keep_within($color['b'], 0, 255), + 'a' => $this->keep_within(isset($color['a']) ? $color['a'] : 0, 0, 127) + ); + } elseif (isset($color[0], $color[1], $color[2])) { + return array( + 'r' => $this->keep_within($color[0], 0, 255), + 'g' => $this->keep_within($color[1], 0, 255), + 'b' => $this->keep_within($color[2], 0, 255), + 'a' => $this->keep_within(isset($color[3]) ? $color[3] : 0, 0, 127) + ); + } + + } + return false; + } + +} diff --git a/kirby/toolkit/vendors/mimereader/mimereader.php b/kirby/toolkit/vendors/mimereader/mimereader.php new file mode 100644 index 0000000..6752376 --- /dev/null +++ b/kirby/toolkit/vendors/mimereader/mimereader.php @@ -0,0 +1,776 @@ +file = $file; + + if ( empty( self::$binary_characters ) ) { + self::$binary_characters .= "\x00\x01\x02\x03\x04\x05\x06\x07\0x08\x0B\x0E\x0F\x10\x11"; + self::$binary_characters .= "\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1C\x1D\x1E\x1F"; + } + if ( empty( self::$whitespace_characters ) ) { + self::$whitespace_characters .= "\x09\x0A\x0C\x0D\x20"; + } + if ( empty( self::$tag_terminating_characters ) ) { + self::$tag_terminating_characters .= "\x20\x3E"; + } + + if ( is_null( self::$image ) ) { + $image = &self::$image; + $image = array(); + + // Windows Icon + $image[] = array ( + 'mime' => 'image/vnd.microsoft.icon', + 'pattern' => "\x00\x00\x01\x00", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '', // none + ); + // "BM" - BMP signature + $image[] = array ( + 'mime' => 'image/bmp', + 'pattern' => "\x42\x4D", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // "GIF87a" - GIF signature + $image[] = array ( + 'mime' => 'image/gif', + 'pattern' => "\x47\x49\x46\x38\x37\x61", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "GIF89a" - GIF signature + $image[] = array ( + 'mime' => 'image/gif', + 'pattern' => "\x47\x49\x46\x38\x39\x61", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "WEBPVP" + $image[] = array ( + 'mime' => 'image/webp', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x57\x45\x42\x50\x56\x50", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // A byte with only the highest bit set followed by the string "PNG" followed by CR LF SUB LF - PNG signature + $image[] = array ( + 'mime' => 'image/png', + 'pattern' => "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // JPEG start of image marker followed by another marker + $image[] = array ( + 'mime' => 'image/jpeg', + 'pattern' => "\xFF\xD8\xFF", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // PSD signature + $image[] = array ( + 'mime' => 'application/psd', + 'pattern' => "\x38\x42\x50\x53", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$media ) ) { + $media = &self::$media; + $media = array(); + + // The WebM signature + $media[] = array ( + 'mime' => 'video/webm', + 'pattern' => "\x1A\x45\xDF\xA3", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // The .snd signature + $media[] = array ( + 'mime' => 'audio/basic', + 'pattern' => "\x2E\x73\x6E\x64", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "FORM" followed by 4 bytes followed by "AIFF" - the AIFF signature + $media[] = array ( + 'mime' => 'audio/aiff', + 'pattern' => "\x46\x4F\x52\x4D\x00\x00\x00\x00\x41\x49\x46\x46", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // MP3 without ID3 tag /****** UNTESTED ******/ + $media[] = array ( + 'mime' => 'audio/mpeg', + 'pattern' => "\xFF\xFB", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // "ID3" and the ID3v2-tagged MP3 signature + $media[] = array ( + 'mime' => 'audio/mpeg', + 'pattern' => "\x49\x44\x33", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // "OggS" followed by NUL - The OGG signature + $media[] = array ( + 'mime' => 'application/ogg', + 'pattern' => "\x4F\x67\x67\x53\x00", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "MThd" followed by 4 bytes representing the number 6 in 32 bits (big endian) - MIDI signature + $media[] = array ( + 'mime' => 'audio/midi', + 'pattern' => "\x4D\x54\x68\x64\x00\x00\x00\x06", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "AVI" - AVI signature + $media[] = array ( + 'mime' => 'video/avi', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x41\x56\x49\x20", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "RIFF" followed by 4 bytes followed by "WAVE" - WAVE signature + $media[] = array ( + 'mime' => 'audio/wave', + 'pattern' => "\x52\x49\x46\x46\x00\x00\x00\x00\x57\x41\x56\x45", + 'mask' => "\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$fonts ) ) { + $fonts = &self::$fonts; + $fonts = array(); + + // 34 bytes followed by "LP" - Opentype signature + $fonts[] = array ( + 'mime' => 'application/vnd.ms-fontobject', + 'pattern' => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" . + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4C\x50", + 'mask' => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" . + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF", + 'ignore' => '' + ); + // 4 bytes representing version type 1 of true type font + $fonts[] = array ( + 'mime' => 'application/font-ttf', + 'pattern' => "\x00\x01\x00\x00", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "OTTO" - Opentype signature + $fonts[] = array ( + 'mime' => 'application/font-off', // application/vnd.ms-opentype + 'pattern' => "\x4F\x54\x54\x4F", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "ttcf" - Truetype Collection signature + $fonts[] = array ( + 'mime' => 'application/x-font-truetype-collection', + 'pattern' => "\x74\x74\x63\x66", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // 'wOFF' - Web Open Font Format signature + $fonts[] = array ( + 'mime' => 'application/font-woff', + 'pattern' => "\x77\x4F\x46\x46", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$archive ) ) { + $archive = &self::$archive; + $archive = array(); + + // GZIP signature + $archive[] = array ( + 'mime' => 'application/x-gzip', + 'pattern' => "\x1F\x8B\x08", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + // "PK" followed by ETX, EOT - ZIP signature + $archive[] = array ( + 'mime' => 'application/zip', + 'pattern' => "\x50\x4B\x03\x04", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // "Rar " followed by SUB, BEL, NUL - RAR signature + $archive[] = array ( + 'mime' => 'application/x-rar-compressed', + 'pattern' => "\x52\x61\x72\x20\x1A\x07\x00", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$text ) ) { + $text = &self::$text; + $text = array(); + + // "%!PS-Adobe-" - Postscript signature + $text[] = array ( + 'mime' => 'application/postscript', + 'pattern' => "\x25\x50\x53\x2D\x41\x64\x6F\x62\x65", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + // UTF-16 Big Endian BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xFF\xFE", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // UTF-16 Little Endian BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xFE\xFF", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + // UTF-8 BOM text + $text[] = array ( + 'mime' => 'text/plain', + 'pattern' => "\xEF\xBB\xBF", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => '' + ); + } + if ( is_null( self::$others ) ) { + $others = &self::$others; + $others = array(); + + $others[] = array ( + 'mime' => 'WINDOWS EXECUTABLE', + 'pattern' => "\x4D\x5A", + 'mask' => "\xFF\xFF", + 'ignore' => '' + ); + $others[] = array ( + 'mime' => 'EXEC_LINKABLE', + 'pattern' => "\x7F\x45\x4C\x46", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => '' + ); + + } + if ( is_null( self::$unknown ) ) { + $unknown = &self::$unknown; + $unknown = array(); + + // " 'text/html', + 'pattern' => "\x3C\x21\x44\x4F\x43\x54\x59\x50\x45\x20\x48\x54\x4D\x4C", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x54\x4D\x4C", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x45\x41\x44", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x53\x43\x52\x49\x50\x54", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x49\x46\x52\x41\x4D\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x48\x31", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x44\x49\x56", + 'mask' => "\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x46\x4F\x4E\x54", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x54\x41\x42\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x41", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x53\x54\x59\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x54\x49\x54\x4C\x45", + 'mask' => "\xFF\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42\x4F\x44\x59", + 'mask' => "\xFF\xFF\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x42\x52", + 'mask' => "\xFF\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // " 'text/html', + 'pattern' => "\x3C\x50", + 'mask' => "\xFF\xFF", + 'ignore' => self::$whitespace_characters + ); + // "$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + $class = 'language-'.$matches[1]; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body'];; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped) + { + return; + } + + if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $matches[0][0], + 'attributes' => array( + 'href' => $matches[0][0], + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($Element['text'])) + { + $markup .= '>'; + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($Element['text']); + } + else + { + $markup .= $Element['text']; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + # + # Static Methods + # + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'sub', 'mark', + 'u', 'xm', 'sup', 'nobr', + 'var', 'ruby', + 'wbr', 'span', + 'time', + ); +} diff --git a/kirby/vendors/parsedownextra.php b/kirby/vendors/parsedownextra.php new file mode 100644 index 0000000..be6966d --- /dev/null +++ b/kirby/vendors/parsedownextra.php @@ -0,0 +1,526 @@ +BlockTypes[':'] []= 'DefinitionList'; + $this->BlockTypes['*'] []= 'Abbreviation'; + + # identify footnote definitions before reference definitions + array_unshift($this->BlockTypes['['], 'Footnote'); + + # identify footnote markers before before links + array_unshift($this->InlineTypes['['], 'FootnoteMarker'); + } + + # + # ~ + + function text($text) + { + $markup = parent::text($text); + + # merge consecutive dl elements + + $markup = preg_replace('/<\/dl>\s+
\s+/', '', $markup); + + # add footnotes + + if (isset($this->DefinitionData['Footnote'])) + { + $Element = $this->buildFootnoteElement(); + + $markup .= "\n" . $this->element($Element); + } + + return $markup; + } + + # + # Blocks + # + + # + # Abbreviation + + protected function blockAbbreviation($Line) + { + if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) + { + $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Footnote + + protected function blockFootnote($Line) + { + if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) + { + $Block = array( + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ); + + return $Block; + } + } + + protected function blockFootnoteContinue($Line, $Block) + { + if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + if ($Line['indent'] >= 4) + { + $Block['text'] .= "\n\n" . $Line['text']; + + return $Block; + } + } + else + { + $Block['text'] .= "\n" . $Line['text']; + + return $Block; + } + } + + protected function blockFootnoteComplete($Block) + { + $this->DefinitionData['Footnote'][$Block['label']] = array( + 'text' => $Block['text'], + 'count' => null, + 'number' => null, + ); + + return $Block; + } + + # + # Definition List + + protected function blockDefinitionList($Line, $Block) + { + if ( ! isset($Block) or isset($Block['type'])) + { + return; + } + + $Element = array( + 'name' => 'dl', + 'handler' => 'elements', + 'text' => array(), + ); + + $terms = explode("\n", $Block['element']['text']); + + foreach ($terms as $term) + { + $Element['text'] []= array( + 'name' => 'dt', + 'handler' => 'line', + 'text' => $term, + ); + } + + $Block['element'] = $Element; + + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + + protected function blockDefinitionListContinue($Line, array $Block) + { + if ($Line['text'][0] === ':') + { + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + else + { + if (isset($Block['interrupted']) and $Line['indent'] === 0) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler'] = 'text'; + $Block['dd']['text'] .= "\n\n"; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['text'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkupComplete($Block) + { + if ( ! isset($Block['void'])) + { + $Block['markup'] = $this->processTag($Block['markup']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) + { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Inline Elements + # + + # + # Footnote Marker + + protected function inlineFootnoteMarker($Excerpt) + { + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) + { + $name = $matches[1]; + + if ( ! isset($this->DefinitionData['Footnote'][$name])) + { + return; + } + + $this->DefinitionData['Footnote'][$name]['count'] ++; + + if ( ! isset($this->DefinitionData['Footnote'][$name]['number'])) + { + $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & + } + + $Element = array( + 'name' => 'sup', + 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), + 'handler' => 'element', + 'text' => array( + 'name' => 'a', + 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), + 'text' => $this->DefinitionData['Footnote'][$name]['number'], + ), + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => $Element, + ); + } + } + + private $footnoteCount = 0; + + # + # Link + + protected function inlineLink($Excerpt) + { + $Link = parent::inlineLink($Excerpt); + + $remainder = substr($Excerpt['text'], $Link['extent']); + + if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) + { + $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); + + $Link['extent'] += strlen($matches[0]); + } + + return $Link; + } + + # + # ~ + # + + protected function unmarkedText($text) + { + $text = parent::unmarkedText($text); + + if (isset($this->DefinitionData['Abbreviation'])) + { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) + { + $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/'; + + $text = preg_replace($pattern, ''.$abbreviation.'', $text); + } + } + + return $text; + } + + # + # Util Methods + # + + protected function addDdElement(array $Line, array $Block) + { + $text = substr($Line['text'], 1); + $text = trim($text); + + unset($Block['dd']); + + $Block['dd'] = array( + 'name' => 'dd', + 'handler' => 'line', + 'text' => $text, + ); + + if (isset($Block['interrupted'])) + { + $Block['dd']['handler'] = 'text'; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'handler' => 'elements', + 'text' => array( + array( + 'name' => 'hr', + ), + array( + 'name' => 'ol', + 'handler' => 'elements', + 'text' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) + { + if ( ! isset($DefinitionData['number'])) + { + continue; + } + + $text = $DefinitionData['text']; + + $text = parent::text($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinksMarkup = ''; + + foreach ($numbers as $number) + { + $backLinksMarkup .= ' '; + } + + $backLinksMarkup = substr($backLinksMarkup, 1); + + if (substr($text, - 4) === '

') + { + $backLinksMarkup = ' '.$backLinksMarkup; + + $text = substr_replace($text, $backLinksMarkup.'

', - 4); + } + else + { + $text .= "\n".'

'.$backLinksMarkup.'

'; + } + + $Element['text'][1]['text'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'text' => "\n".$text."\n", + ); + } + + return $Element; + } + + # ~ + + protected function parseAttributeData($attributeString) + { + $Data = array(); + + $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); + + foreach ($attributes as $attribute) + { + if ($attribute[0] === '#') + { + $Data['id'] = substr($attribute, 1); + } + else # "." + { + $classes []= substr($attribute, 1); + } + } + + if (isset($classes)) + { + $Data['class'] = implode(' ', $classes); + } + + return $Data; + } + + # ~ + + protected function processTag($elementMarkup) # recursive + { + # http://stackoverflow.com/q/1148928/200145 + libxml_use_internal_errors(true); + + $DOMDocument = new DOMDocument; + + # http://stackoverflow.com/q/11309194/200145 + $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); + + # http://stackoverflow.com/q/4879946/200145 + $DOMDocument->loadHTML($elementMarkup); + $DOMDocument->removeChild($DOMDocument->doctype); + $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + + $elementText = ''; + + if ($DOMDocument->documentElement->getAttribute('markdown') === '1') + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $elementText .= $DOMDocument->saveHTML($Node); + } + + $DOMDocument->documentElement->removeAttribute('markdown'); + + $elementText = "\n".$this->text($elementText)."\n"; + } + else + { + foreach ($DOMDocument->documentElement->childNodes as $Node) + { + $nodeMarkup = $DOMDocument->saveHTML($Node); + + if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) + { + $elementText .= $this->processTag($nodeMarkup); + } + else + { + $elementText .= $nodeMarkup; + } + } + } + + # because we don't want for markup to get encoded + $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + + $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = str_replace('placeholder\x1A', $elementText, $markup); + + return $markup; + } + + # ~ + + protected function sortFootnotes($A, $B) # callback + { + return $A['number'] - $B['number']; + } + + # + # Fields + # + + protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; +} diff --git a/kirby/vendors/smartypants.php b/kirby/vendors/smartypants.php new file mode 100755 index 0000000..1f73456 --- /dev/null +++ b/kirby/vendors/smartypants.php @@ -0,0 +1,1094 @@ + +# +# Original SmartyPants +# Copyright (c) 2003-2004 John Gruber +# +# + + +define( 'SMARTYPANTS_VERSION', "1.5.1f" ); # Sun 23 Jan 2013 +define( 'SMARTYPANTSTYPOGRAPHER_VERSION', "1.0.1" ); # Sun 23 Jan 2013 + +# +# Default configuration: +# +# 1 -> "--" for em-dashes; no en-dash support +# 2 -> "---" for em-dashes; "--" for en-dashes +# 3 -> "--" for em-dashes; "---" for en-dashes +# See docs for more configuration options. +# +define( 'SMARTYPANTS_ATTR', c::get('smartypants.attr', 1) ); + +# Openning and closing smart double-quotes. +define( 'SMARTYPANTS_SMART_DOUBLEQUOTE_OPEN', c::get('smartypants.doublequote.open', '“') ); +define( 'SMARTYPANTS_SMART_DOUBLEQUOTE_CLOSE', c::get('smartypants.doublequote.close', '”') ); + +# Space around em-dashes. "He_—_or she_—_should change that." +define( 'SMARTYPANTS_SPACE_EMDASH', c::get('smartypants.space.emdash', ' ') ); + +# Space around en-dashes. "He_–_or she_–_should change that." +define( 'SMARTYPANTS_SPACE_ENDASH', c::get('smartypants.space.endash', ' ') ); + +# Space before a colon. "He said_: here it is." +define( 'SMARTYPANTS_SPACE_COLON', c::get('smartypants.space.colon', ' ') ); + +# Space before a semicolon. "That's what I said_; that's what he said." +define( 'SMARTYPANTS_SPACE_SEMICOLON', c::get('smartypants.space.semicolon', ' ') ); + +# Space before a question mark and an exclamation mark: "¡_Holà_! What_?" +define( 'SMARTYPANTS_SPACE_MARKS', c::get('smartypants.space.marks', ' ') ); + +# Space inside french quotes. "Voici la «_chose_» qui m'a attaqué." +define( 'SMARTYPANTS_SPACE_FRENCHQUOTE', c::get('smartypants.space.frenchquote', ' ') ); + +# Space as thousand separator. "On compte 10_000 maisons sur cette liste." +define( 'SMARTYPANTS_SPACE_THOUSAND', c::get('smartypants.space.thousand', ' ') ); + +# Space before a unit abreviation. "This 12_kg of matter costs 10_$." +define( 'SMARTYPANTS_SPACE_UNIT', c::get('smartypants.space.unit', ' ') ); + +# SmartyPants will not alter the content of these tags: +define( 'SMARTYPANTS_TAGS_TO_SKIP', c::get('smartypants.skip', 'pre|code|kbd|script|style|math') ); + + +# +# SmartyPants Parser Class +# + +class SmartyPants_Parser { + + # Options to specify which transformations to make: + var $do_nothing = 0; + var $do_quotes = 0; + var $do_backticks = 0; + var $do_dashes = 0; + var $do_ellipses = 0; + var $do_stupefy = 0; + var $convert_quot = 0; # should we translate " entities into normal quotes? + + function __construct($attr = SMARTYPANTS_ATTR) { + # + # Initialize a SmartyPants_Parser with certain attributes. + # + # Parser attributes: + # 0 : do nothing + # 1 : set all + # 2 : set all, using old school en- and em- dash shortcuts + # 3 : set all, using inverted old school en and em- dash shortcuts + # + # q : quotes + # b : backtick quotes (``double'' only) + # B : backtick quotes (``double'' and `single') + # d : dashes + # D : old school dashes + # i : inverted old school dashes + # e : ellipses + # w : convert " entities to " for Dreamweaver users + # + if ($attr == "0") { + $this->do_nothing = 1; + } + else if ($attr == "1") { + # Do everything, turn all options on. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 1; + $this->do_ellipses = 1; + } + else if ($attr == "2") { + # Do everything, turn all options on, use old school dash shorthand. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 2; + $this->do_ellipses = 1; + } + else if ($attr == "3") { + # Do everything, turn all options on, use inverted old school dash shorthand. + $this->do_quotes = 1; + $this->do_backticks = 1; + $this->do_dashes = 3; + $this->do_ellipses = 1; + } + else if ($attr == "-1") { + # Special "stupefy" mode. + $this->do_stupefy = 1; + } + else { + $chars = preg_split('//', $attr); + foreach ($chars as $c){ + if ($c == "q") { $this->do_quotes = 1; } + else if ($c == "b") { $this->do_backticks = 1; } + else if ($c == "B") { $this->do_backticks = 2; } + else if ($c == "d") { $this->do_dashes = 1; } + else if ($c == "D") { $this->do_dashes = 2; } + else if ($c == "i") { $this->do_dashes = 3; } + else if ($c == "e") { $this->do_ellipses = 1; } + else if ($c == "w") { $this->convert_quot = 1; } + else { + # Unknown attribute option, ignore. + } + } + } + } + + function transform($text) { + + if ($this->do_nothing) { + return $text; + } + + $tokens = $this->tokenizeHTML($text); + $result = ''; + $in_pre = 0; # Keep track of when we're inside
 or  tags.
+
+		$prev_token_last_char = ""; # This is a cheat, used to get some context
+									# for one-character tokens that consist of 
+									# just a quote char. What we do is remember
+									# the last character of the previous text
+									# token, to use as context to curl single-
+									# character quote tokens correctly.
+
+		foreach ($tokens as $cur_token) {
+			if ($cur_token[0] == "tag") {
+				# Don't mess with quotes inside tags.
+				$result .= $cur_token[1];
+				if (preg_match('@<(/?)(?:'.SMARTYPANTS_TAGS_TO_SKIP.')[\s>]@', $cur_token[1], $matches)) {
+					$in_pre = isset($matches[1]) && $matches[1] == '/' ? 0 : 1;
+				}
+			} else {
+				$t = $cur_token[1];
+				$last_char = substr($t, -1); # Remember last char of this token before processing.
+				if (! $in_pre) {
+					$t = $this->educate($t, $prev_token_last_char);
+				}
+				$prev_token_last_char = $last_char;
+				$result .= $t;
+			}
+		}
+
+		return $result;
+	}
+
+
+	function educate($t, $prev_token_last_char) {
+		$t = $this->processEscapes($t);
+
+		if ($this->convert_quot) {
+			$t = preg_replace('/"/', '"', $t);
+		}
+
+		if ($this->do_dashes) {
+			if ($this->do_dashes == 1) $t = $this->educateDashes($t);
+			if ($this->do_dashes == 2) $t = $this->educateDashesOldSchool($t);
+			if ($this->do_dashes == 3) $t = $this->educateDashesOldSchoolInverted($t);
+		}
+
+		if ($this->do_ellipses) $t = $this->educateEllipses($t);
+
+		# Note: backticks need to be processed before quotes.
+		if ($this->do_backticks) {
+			$t = $this->educateBackticks($t);
+			if ($this->do_backticks == 2) $t = $this->educateSingleBackticks($t);
+		}
+
+		if ($this->do_quotes) {
+			if ($t == "'") {
+				# Special case: single-character ' token
+				if (preg_match('/\S/', $prev_token_last_char)) {
+					$t = "’";
+				}
+				else {
+					$t = "‘";
+				}
+			}
+			else if ($t == '"') {
+				# Special case: single-character " token
+				if (preg_match('/\S/', $prev_token_last_char)) {
+					$t = "”";
+				}
+				else {
+					$t = "“";
+				}
+			}
+			else {
+				# Normal case:
+				$t = $this->educateQuotes($t);
+			}
+		}
+
+		if ($this->do_stupefy) $t = $this->stupefyEntities($t);
+		
+		return $t;
+	}
+
+
+	function educateQuotes($_) {
+	#
+	#   Parameter:  String.
+	#
+	#   Returns:    The string, with "educated" curly quote HTML entities.
+	#
+	#   Example input:  "Isn't this fun?"
+	#   Example output: “Isn’t this fun?”
+	#
+		# Make our own "punctuation" character class, because the POSIX-style
+		# [:PUNCT:] is only available in Perl 5.6 or later:
+		$punct_class = "[!\"#\\$\\%'()*+,-.\\/:;<=>?\\@\\[\\\\\]\\^_`{|}~]";
+
+		# Special case if the very first character is a quote
+		# followed by punctuation at a non-word-break. Close the quotes by brute force:
+		$_ = preg_replace(
+			array("/^'(?=$punct_class\\B)/", "/^\"(?=$punct_class\\B)/"),
+			array('’',                 '”'), $_);
+
+
+		# Special case for double sets of quotes, e.g.:
+		#   

He said, "'Quoted' words in a larger quote."

+ $_ = preg_replace( + array("/\"'(?=\w)/", "/'\"(?=\w)/"), + array('“‘', '‘“'), $_); + + # Special case for decade abbreviations (the '80s): + $_ = preg_replace("/'(?=\\d{2}s)/", '’', $_); + + $close_class = '[^\ \t\r\n\[\{\(\-]'; + $dec_dashes = '&\#8211;|&\#8212;'; + + # Get most opening single quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + ' # the quote + (?=\\w) # followed by a word character + }x", '\1‘', $_); + # Single closing quotes: + $_ = preg_replace("{ + ($close_class)? + ' + (?(1)| # If $1 captured, then do nothing; + (?=\\s | s\\b) # otherwise, positive lookahead for a whitespace + ) # char or an 's' at a word ending position. This + # is a special case to handle something like: + # \"Custer's Last Stand.\" + }xi", '\1’', $_); + + # Any remaining single quotes should be opening ones: + $_ = str_replace("'", '‘', $_); + + + # Get most opening double quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + \" # the quote + (?=\\w) # followed by a word character + }x", '\1“', $_); + + # Double closing quotes: + $_ = preg_replace("{ + ($close_class)? + \" + (?(1)|(?=\\s)) # If $1 captured, then do nothing; + # if not, then make sure the next char is whitespace. + }x", '\1”', $_); + + # Any remaining quotes should be opening ones. + $_ = str_replace('"', '“', $_); + + return $_; + } + + + function educateBackticks($_) { + # + # Parameter: String. + # Returns: The string, with ``backticks'' -style double quotes + # translated into HTML curly quote entities. + # + # Example input: ``Isn't this fun?'' + # Example output: “Isn't this fun?” + # + + $_ = str_replace(array("``", "''",), + array('“', '”'), $_); + return $_; + } + + + function educateSingleBackticks($_) { + # + # Parameter: String. + # Returns: The string, with `backticks' -style single quotes + # translated into HTML curly quote entities. + # + # Example input: `Isn't this fun?' + # Example output: ‘Isn’t this fun?’ + # + + $_ = str_replace(array("`", "'",), + array('‘', '’'), $_); + return $_; + } + + + function educateDashes($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an em-dash HTML entity. + # + + $_ = str_replace('--', '—', $_); + return $_; + } + + + function educateDashesOldSchool($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an en-dash HTML entity, and each "---" translated to + # an em-dash HTML entity. + # + + # em en + $_ = str_replace(array("---", "--",), + array('—', '–'), $_); + return $_; + } + + + function educateDashesOldSchoolInverted($_) { + # + # Parameter: String. + # + # Returns: The string, with each instance of "--" translated to + # an em-dash HTML entity, and each "---" translated to + # an en-dash HTML entity. Two reasons why: First, unlike the + # en- and em-dash syntax supported by + # EducateDashesOldSchool(), it's compatible with existing + # entries written before SmartyPants 1.1, back when "--" was + # only used for em-dashes. Second, em-dashes are more + # common than en-dashes, and so it sort of makes sense that + # the shortcut should be shorter to type. (Thanks to Aaron + # Swartz for the idea.) + # + + # en em + $_ = str_replace(array("---", "--",), + array('–', '—'), $_); + return $_; + } + + + function educateEllipses($_) { + # + # Parameter: String. + # Returns: The string, with each instance of "..." translated to + # an ellipsis HTML entity. Also converts the case where + # there are spaces between the dots. + # + # Example input: Huh...? + # Example output: Huh…? + # + + $_ = str_replace(array("...", ". . .",), '…', $_); + return $_; + } + + + function stupefyEntities($_) { + # + # Parameter: String. + # Returns: The string, with each SmartyPants HTML entity translated to + # its ASCII counterpart. + # + # Example input: “Hello — world.” + # Example output: "Hello -- world." + # + + # en-dash em-dash + $_ = str_replace(array('–', '—'), + array('-', '--'), $_); + + # single quote open close + $_ = str_replace(array('‘', '’'), "'", $_); + + # double quote open close + $_ = str_replace(array('“', '”'), '"', $_); + + $_ = str_replace('…', '...', $_); # ellipsis + + return $_; + } + + + function processEscapes($_) { + # + # Parameter: String. + # Returns: The string, with after processing the following backslash + # escape sequences. This is useful if you want to force a "dumb" + # quote or other character to appear. + # + # Escape Value + # ------ ----- + # \\ \ + # \" " + # \' ' + # \. . + # \- - + # \` ` + # + $_ = str_replace( + array('\\\\', '\"', "\'", '\.', '\-', '\`'), + array('\', '"', ''', '.', '-', '`'), $_); + + return $_; + } + + + function tokenizeHTML($str) { + # + # Parameter: String containing HTML markup. + # Returns: An array of the tokens comprising the input + # string. Each token is either a tag (possibly with nested, + # tags contained therein, such as , or a + # run of text between tags. Each element of the array is a + # two-element array; the first is either 'tag' or 'text'; + # the second is the actual value. + # + # + # Regular expression derived from the _tokenize() subroutine in + # Brad Choate's MTRegex plugin. + # + # + $index = 0; + $tokens = array(); + + $match = '(?s:)|'. # comment + '(?s:<\?.*?\?>)|'. # processing instruction + # regular tags + '(?:<[/!$]?[-a-zA-Z0-9:]+\b(?>[^"\'>]+|"[^"]*"|\'[^\']*\')*>)'; + + $parts = preg_split("{($match)}", $str, -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach ($parts as $part) { + if (++$index % 2 && $part != '') + $tokens[] = array('text', $part); + else + $tokens[] = array('tag', $part); + } + return $tokens; + } + +} + + +# +# SmartyPants Typographer Parser Class +# +class SmartyPantsTypographer_Parser extends SmartyPants_Parser { + + # Options to specify which transformations to make: + var $do_comma_quotes = 0; + var $do_guillemets = 0; + var $do_space_emdash = 0; + var $do_space_endash = 0; + var $do_space_colon = 0; + var $do_space_semicolon = 0; + var $do_space_marks = 0; + var $do_space_frenchquote = 0; + var $do_space_thousand = 0; + var $do_space_unit = 0; + + # Smart quote characters: + var $smart_doublequote_open = SMARTYPANTS_SMART_DOUBLEQUOTE_OPEN; + var $smart_doublequote_close = SMARTYPANTS_SMART_DOUBLEQUOTE_CLOSE; + var $smart_singlequote_open = '‘'; + var $smart_singlequote_close = '’'; # Also apostrophe. + + # Space characters for different places: + var $space_emdash = SMARTYPANTS_SPACE_EMDASH; + var $space_endash = SMARTYPANTS_SPACE_ENDASH; + var $space_colon = SMARTYPANTS_SPACE_COLON; + var $space_semicolon = SMARTYPANTS_SPACE_SEMICOLON; + var $space_marks = SMARTYPANTS_SPACE_MARKS; + var $space_frenchquote = SMARTYPANTS_SPACE_FRENCHQUOTE; + var $space_thousand = SMARTYPANTS_SPACE_THOUSAND; + var $space_unit = SMARTYPANTS_SPACE_UNIT; + + # Expression of a space (breakable or not): + var $space = '(?: | | |�*160;|�*[aA]0;)'; + + + + function __construct($attr = SMARTYPANTS_ATTR) { + # + # Initialize a SmartyPantsTypographer_Parser with certain attributes. + # + # Parser attributes: + # 0 : do nothing + # 1 : set all, except dash spacing + # 2 : set all, except dash spacing, using old school en- and em- dash shortcuts + # 3 : set all, except dash spacing, using inverted old school en and em- dash shortcuts + # + # Punctuation: + # q -> quotes + # b -> backtick quotes (``double'' only) + # B -> backtick quotes (``double'' and `single') + # c -> comma quotes (,,double`` only) + # g -> guillemets (<> only) + # d -> dashes + # D -> old school dashes + # i -> inverted old school dashes + # e -> ellipses + # w -> convert " entities to " for Dreamweaver users + # + # Spacing: + # : -> colon spacing +- + # ; -> semicolon spacing +- + # m -> question and exclamation marks spacing +- + # h -> em-dash spacing +- + # H -> en-dash spacing +- + # f -> french quote spacing +- + # t -> thousand separator spacing - + # u -> unit spacing +- + # (you can add a plus sign after some of these options denoted by + to + # add the space when it is not already present, or you can add a minus + # sign to completly remove any space present) + # + # Initialize inherited SmartyPants parser. + + parent::__construct($attr); + if ($attr == "1" || $attr == "2" || $attr == "3") { + # Do everything, turn all options on. + $this->do_comma_quotes = 1; + $this->do_guillemets = 1; + $this->do_space_emdash = 1; + $this->do_space_endash = 1; + $this->do_space_colon = 1; + $this->do_space_semicolon = 1; + $this->do_space_marks = 1; + $this->do_space_frenchquote = 1; + $this->do_space_thousand = 1; + $this->do_space_unit = 1; + } + else if ($attr == "-1") { + # Special "stupefy" mode. + $this->do_stupefy = 1; + } + else { + $chars = preg_split('//', $attr); + foreach ($chars as $c){ + if ($c == "c") { $current =& $this->do_comma_quotes; } + else if ($c == "g") { $current =& $this->do_guillemets; } + else if ($c == ":") { $current =& $this->do_space_colon; } + else if ($c == ";") { $current =& $this->do_space_semicolon; } + else if ($c == "m") { $current =& $this->do_space_marks; } + else if ($c == "h") { $current =& $this->do_space_emdash; } + else if ($c == "H") { $current =& $this->do_space_endash; } + else if ($c == "f") { $current =& $this->do_space_frenchquote; } + else if ($c == "t") { $current =& $this->do_space_thousand; } + else if ($c == "u") { $current =& $this->do_space_unit; } + else if ($c == "+") { + $current = 2; + unset($current); + } + else if ($c == "-") { + $current = -1; + unset($current); + } + else { + # Unknown attribute option, ignore. + } + $current = 1; + } + } + } + + + function educate($t, $prev_token_last_char) { + $t = parent::educate($t, $prev_token_last_char); + + if ($this->do_comma_quotes) $t = $this->educateCommaQuotes($t); + if ($this->do_guillemets) $t = $this->educateGuillemets($t); + + if ($this->do_space_emdash) $t = $this->spaceEmDash($t); + if ($this->do_space_endash) $t = $this->spaceEnDash($t); + if ($this->do_space_colon) $t = $this->spaceColon($t); + if ($this->do_space_semicolon) $t = $this->spaceSemicolon($t); + if ($this->do_space_marks) $t = $this->spaceMarks($t); + if ($this->do_space_frenchquote) $t = $this->spaceFrenchQuotes($t); + if ($this->do_space_thousand) $t = $this->spaceThousandSeparator($t); + if ($this->do_space_unit) $t = $this->spaceUnit($t); + + return $t; + } + + + function educateQuotes($_) { + # + # Parameter: String. + # + # Returns: The string, with "educated" curly quote HTML entities. + # + # Example input: "Isn't this fun?" + # Example output: “Isn’t this fun?” + # + $dq_open = $this->smart_doublequote_open; + $dq_close = $this->smart_doublequote_close; + $sq_open = $this->smart_singlequote_open; + $sq_close = $this->smart_singlequote_close; + + # Make our own "punctuation" character class, because the POSIX-style + # [:PUNCT:] is only available in Perl 5.6 or later: + $punct_class = "[!\"#\\$\\%'()*+,-.\\/:;<=>?\\@\\[\\\\\]\\^_`{|}~]"; + + # Special case if the very first character is a quote + # followed by punctuation at a non-word-break. Close the quotes by brute force: + $_ = preg_replace( + array("/^'(?=$punct_class\\B)/", "/^\"(?=$punct_class\\B)/"), + array($sq_close, $dq_close), $_); + + # Special case for double sets of quotes, e.g.: + #

He said, "'Quoted' words in a larger quote."

+ $_ = preg_replace( + array("/\"'(?=\w)/", "/'\"(?=\w)/"), + array($dq_open.$sq_open, $sq_open.$dq_open), $_); + + # Special case for decade abbreviations (the '80s): + $_ = preg_replace("/'(?=\\d{2}s)/", $sq_close, $_); + + $close_class = '[^\ \t\r\n\[\{\(\-]'; + $dec_dashes = '&\#8211;|&\#8212;'; + + # Get most opening single quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + ' # the quote + (?=\\w) # followed by a word character + }x", '\1'.$sq_open, $_); + # Single closing quotes: + $_ = preg_replace("{ + ($close_class)? + ' + (?(1)| # If $1 captured, then do nothing; + (?=\\s | s\\b) # otherwise, positive lookahead for a whitespace + ) # char or an 's' at a word ending position. This + # is a special case to handle something like: + # \"Custer's Last Stand.\" + }xi", '\1'.$sq_close, $_); + + # Any remaining single quotes should be opening ones: + $_ = str_replace("'", $sq_open, $_); + + + # Get most opening double quotes: + $_ = preg_replace("{ + ( + \\s | # a whitespace char, or +   | # a non-breaking space entity, or + -- | # dashes, or + &[mn]dash; | # named dash entities + $dec_dashes | # or decimal entities + &\\#x201[34]; # or hex + ) + \" # the quote + (?=\\w) # followed by a word character + }x", '\1'.$dq_open, $_); + + # Double closing quotes: + $_ = preg_replace("{ + ($close_class)? + \" + (?(1)|(?=\\s)) # If $1 captured, then do nothing; + # if not, then make sure the next char is whitespace. + }x", '\1'.$dq_close, $_); + + # Any remaining quotes should be opening ones. + $_ = str_replace('"', $dq_open, $_); + + return $_; + } + + + function educateCommaQuotes($_) { + # + # Parameter: String. + # Returns: The string, with ,,comma,, -style double quotes + # translated into HTML curly quote entities. + # + # Example input: ,,Isn't this fun?,, + # Example output: „Isn't this fun?„ + # + # Note: this is meant to be used alongside with backtick quotes; there is + # no language that use only lower quotations alone mark like in the example. + # + $_ = str_replace(",,", '„', $_); + return $_; + } + + + function educateGuillemets($_) { + # + # Parameter: String. + # Returns: The string, with << guillemets >> -style quotes + # translated into HTML guillemets entities. + # + # Example input: << Isn't this fun? >> + # Example output: „ Isn't this fun? „ + # + $_ = preg_replace("/(?:<|<){2}/", '«', $_); + $_ = preg_replace("/(?:>|>){2}/", '»', $_); + return $_; + } + + + function spaceFrenchQuotes($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside french-style quotes, only french quotes. + # + # Example input: Quotes in « French », »German« and »Finnish» style. + # Example output: Quotes in «_French_», »German« and »Finnish» style. + # + $opt = ( $this->do_space_frenchquote == 2 ? '?' : '' ); + $chr = ( $this->do_space_frenchquote != -1 ? $this->space_frenchquote : '' ); + + # Characters allowed immediatly outside quotes. + $outside_char = $this->space . '|\s|[.,:;!?\[\](){}|@*~=+-]|¡|¿'; + + $_ = preg_replace( + "/(^|$outside_char)(«|«|›|‹)$this->space$opt/", + "\\1\\2$chr", $_); + $_ = preg_replace( + "/$this->space$opt(»|»|‹|›)($outside_char|$)/", + "$chr\\1\\2", $_); + return $_; + } + + + function spaceColon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before colons. + # + # Example input: Ingredients : fun. + # Example output: Ingredients_: fun. + # + $opt = ( $this->do_space_colon == 2 ? '?' : '' ); + $chr = ( $this->do_space_colon != -1 ? $this->space_colon : '' ); + + $_ = preg_replace("/$this->space$opt(:)(\\s|$)/m", + "$chr\\1\\2", $_); + return $_; + } + + + function spaceSemicolon($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before semicolons. + # + # Example input: There he goes ; there she goes. + # Example output: There he goes_; there she goes. + # + $opt = ( $this->do_space_semicolon == 2 ? '?' : '' ); + $chr = ( $this->do_space_semicolon != -1 ? $this->space_semicolon : '' ); + + $_ = preg_replace("/$this->space(;)(?=\\s|$)/m", + " \\1", $_); + $_ = preg_replace("/((?:^|\\s)(?>[^&;\\s]+|&#?[a-zA-Z0-9]+;)*)". + " $opt(;)(?=\\s|$)/m", + "\\1$chr\\2", $_); + return $_; + } + + + function spaceMarks($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around question and exclamation marks. + # + # Example input: ¡ Holà ! What ? + # Example output: ¡_Holà_! What_? + # + $opt = ( $this->do_space_marks == 2 ? '?' : '' ); + $chr = ( $this->do_space_marks != -1 ? $this->space_marks : '' ); + + // Regular marks. + $_ = preg_replace("/$this->space$opt([?!]+)/", "$chr\\1", $_); + + // Inverted marks. + $imarks = "(?:¡|¡|¡|&#x[Aa]1;|¿|¿|¿|&#x[Bb][Ff];)"; + $_ = preg_replace("/($imarks+)$this->space$opt/", "\\1$chr", $_); + + return $_; + } + + + function spaceEmDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_emdash == 2 ? '?' : '' ); + $chr = ( $this->do_space_emdash != -1 ? $this->space_emdash : '' ); + $_ = preg_replace("/$this->space$opt(—|—)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + function spaceEnDash($_) { + # + # Parameters: String, two replacement characters separated by a hyphen (`-`), + # and forcing flag. + # + # Returns: The string, with appropriates spaces replaced + # around dashes. + # + # Example input: Then — without any plan — the fun happend. + # Example output: Then_—_without any plan_—_the fun happend. + # + $opt = ( $this->do_space_endash == 2 ? '?' : '' ); + $chr = ( $this->do_space_endash != -1 ? $this->space_endash : '' ); + $_ = preg_replace("/$this->space$opt(–|–)$this->space$opt/", + "$chr\\1$chr", $_); + return $_; + } + + + function spaceThousandSeparator($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # inside numbers (thousand separator in french). + # + # Example input: Il y a 10 000 insectes amusants dans ton jardin. + # Example output: Il y a 10_000 insectes amusants dans ton jardin. + # + $chr = ( $this->do_space_thousand != -1 ? $this->space_thousand : '' ); + $_ = preg_replace('/([0-9]) ([0-9])/', "\\1$chr\\2", $_); + return $_; + } + + + var $units = ' + ### Metric units (with prefixes) + (?: + p | + µ | µ | &\#0*181; | &\#[xX]0*[Bb]5; | + [mcdhkMGT] + )? + (?: + [mgstAKNJWCVFSTHBL]|mol|cd|rad|Hz|Pa|Wb|lm|lx|Bq|Gy|Sv|kat| + Ω | Ohm | Ω | &\#0*937; | &\#[xX]0*3[Aa]9; + )| + ### Computers units (KB, Kb, TB, Kbps) + [kKMGT]?(?:[oBb]|[oBb]ps|flops)| + ### Money + ¢ | ¢ | &\#0*162; | &\#[xX]0*[Aa]2; | + M?(?: + £ | £ | &\#0*163; | &\#[xX]0*[Aa]3; | + ¥ | ¥ | &\#0*165; | &\#[xX]0*[Aa]5; | + € | € | &\#0*8364; | &\#[xX]0*20[Aa][Cc]; | + $ + )| + ### Other units + (?: ° | ° | &\#0*176; | &\#[xX]0*[Bb]0; ) [CF]? | + %|pt|pi|M?px|em|en|gal|lb|[NSEOW]|[NS][EOW]|ha|mbar + '; //x + + function spaceUnit($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # before unit symbols. + # + # Example input: Get 3 mol of fun for 3 $. + # Example output: Get 3_mol of fun for 3_$. + # + $opt = ( $this->do_space_unit == 2 ? '?' : '' ); + $chr = ( $this->do_space_unit != -1 ? $this->space_unit : '' ); + + $_ = preg_replace('/ + (?:([0-9])[ ]'.$opt.') # Number followed by space. + ('.$this->units.') # Unit. + (?![a-zA-Z0-9]) # Negative lookahead for other unit characters. + /x', + "\\1$chr\\2", $_); + + return $_; + } + + + function spaceAbbr($_) { + # + # Parameters: String, replacement character, and forcing flag. + # Returns: The string, with appropriates spaces replaced + # around abbreviations. + # + # Example input: Fun i.e. something pleasant. + # Example output: Fun i.e._something pleasant. + # + $opt = ( $this->do_space_abbr == 2 ? '?' : '' ); + + $_ = preg_replace("/(^|\s)($this->abbr_after) $opt/m", + "\\1\\2$this->space_abbr", $_); + $_ = preg_replace("/( )$opt($this->abbr_sp_before)(?![a-zA-Z'])/m", + "\\1$this->space_abbr\\2", $_); + return $_; + } + + + function stupefyEntities($_) { + # + # Adding angle quotes and lower quotes to SmartyPants's stupefy mode. + # + $_ = parent::stupefyEntities($_); + + $_ = str_replace(array('„', '«', '»'), '"', $_); + + return $_; + } + + + function processEscapes($_) { + # + # Adding a few more escapes to SmartyPants's escapes: + # + # Escape Value + # ------ ----- + # \, , + # \< < + # \> > + # + $_ = parent::processEscapes($_); + + $_ = str_replace( + array('\,', '\<', '\>', '\<', '\>'), + array(',', '<', '>', '<', '>'), $_); + + return $_; + } +} + + +/* + +PHP SmartyPants Typographer +=========================== + +Version History +--------------- + +1.0 (28 Jun 2006) + +* First public release of PHP SmartyPants Typographer. + + +Bugs +---- + +To file bug reports or feature requests (other than topics listed in the +Caveats section above) please send email to: + + + +If the bug involves quotes being curled the wrong way, please send example +text to illustrate. + + +### Algorithmic Shortcomings ### + +One situation in which quotes will get curled the wrong way is when +apostrophes are used at the start of leading contractions. For example: + + 'Twas the night before Christmas. + +In the case above, SmartyPants will turn the apostrophe into an opening +single-quote, when in fact it should be a closing one. I don't think +this problem can be solved in the general case -- every word processor +I've tried gets this wrong as well. In such cases, it's best to use the +proper HTML entity for closing single-quotes (`’`) by hand. + + +Copyright and License +--------------------- + +PHP SmartyPants & Typographer +Copyright (c) 2004-2006 Michel Fortin + +All rights reserved. + +Original SmartyPants +Copyright (c) 2003-2004 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "SmartyPants" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" +and any express or implied warranties, including, but not limited to, the +implied warranties of merchantability and fitness for a particular purpose +are disclaimed. In no event shall the copyright owner or contributors be +liable for any direct, indirect, incidental, special, exemplary, or +consequential damages (including, but not limited to, procurement of +substitute goods or services; loss of use, data, or profits; or business +interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) +arising in any way out of the use of this software, even if advised of the +possibility of such damage. + +*/ +?> \ No newline at end of file diff --git a/panel b/panel deleted file mode 160000 index 53ebd4b..0000000 --- a/panel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 53ebd4b6964c02aab5671d2735f59ccc0178f144 diff --git a/panel/app/bootstrap.php b/panel/app/bootstrap.php new file mode 100644 index 0000000..2b73833 --- /dev/null +++ b/panel/app/bootstrap.php @@ -0,0 +1,71 @@ + 'panel.php', + + // global stuff + 'kirby\\panel\\login' => 'panel' . DS . 'login.php', + 'kirby\\panel\\translation' => 'panel' . DS . 'translation.php', + 'kirby\\panel\\autocomplete' => 'panel' . DS . 'autocomplete.php', + 'kirby\\panel\\roots' => 'panel' . DS . 'roots.php', + 'kirby\\panel\\urls' => 'panel' . DS . 'urls.php', + 'kirby\\panel\\view' => 'panel' . DS . 'view.php', + 'kirby\\panel\\layout' => 'panel' . DS . 'layout.php', + 'kirby\\panel\\snippet' => 'panel' . DS . 'snippet.php', + 'kirby\\panel\\installer' => 'panel' . DS . 'installer.php', + 'kirby\\panel\\widgets' => 'panel' . DS . 'widgets.php', + 'kirby\\panel\\topbar' => 'panel' . DS . 'topbar.php', + 'kirby\\panel\\search' => 'panel' . DS . 'search.php', + 'kirby\\panel\\structure' => 'panel' . DS . 'structure.php', + 'kirby\\panel\\structure\\store' => 'panel' . DS . 'structure' . DS . 'store.php', + 'kirby\\panel\\upload' => 'panel' . DS . 'upload.php', + + // controllers + 'kirby\\panel\\controllers\\base' => 'panel' . DS . 'controllers' . DS . 'base.php', + 'kirby\\panel\\controllers\\field' => 'panel' . DS . 'controllers' . DS . 'field.php', + + // form + 'kirby\\panel\\form' => 'panel' . DS . 'form.php', + 'kirby\\panel\\form\\plugins' => 'panel' . DS . 'form' . DS . 'plugins.php', + 'kirby\\panel\\form\\fieldoptions' => 'panel' . DS . 'form' . DS . 'fieldoptions.php', + + // models + 'kirby\\panel\\models\\site' => 'panel' . DS . 'models' . DS . 'site.php', + 'kirby\\panel\\models\\page' => 'panel' . DS . 'models' . DS . 'page.php', + 'kirby\\panel\\models\\page\\addbutton' => 'panel' . DS . 'models' . DS . 'page' . DS . 'addbutton.php', + 'kirby\\panel\\models\\page\\menu' => 'panel' . DS . 'models' . DS . 'page' . DS . 'menu.php', + 'kirby\\panel\\models\\page\\sidebar' => 'panel' . DS . 'models' . DS . 'page' . DS . 'sidebar.php', + 'kirby\\panel\\models\\page\\changes' => 'panel' . DS . 'models' . DS . 'page' . DS . 'changes.php', + 'kirby\\panel\\models\\page\\uploader' => 'panel' . DS . 'models' . DS . 'page' . DS . 'uploader.php', + 'kirby\\panel\\models\\page\\sorter' => 'panel' . DS . 'models' . DS . 'page' . DS . 'sorter.php', + 'kirby\\panel\\models\\page\\blueprint' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint.php', + 'kirby\\panel\\models\\page\\blueprint\\pages' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'pages.php', + 'kirby\\panel\\models\\page\\blueprint\\files' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'files.php', + 'kirby\\panel\\models\\page\\blueprint\\fields' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'fields.php', + 'kirby\\panel\\models\\page\\blueprint\\field' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'field.php', + 'kirby\\panel\\models\\page\\blueprint\\options' => 'panel' . DS . 'models' . DS . 'page' . DS . 'blueprint' . DS . 'options.php', + 'kirby\\panel\\models\\file' => 'panel' . DS . 'models' . DS . 'file.php', + 'kirby\\panel\\models\\file\\menu' => 'panel' . DS . 'models' . DS . 'file' . DS . 'menu.php', + 'kirby\\panel\\models\\user' => 'panel' . DS . 'models' . DS . 'user.php', + 'kirby\\panel\\models\\user\\blueprint' => 'panel' . DS . 'models' . DS . 'user' . DS . 'blueprint.php', + 'kirby\\panel\\models\\user\\avatar' => 'panel' . DS . 'models' . DS . 'user' . DS . 'avatar.php', + 'kirby\\panel\\models\\user\\history' => 'panel' . DS . 'models' . DS . 'user' . DS . 'history.php', + + // collections + 'kirby\\panel\\collections\\users' => 'panel' . DS . 'collections' . DS . 'users.php', + 'kirby\\panel\\collections\\files' => 'panel' . DS . 'collections' . DS . 'files.php', + 'kirby\\panel\\collections\\children' => 'panel' . DS . 'collections' . DS . 'children.php', + +), __DIR__ . DS . 'src'); + + +// some fallbacks for possible namespace issues and convenience +class_alias('Kirby\\Panel\\Form\\FieldOptions', 'FieldOptions'); +class_alias('Kirby\\Panel', 'Panel'); + +include(__DIR__ . DS . 'helpers.php'); + diff --git a/panel/app/config/routes.php b/panel/app/config/routes.php new file mode 100644 index 0000000..6607eef --- /dev/null +++ b/panel/app/config/routes.php @@ -0,0 +1,298 @@ + 'login', + 'action' => 'AuthController::login', + 'filter' => 'isInstalled', + 'method' => 'GET|POST' + ), + array( + 'pattern' => 'logout', + 'action' => 'AuthController::logout', + 'method' => 'GET', + 'filter' => 'auth', + ), + + // Installation + array( + 'pattern' => 'install', + 'action' => 'InstallationController::index', + 'method' => 'GET|POST' + ), + + // Dashboard + array( + 'pattern' => '/', + 'action' => 'DashboardController::index', + 'filter' => array('auth', 'isInstalled'), + ), + + // Search + array( + 'pattern' => 'search', + 'action' => 'SearchController::results', + 'method' => 'GET|POST', + 'filter' => array('auth'), + ), + + // Options + array( + 'pattern' => 'options', + 'action' => 'OptionsController::index', + 'method' => 'GET|POST', + 'filter' => 'auth' + ), + + // Files + array( + 'pattern' => array( + 'site(/)file/(:any)/edit', + 'pages/(:all)/file/(:any)/edit', + ), + 'action' => 'FilesController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/context', + 'pages/(:all)/file/(:any)/context', + ), + 'action' => 'FilesController::context', + 'filter' => 'auth', + 'method' => 'GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/thumb', + 'pages/(:all)/file/(:any)/thumb', + ), + 'action' => 'FilesController::thumb', + 'filter' => 'auth', + 'method' => 'GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/delete', + 'pages/(:all)/file/(:any)/delete', + ), + 'action' => 'FilesController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + array( + 'pattern' => array( + 'site(/)file/(:any)/replace', + 'pages/(:all)/file/(:any)/replace', + ), + 'action' => 'FilesController::replace', + 'filter' => 'auth', + 'method' => 'POST', + ), + array( + 'pattern' => array( + 'site(/)files', + 'pages/(:all)/files', + ), + 'action' => 'FilesController::index', + 'filter' => 'auth', + 'method' => 'POST|GET', + ), + + // Field routes + array( + 'pattern' => array( + 'site(/)file/(:any)/field/(:any)/(:any)/(:all)', + 'pages/(:all)/file/(:any)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forFile', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + array( + 'pattern' => array( + 'site(/)field/(:any)/(:any)/(:all)', + 'pages/(:all)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forPage', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + array( + 'pattern' => array( + 'users/(:all)/field/(:any)/(:any)/(:all)', + ), + 'action' => 'FieldController::forUser', + 'filter' => 'auth', + 'method' => 'GET|POST' + ), + + // New Page + array( + 'pattern' => array( + 'site(/)add', + 'pages/(:all)/add', + ), + 'action' => 'PagesController::add', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // URL Settings + array( + 'pattern' => 'pages/(:all)/url', + 'action' => 'PagesController::url', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Template Modal + array( + 'pattern' => 'pages/(:all)/template', + 'action' => 'PagesController::template', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Toggle visibility + array( + 'pattern' => 'pages/(:all)/toggle', + 'action' => 'PagesController::toggle', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Delete a page + array( + 'pattern' => 'pages/(:all)/delete', + 'action' => 'PagesController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Keeping page changes + array( + 'pattern' => array( + 'site(/)keep', + 'pages/(:all)/keep', + ), + 'action' => 'PagesController::keep', + 'method' => 'GET|POST', + 'filter' => 'auth', + ), + + // Discarding page changes + array( + 'pattern' => array( + 'site(/)discard', + 'pages/(:all)/discard', + ), + 'action' => 'PagesController::discard', + 'method' => 'GET|POST', + 'filter' => 'auth', + ), + + // Page context menu + array( + 'pattern' => 'pages/(:all)/context', + 'action' => 'PagesController::context', + 'method' => 'GET', + 'filter' => 'auth', + ), + + // Upload a file + array( + 'pattern' => array( + 'site(/)upload', + 'pages/(:all)/upload', + ), + 'action' => 'FilesController::upload', + 'filter' => 'auth', + 'method' => 'POST' + ), + + // Subpages + array( + 'pattern' => array( + 'site(/)subpages', + 'pages/(:all)/subpages', + ), + 'action' => 'SubpagesController::index', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Page + array( + 'pattern' => 'pages/(:all)/edit', + 'action' => 'PagesController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Users + array( + 'pattern' => 'users', + 'action' => 'UsersController::index', + 'filter' => 'auth' + ), + array( + 'pattern' => 'users/add', + 'action' => 'UsersController::add', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + array( + 'pattern' => 'users/(:any)/edit', + 'action' => 'UsersController::edit', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + array( + 'pattern' => 'users/(:any)/delete', + 'action' => 'UsersController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Avatars + array( + 'pattern' => 'users/(:any)/avatar', + 'action' => 'AvatarsController::upload', + 'filter' => 'auth', + 'method' => 'POST' + ), + array( + 'pattern' => 'users/(:any)/avatar/delete', + 'action' => 'AvatarsController::delete', + 'filter' => 'auth', + 'method' => 'POST|GET' + ), + + // Autocomplete + array( + 'pattern' => 'api/autocomplete/(:any)', + 'action' => 'AutocompleteController::index', + 'method' => 'POST', + 'filter' => 'auth', + ), + + // form assets + array( + 'pattern' => 'plugins/js', + 'action' => 'AssetsController::js', + 'method' => 'GET', + 'filter' => 'auth' + ), + array( + 'pattern' => 'plugins/css', + 'action' => 'AssetsController::css', + 'method' => 'GET', + 'filter' => 'auth' + ), + +); \ No newline at end of file diff --git a/panel/app/controllers/assets.php b/panel/app/controllers/assets.php new file mode 100644 index 0000000..705356f --- /dev/null +++ b/panel/app/controllers/assets.php @@ -0,0 +1,17 @@ +plugins()->js(), 'text/javascript'); + } + + public function css() { + $form = new Form(); + return new Response($form->plugins()->css(), 'text/css'); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/auth.php b/panel/app/controllers/auth.php new file mode 100644 index 0000000..5e0f399 --- /dev/null +++ b/panel/app/controllers/auth.php @@ -0,0 +1,62 @@ +layout('base', array( + 'content' => $this->view('auth/error', array( + 'error' => $e->getMessage() + )) + )); + } + + if($login->isAuthenticated()) { + $this->redirect(); + } + + if($login->isBlocked()) { + return $this->layout('base', array( + 'content' => $this->view('auth/block') + )); + } + + $self = $this; + $form = $this->form('auth/login', null, function($form) use($self, $login) { + + $data = $form->serialize(); + + try { + $login->attempt($data['username'], $data['password']); + $self->redirect(); + } catch(Exception $e) { + $form->alert(l('login.error')); + $form->fields->username->error = true; + $form->fields->password->error = true; + } + + }); + + return $this->layout('base', array( + 'bodyclass' => 'login', + 'content' => $this->view('auth/login', compact('form')) + )); + + } + + public function logout() { + + if($user = panel()->user()) { + $user->logout(); + } + + $this->redirect('login'); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/autocomplete.php b/panel/app/controllers/autocomplete.php new file mode 100644 index 0000000..6ec6b84 --- /dev/null +++ b/panel/app/controllers/autocomplete.php @@ -0,0 +1,20 @@ +result(); + } catch(Exception $e) { + $result = array(); + } + + return $this->json(array( + 'data' => $result + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/avatars.php b/panel/app/controllers/avatars.php new file mode 100644 index 0000000..c82c2bf --- /dev/null +++ b/panel/app/controllers/avatars.php @@ -0,0 +1,49 @@ +user($username); + + try { + $user->avatar()->upload(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($user); + + } + + public function delete($username) { + + $self = $this; + $user = $this->user($username); + $avatar = $user->avatar(); + + if(!$avatar->exists()) { + return $this->modal('error', array( + 'text' => l('users.avatar.missing'), + 'back' => $user->url() + )); + } + + $form = $avatar->form('delete', function($form) use($user, $avatar, $self) { + + try { + $avatar->delete(); + $self->notify(':)'); + $self->redirect($user); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('avatars/delete', compact('form')); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/dashboard.php b/panel/app/controllers/dashboard.php new file mode 100644 index 0000000..5f3f843 --- /dev/null +++ b/panel/app/controllers/dashboard.php @@ -0,0 +1,13 @@ +screen('dashboard/index', panel()->site(), array( + 'widgets' => new Kirby\Panel\Widgets() + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/error.php b/panel/app/controllers/error.php new file mode 100644 index 0000000..cf6ee6f --- /dev/null +++ b/panel/app/controllers/error.php @@ -0,0 +1,35 @@ +auth(); + + if(is_null($text)) { + $text = l('pages.error.missing'); + } + + if(server::get('HTTP_MODAL')) { + return $this->modal('error', array( + 'text' => $text, + 'back' => url::last(), + )); + } else { + return $this->screen('error/index', 'error', array( + 'text' => $text, + 'exception' => $exception + )); + } + + } + + public function auth() { + try { + $user = panel()->user(); + } catch(Exception $e) { + $this->redirect('login'); + } + } + +} \ No newline at end of file diff --git a/panel/app/controllers/field.php b/panel/app/controllers/field.php new file mode 100644 index 0000000..ae571c3 --- /dev/null +++ b/panel/app/controllers/field.php @@ -0,0 +1,82 @@ +page($pageId); + $file = $page->file(File::decodeFilename($filename)); + + if(!$file) { + throw new Exception(l('files.error.missing.file')); + } + + $form = $file->form('edit', function() {}); + + return $this->route($file, $form, $fieldName, $fieldType, $path); + + } + + public function forPage($pageId, $fieldName, $fieldType, $path) { + + $page = $this->page($pageId); + $form = $page->form('edit', function() {}); + + return $this->route($page, $form, $fieldName, $fieldType, $path); + + } + + public function forUser($username, $fieldName, $fieldType, $path) { + + $user = panel()->user($username); + $form = $user->form('user', function() {}); + + return $this->route($user, $form, $fieldName, $fieldType, $path); + + } + + public function route($model, $form, $fieldName, $fieldType, $path) { + + $field = $form->fields()->$fieldName; + + if(!$field or $field->type() !== $fieldType) { + throw new Exception('Invalid field'); + } + + $routes = $field->routes(); + $router = new Router($routes); + + if($route = $router->run($path)) { + + if(is_callable($route->action()) and is_a($route->action(), 'Closure')) { + return call($route->action(), $route->arguments()); + } else { + + $controllerFile = $field->root() . DS . 'controller.php'; + $controllerName = $fieldType . 'FieldController'; + + if(!file_exists($controllerFile)) { + throw new Exception(l('fields.error.missing.controller')); + } + + require_once($controllerFile); + + if(!class_exists($controllerName)) { + throw new Exception(l('fields.error.missing.class')); + } + + $controller = new $controllerName($model, $field); + + return call(array($controller, $route->action()), $route->arguments()); + + } + + } else { + throw new Exception(l('fields.error.route.invalid')); + } + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/files.php b/panel/app/controllers/files.php new file mode 100644 index 0000000..3740234 --- /dev/null +++ b/panel/app/controllers/files.php @@ -0,0 +1,195 @@ +page($id); + $files = $page->files(); + + // don't create the view if the page is not allowed to have files + if(!$page->canHaveFiles()) { + throw new Exception(l('files.index.error.disabled')); + } + + // sort action + $this->sort($page); + + return $this->screen('files/index', $files, array( + 'page' => $page, + 'files' => $files, + 'back' => $page->url('edit'), + 'sortable' => $page->canSortFiles(), + 'uploader' => $this->snippet('uploader', array('url' => $page->url('upload'))) + )); + + } + + public function edit($id, $filename) { + + $self = $this; + $page = $this->page($id); + + try { + $file = $this->file($page, $filename); + } catch(Exception $e) { + $this->alert(l('files.error.missing.file')); + $this->redirect($page); + } + + // setup the form and form action + $form = $file->form('edit', function($form) use($file, $page, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return $self->alert(l('files.show.error.form')); + } + + try { + $file->update($form->serialize()); + $self->notify(':)'); + $self->redirect($file); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('files/edit', $file, array( + 'form' => $form, + 'page' => $page, + 'file' => $file, + 'returnTo' => url::last() == $page->url('files') ? $page->uri('files') : $page->uri('edit'), + 'uploader' => $this->snippet('uploader', array( + 'url' => $file->url('replace'), + 'accept' => $file->mime(), + 'multiple' => false + )) + )); + + } + + public function upload($id) { + + $page = $this->page($id); + + try { + $page->upload(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($page); + + } + + public function replace($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + + try { + $file->replace(); + $this->notify(':)'); + } catch(Exception $e) { + $this->alert($e->getMessage()); + } + + $this->redirect($file); + + } + + public function context($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + + return $file->menu(); + + } + + public function thumb($id, $filename) { + + $page = $this->page($id); + $file = $this->file($page, $filename); + $width = intval(get('width')); + $height = intval(get('height')); + + if(!$file->canHavePreview()) { + return response::error('No preview available', 404); + } + + if(!$file->canHaveThumb()) { + go($file->url()); + } + + if(get('crop') == true) { + $thumb = $file->crop($width, $height, 80); + } else { + $thumb = $file->resize($width, $height, 80); + } + + go($thumb->url()); + + } + + public function delete($id, $filename) { + + $self = $this; + $page = $this->page($id); + $file = $this->file($page, $filename); + $form = $this->form('files/delete', $file, function($form) use($file, $page, $self) { + + try { + $file->delete(); + $self->notify(':)'); + $self->redirect($page, 'edit'); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('files/delete', compact('form')); + + } + + protected function file($page, $filename) { + + $file = $page->file(File::decodeFilename($filename)); + + if(!$file) { + throw new Exception(l('files.error.missing.file')); + } + + return $file; + + } + + protected function sort($page) { + + if(!r::is('post') or get('action') != 'sort') return; + + $filenames = get('filenames'); + $counter = 0; + + foreach($filenames as $filename) { + if($file = $page->file($filename)) { + $counter++; + try { + $file->update('sort', $counter); + } catch(Exception $e) { + + } + } + } + + $this->redirect($page, 'files'); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/installation.php b/panel/app/controllers/installation.php new file mode 100644 index 0000000..e9fc0b7 --- /dev/null +++ b/panel/app/controllers/installation.php @@ -0,0 +1,76 @@ +isCompleted()) { + $this->redirect(); + } else if($problems = $installer->problems()) { + return $this->problems($problems); + } else { + return $this->signup(); + } + + } + + protected function problems($problems) { + $form = $this->form('installation/check', array($problems)); + return $this->modal('index', compact('form')); + } + + protected function signup() { + + $self = $this; + $form = $this->form('installation/signup', array(), function($form) use($self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + try { + + // fetch all the form data + $data = $form->serialize(); + + // make sure that the first user is an admin + $data['role'] = 'admin'; + + // try to create the new user + $user = site()->users()->create($data); + + // store the new username for the login screen + s::set('username', $user->username()); + + // try to login the user automatically + if($user->hasPanelAccess()) { + $user->login($data['password']); + } + + // redirect to the login + $self->redirect('login'); + + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('index', compact('form')); + + } + + public function modal($view, $data = array()) { + return $this->layout('base', array( + 'bodyclass' => 'installation', + 'content' => $this->view('installation/' . $view, $data) + )); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/options.php b/panel/app/controllers/options.php new file mode 100644 index 0000000..d33c259 --- /dev/null +++ b/panel/app/controllers/options.php @@ -0,0 +1,40 @@ +site(); + $sidebar = $site->sidebar(); + $form = $site->form('edit', function($form) use($site, $self) { + + // validate all fields + $form->validate(); + + // stop at invalid fields + if(!$form->isValid()) { + return $self->alert(l('pages.show.error.form')); + } + + try { + $site->update($form->serialize()); + $self->notify(':)'); + return $self->redirect('options'); + } catch(Exception $e) { + return $self->alert($e->getMessage()); + } + + }); + + return $this->screen('options/index', $site, array( + 'site' => $site, + 'form' => $form, + 'files' => $sidebar->files(), + 'license' => panel()->license(), + 'uploader' => $this->snippet('uploader', array('url' => $site->url('upload'))) + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/pages.php b/panel/app/controllers/pages.php new file mode 100644 index 0000000..11d2d51 --- /dev/null +++ b/panel/app/controllers/pages.php @@ -0,0 +1,218 @@ +page($id); + $form = $parent->form('add', function($form) use($parent, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return $form->alert(l('pages.add.error.template')); + } + + try { + + $data = $form->serialize(); + $page = $parent->children()->create($data['uid'], $data['template'], array( + 'title' => $data['title'] + )); + + $self->notify(':)'); + $this->redirect($page, 'edit'); + + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/add', compact('form')); + + } + + public function edit($id) { + + $self = $this; + + try { + $page = $this->page($id); + } catch(Exception $e) { + if($page = $this->page(dirname($id))) { + $this->alert(l('pages.error.missing')); + $this->redirect($page); + } + } + + $form = $page->form('edit', function($form) use($page, $self) { + + // validate all fields + $form->validate(); + + // stop at invalid fields + if(!$form->isValid()) { + return $self->alert(l('pages.show.error.form')); + } + + try { + $page->update($form->serialize()); + $self->notify(':)'); + return $self->redirect($page); + } catch(Exception $e) { + return $self->alert($e->getMessage()); + } + + }); + + return $this->screen('pages/edit', $page, array( + 'page' => $page, + 'sidebar' => $page->sidebar(), + 'form' => $form, + 'uploader' => $this->snippet('uploader', array('url' => $page->url('upload'))) + )); + + } + + public function delete($id) { + + $self = $this; + $page = $this->page($id); + + try { + $page->isDeletable(true); + } catch(Exception $e) { + return $this->modal('error', array( + 'headline' => l($e->getMessage() . '.headline'), + 'text' => l($e->getMessage() . '.text'), + 'back' => $page->url() + )); + } + + $form = $page->form('delete', function($form) use($page, $self) { + try { + $page->delete(); + $self->notify(':)'); + $self->redirect($page->parent()->isSite() ? '/' : $page->parent()); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + }); + + return $this->modal('pages/delete', compact('form')); + + } + + public function keep($id) { + $page = $this->page($id); + $page->changes()->keep(); + $this->redirect($page); + } + + public function discard($id) { + $page = $this->page($id); + $page->changes()->discard(); + $this->redirect($page); + } + + public function url($id) { + + $self = $this; + $page = $this->page($id); + + if(!$page->canChangeUrl()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.url.error.rights'), + )); + } + + $form = $page->form('url', function($form) use($page, $self) { + + try { + $page->move(get('uid')); + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + $form->fields->uid->error = true; + } + + }); + + return $this->modal('pages/url', compact('form')); + + } + + public function template($id) { + + $self = $this; + $page = $this->page($id); + + if(!$page->canChangeTemplate()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.template.error'), + )); + } + + if($info = get('info')) { + $prep = $page->prepareForNewTemplate($page->blueprint()->name(), $info); + return $this->snippet('template', $prep); + } + + $form = $page->form('template', function($form) use($page, $self) { + + try { + + $data = $form->serialize(); + + $page->changeTemplate(a::get($data, 'template')); + + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/template', compact('form')); + + } + + public function toggle($id) { + + $self = $this; + $page = $this->page($id); + + if($page->isErrorPage()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('pages.toggle.error.error'), + )); + } + + $form = $page->form('toggle', function($form) use($page, $self) { + + try { + $page->toggle(get('position', 'last')); + $self->notify(':)'); + $self->redirect($page); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('pages/toggle', compact('form')); + + } + + public function context($id) { + return $this->page($id)->menu('context'); + } + +} \ No newline at end of file diff --git a/panel/app/controllers/search.php b/panel/app/controllers/search.php new file mode 100644 index 0000000..b2a43ff --- /dev/null +++ b/panel/app/controllers/search.php @@ -0,0 +1,18 @@ +view('search/results', array( + 'pages' => $search->pages(), + 'users' => $search->users(), + )); + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/subpages.php b/panel/app/controllers/subpages.php new file mode 100644 index 0000000..7cac180 --- /dev/null +++ b/panel/app/controllers/subpages.php @@ -0,0 +1,91 @@ +page($id); + + // don't create the view if the page is not allowed to have subpages + if(!$page->canHaveSubpages()) { + throw new Exception(l('subpages.add.error')); + } + + // get the subpages + $visible = $this->visible($page); + $invisible = $this->invisible($page); + + // activate the sorting + $this->sort($page); + + return $this->screen('subpages/index', $page, array( + 'page' => $page, + 'addbutton' => $page->addbutton(), + 'sortable' => $page->blueprint()->pages()->sortable(), + 'flip' => $page->blueprint()->pages()->sort() == 'flip', + 'visible' => $visible, + 'invisible' => $invisible, + )); + + } + + protected function subpages($page, $type) { + + $pages = $page->children()->$type()->paginated('subpages/' . $type); + $pagination = $this->snippet('pagination', array( + 'pagination' => $pages->pagination(), + 'nextUrl' => $pages->pagination()->nextPageUrl(), + 'prevUrl' => $pages->pagination()->prevPageUrl(), + )); + + return new Obj(array( + 'pages' => $pages, + 'pagination' => $pagination, + 'start' => $pages->pagination()->numStart(), + 'total' => $pages->pagination()->items(), + 'firstPage' => $pages->pagination()->firstPageUrl(), + )); + + } + + protected function visible($page) { + return $this->subpages($page, 'visible'); + } + + protected function invisible($page) { + return $this->subpages($page, 'invisible'); + } + + protected function sort($page) { + + // handle sorting + if(r::is('post') and $action = get('action') and $id = get('id')) { + + $subpage = $this->page($page->id() . '/' . $id); + + switch($action) { + case 'sort': + try { + $subpage->sort(get('to')); + } catch(Exception $e) { + // no error handling, because if sorting + // breaks, the refresh will fix it. + } + break; + case 'hide': + try { + $subpage->hide(); + } catch(Exception $e) { + // no error handling, because if sorting + // breaks, the refresh will fix it. + } + break; + } + + $this->redirect($page, 'subpages'); + + } + + } + +} \ No newline at end of file diff --git a/panel/app/controllers/users.php b/panel/app/controllers/users.php new file mode 100644 index 0000000..ba41eea --- /dev/null +++ b/panel/app/controllers/users.php @@ -0,0 +1,134 @@ +users()->paginate(20, array('method' => 'query')); + $admin = panel()->user()->isAdmin(); + $pagination = $this->snippet('pagination', array( + 'pagination' => $users->pagination(), + 'nextUrl' => $users->pagination()->nextPageUrl(), + 'prevUrl' => $users->pagination()->prevPageUrl(), + )); + + return $this->screen('users/index', $users, array( + 'users' => $users, + 'admin' => $admin, + 'pagination' => $pagination + )); + + } + + public function add() { + + if(!panel()->user()->isAdmin()) { + $this->redirect('users'); + } + + $self = $this; + $form = $this->form('users/user', null, function($form) use($self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $data = $form->serialize(); + + try { + $user = panel()->users()->create($data); + $self->notify(':)'); + $self->redirect('users'); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('users/edit', 'user', array( + 'user' => null, + 'form' => $form, + 'writable' => is_writable(kirby()->roots()->accounts()), + 'uploader' => null + )); + + } + + public function edit($username) { + + $self = $this; + $user = $this->user($username); + + if(!panel()->user()->isAdmin() and !$user->isCurrent()) { + $this->redirect('users'); + } + + $form = $user->form('user', function($form) use($user, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $data = $form->serialize(); + + try { + $user->update($data); + $self->notify(':)'); + $self->redirect($user, 'edit'); + } catch(Exception $e) { + $self->alert($e->getMessage()); + } + + }); + + return $this->screen('users/edit', $user, array( + 'user' => $user, + 'form' => $form, + 'writable' => is_writable(kirby()->roots()->accounts()), + 'uploader' => $this->snippet('uploader', array( + 'url' => $user->url('avatar'), + 'accept' => 'image/jpeg,image/png,image/gif', + 'multiple' => false + )) + )); + + } + + public function delete($username) { + + $user = $this->user($username); + $self = $this; + + if(!panel()->user()->isAdmin() and !$user->isCurrent()) { + return $this->modal('error', array( + 'headline' => l('error'), + 'text' => l('users.delete.error.rights'), + 'back' => purl('users') + )); + } else { + + $form = $user->form('delete', function($form) use($user, $self) { + + try { + $user->delete(); + $self->notify(':)'); + $self->redirect('users'); + } catch(Exception $e) { + $form->alert($e->getMessage()); + } + + }); + + return $this->modal('users/delete', compact('form')); + + } + + } + +} \ No newline at end of file diff --git a/panel/app/fields/base/base.php b/panel/app/fields/base/base.php new file mode 100644 index 0000000..2d991ce --- /dev/null +++ b/panel/app/fields/base/base.php @@ -0,0 +1,191 @@ + array(), 'css' => array()); + + public $id; + public $name; + public $input; + public $label; + public $icon; + public $type; + public $help; + public $value; + public $text; + public $autofocus; + public $placeholder; + public $options; + public $content; + public $readonly; + public $disabled; + public $required; + public $validate; + public $width; + public $default; + public $error = false; + public $parentField = false; + public $page; + public $model; + + public function root() { + $obj = new ReflectionClass($this); + return dirname($obj->getFileName()); + } + + public function validate() { + + try { + + if(!$this->validate) { + return true; + } else if(is_array($this->validate)) { + foreach($this->validate as $validator => $options) { + if(!is_null($options)) { + if(is_numeric($validator)) { + $result = call('v::' . $options, $this->value()); + } else { + $result = call('v::' . $validator, array($this->value(), $options)); + } + if(!$result) return false; + } + } + return true; + } else { + return call('v::' . $this->validate, $this->value()); + } + + } catch(Exception $e) { + return true; + } + + } + + public function result() { + return get($this->name()); + } + + public function __call($name, $args) { + return isset($this->{$name}) ? $this->{$name} : null; + } + + public function id() { + if(!is_null($this->id)) return $this->id; + return 'form-field-' . $this->name; + } + + public function label() { + + if(!$this->label) return null; + + $label = new Brick('label', $this->i18n($this->label)); + $label->addClass('label'); + $label->attr('for', $this->id()); + + if($this->required()) { + $label->append(new Brick('abbr', '*', array('title' => l::get('required', 'Required')))); + } + + return $label; + + } + + public function i18n($value) { + return i18n($value); + } + + public function icon() { + + if(empty($this->icon)) { + return null; + } else if($this->readonly() and empty($this->icon)) { + $this->icon = 'lock'; + } + + $i = new Brick('i'); + $i->addClass('icon fa fa-' . $this->icon); + + $icon = new Brick('div'); + $icon->addClass('field-icon'); + $icon->append($i); + + return $icon; + + } + + public function help() { + + if(!$this->help) return null; + + $help = new Brick('div'); + $help->addClass('field-help marginalia text'); + $help->html($this->i18n($this->help)); + return $help; + + } + + public function input() { + return $this->input; + } + + public function content() { + + $content = new Brick('div'); + $content->addClass('field-content'); + $content->append($this->input()); + $content->append($this->icon()); + return $content; + + } + + public function element() { + + $element = new Brick('div'); + + $element->addClass('field'); + $element->addClass('field-grid-item'); + + if($this->error) { + $element->addClass('field-with-error'); + } + + if($this->width) { + $element->addClass('field-grid-item-' . str_replace('/', '-', $this->width)); + } + + if($this->readonly) { + $element->addClass('field-is-readonly'); + } + + if($this->disabled) { + $element->addClass('field-is-disabled'); + } + + if($this->icon) { + $element->addClass('field-with-icon'); + } + + $element->addClass('field-name-' . $this->name); + + return $element; + + } + + public function template() { + + return $this->element() + ->append($this->label()) + ->append($this->content()) + ->append($this->help()); + + } + + public function __toString() { + try { + return (string)$this->template(); + } catch(Exception $e) { + return (string)$e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/fields/checkbox/checkbox.php b/panel/app/fields/checkbox/checkbox.php new file mode 100644 index 0000000..c1db055 --- /dev/null +++ b/panel/app/fields/checkbox/checkbox.php @@ -0,0 +1,46 @@ +addClass('checkbox'); + $input->attr(array( + 'id' => $this->id(), + 'name' => $this->name(), + 'required' => $this->required(), + 'autofocus' => $this->autofocus(), + 'autocomplete' => $this->autocomplete(), + 'readonly' => $this->readonly(), + 'type' => 'checkbox', + 'checked' => v::accepted($this->value()), + )); + + $wrapper = parent::input(); + $wrapper->tag('label'); + $wrapper->text($this->i18n($this->text())); + $wrapper->attr('for', $this->id()); + $wrapper->removeAttr('id'); + $wrapper->addClass('input-with-checkbox'); + $wrapper->prepend($input); + + return $wrapper; + + } + + public function value() { + $value = parent::value(); + return empty($value) ? '0' : $value; + } + + public function result() { + $result = parent::result(); + return v::accepted($result) ? '1' : '0'; + } + + public function validate() { + return v::accepted($this->value()) or v::denied($this->value()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/checkboxes/checkboxes.php b/panel/app/fields/checkboxes/checkboxes.php new file mode 100644 index 0000000..fb100ae --- /dev/null +++ b/panel/app/fields/checkboxes/checkboxes.php @@ -0,0 +1,45 @@ +replaceClass('radio', 'checkbox'); + $input->attr(array( + 'name' => $this->name() . '[]', + 'type' => 'checkbox', + 'value' => $value, + 'checked' => ($this->value === 'all') ? true : in_array($value, (array)$this->value()), + 'required' => false, + )); + + return $input; + + } + + public function value() { + + $value = InputListField::value(); + + if(is_array($value)) { + return $value; + } else { + return str::split($value, ','); + } + + } + + public function result() { + $result = parent::result(); + return is_array($result) ? implode(', ', $result) : ''; + } + + public function item($value, $text) { + $item = parent::item($value, $text); + $item->replaceClass('input-with-radio', 'input-with-checkbox'); + return $item; + } + +} diff --git a/panel/app/fields/date/assets/js/date.js b/panel/app/fields/date/assets/js/date.js new file mode 100755 index 0000000..8d97e6a --- /dev/null +++ b/panel/app/fields/date/assets/js/date.js @@ -0,0 +1,50 @@ +(function($) { + + $.fn.date = function() { + + return this.each(function() { + + if($(this).data('pikaday')) { + return $(this); + } + + var input = $(this).attr('type', 'text'); + var hidden = input.next(); + var format = input.data('format'); + var val = input.val(); + var date = val ? moment(val).format(format) : null; + + input.attr('placeholder', format); + input.val(date); + + // don't initialize the datepicker on readonly fields + if(input.is('[readonly]')) { + return false; + } + + input.on('change', function() { + var val = input.val(); + if(val) { + hidden.val(moment(val, format).format('YYYY-MM-DD')); + } else { + hidden.val(''); + } + }); + + var pikaday = new Pikaday({ + field : this, + firstDay : 1, + format : format, + i18n : input.data('i18n'), + onSelect : function(date) { + hidden.val(moment(date).format('YYYY-MM-DD')); + } + }); + + $(this).data('pikaday', pikaday); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/date/date.php b/panel/app/fields/date/date.php new file mode 100644 index 0000000..cd6648c --- /dev/null +++ b/panel/app/fields/date/date.php @@ -0,0 +1,63 @@ + array( + 'date.js' + ) + ); + + public function __construct() { + + $this->type = 'date'; + $this->icon = 'calendar'; + $this->label = l::get('fields.date.label', 'Date'); + $this->format = 'YYYY-MM-DD'; + + } + + public function format() { + $format = str::upper($this->format); + return empty($format) ? 'YYYY-MM-DD' : $format; + } + + public function validate() { + return v::date($this->result()); + } + + public function value() { + if($this->override()) { + $this->value = $this->default(); + } + return !empty($this->value) ? date('Y-m-d', strtotime($this->value)) : null; + } + + public function input() { + + $input = parent::input(); + $input->removeAttr('name'); + $input->data(array( + 'field' => 'date', + 'format' => $this->format(), + 'i18n' => html(json_encode(array( + 'previousMonth' => '‹', + 'nextMonth' => '›', + 'months' => l::get('fields.date.months'), + 'weekdays' => l::get('fields.date.weekdays'), + 'weekdaysShort' => l::get('fields.date.weekdays.short') + )), false) + )); + + $hidden = new Brick('input', null); + $hidden->type = 'hidden'; + $hidden->name = $this->name(); + $hidden->value = $this->value(); + + return $input . $hidden; + + } + +} diff --git a/panel/app/fields/datetime/datetime.php b/panel/app/fields/datetime/datetime.php new file mode 100644 index 0000000..476eb7c --- /dev/null +++ b/panel/app/fields/datetime/datetime.php @@ -0,0 +1,86 @@ +date = array( + 'format' => 'YYYY-MM-DD' + ); + + $this->time = array( + 'interval' => 60, + 'format' => 24 + ); + + } + + public function validate() { + + $result = $this->result(); + + if(empty($result)) { + return !$this->required(); + } else { + return v::date($result); + } + + } + + public function result() { + + $value = array_filter($this->value()); + + if(empty($value) or !isset($value['date']) or !isset($value['time'])) { + return ''; + } + + return a::get($value, 'date') . ' ' . a::get($value, 'time') . ':00'; + + } + + public function content() { + + if(is_array($this->value())) { + $timestamp = strtotime($this->result()); + } else { + $timestamp = strtotime($this->value()); + } + + $dateDefault = a::get($this->date, 'default', ($this->required() ? 'now' : false)); + $timeDefault = a::get($this->time, 'default', ($this->required() ? 'now' : false)); + + $dateValue = $timestamp ? date('Y-m-d', $timestamp) : $dateDefault; + $timeValue = $timestamp ? date('H:i', $timestamp) : $timeDefault; + + $date = form::field('date', array( + 'name' => $this->name() . '[date]', + 'value' => $dateValue, + 'format' => a::get($this->date, 'format', 'YYYY-MM-DD'), + 'id' => 'form-field-' . $this->name() . '-date', + 'required' => $this->required(), + 'readonly' => $this->readonly(), + )); + + $time = form::field('time', array( + 'name' => $this->name() . '[time]', + 'value' => $timeValue, + 'format' => a::get($this->time, 'format', 24), + 'interval' => a::get($this->time, 'interval', 60), + 'id' => 'form-field-' . $this->name() . '-time', + 'required' => $this->required(), + 'readonly' => $this->readonly(), + )); + + $grid = '
'; + $grid .= '
' . $date->content() . '
'; + $grid .= '
' . $time->content() . '
'; + $grid .= '
'; + + return $grid; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/email/email.php b/panel/app/fields/email/email.php new file mode 100644 index 0000000..aa112ed --- /dev/null +++ b/panel/app/fields/email/email.php @@ -0,0 +1,35 @@ +type = 'email'; + $this->icon = 'envelope'; + $this->label = l::get('fields.email.label', 'Email'); + $this->placeholder = l::get('fields.email.placeholder', 'mail@example.com'); + $this->autocomplete = true; + + } + + public function input() { + + $input = parent::input(); + + if($this->autocomplete) { + $input->attr('autocomplete', 'off'); + $input->data(array( + 'field' => 'autocomplete', + 'url' => panel()->urls()->api() . '/autocomplete/emails?_csrf=' . panel()->csrf() + )); + } + + return $input; + + } + + public function validate() { + return v::email($this->result()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/filename/filename.php b/panel/app/fields/filename/filename.php new file mode 100644 index 0000000..92974b3 --- /dev/null +++ b/panel/app/fields/filename/filename.php @@ -0,0 +1,18 @@ +addClass('field-icon'); + $icon->append('.' . $this->extension . ''); + + return $icon; + + } + +} diff --git a/panel/app/fields/headline/assets/css/headline.css b/panel/app/fields/headline/assets/css/headline.css new file mode 100644 index 0000000..00c0160 --- /dev/null +++ b/panel/app/fields/headline/assets/css/headline.css @@ -0,0 +1,19 @@ +.field-with-headline { + counter-increment: count; +} +.field-with-headline:first-child { + padding-top: 0; +} +.field-with-headline { + padding-top: 6em; +} +.field-with-headline .hgroup span { + padding-left: 1.5em; +} +.field-with-headline .hgroup:before { + position: absolute; + content: counter(count, decimal-leading-zero); + left: 0; + color: #8dae28; + font-weight: 400; +} diff --git a/panel/app/fields/headline/headline.php b/panel/app/fields/headline/headline.php new file mode 100644 index 0000000..4b9126b --- /dev/null +++ b/panel/app/fields/headline/headline.php @@ -0,0 +1,29 @@ + array( + 'headline.css' + ) + ); + + public function result() { + return null; + } + + public function label() { + return null; + } + + public function content() { + return '

' . html($this->i18n($this->label)) . '

'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-headline'); + return $element; + } + +} diff --git a/panel/app/fields/hidden/hidden.php b/panel/app/fields/hidden/hidden.php new file mode 100644 index 0000000..33d61d7 --- /dev/null +++ b/panel/app/fields/hidden/hidden.php @@ -0,0 +1,13 @@ + 'hidden', + 'name' => $this->name(), + 'value' => $this->value() + )); + } + +} \ No newline at end of file diff --git a/panel/app/fields/image/assets/css/image.css b/panel/app/fields/image/assets/css/image.css new file mode 100644 index 0000000..9f01aba --- /dev/null +++ b/panel/app/fields/image/assets/css/image.css @@ -0,0 +1,19 @@ +.field-with-image select { + margin-left: 3rem; +} +.field-with-image .input-preview { + position: absolute; + top: 2px; + left: 2px; + bottom: 2px; + width: 2.75em; + background: url(../images/pattern.png); +} +.field-with-image .input-preview figure { + display: block; + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-position: center center; + background-size: cover; +} \ No newline at end of file diff --git a/panel/app/fields/image/assets/js/image.js b/panel/app/fields/image/assets/js/image.js new file mode 100644 index 0000000..8bf882b --- /dev/null +++ b/panel/app/fields/image/assets/js/image.js @@ -0,0 +1,55 @@ +(function($) { + + $.fn.imagefield = function() { + + return this.each(function() { + + var field = $(this); + + // avoid multiple init + if(field.data('imagefield')) return true; + field.data('imagefield', true); + + var select = field.find('select'); + var preview = field.find('.input-preview figure'); + var link = preview.parent('a'); + + select.on('keydown change', function() { + + var option = select.find('option:selected'); + var url = option.data('url'); + var thumb = option.data('thumb'); + + if(option.val() === '') { + url = '#'; + } + + if(thumb) { + preview.attr('style', 'background-image: url(' + thumb + ')'); + } else { + preview.attr('style', 'background-image: none'); + } + + link.attr('href', url); + + }).trigger('change'); + + field.find('.input-preview').on('click', function() { + if($(this).attr('href') == '#') { + return false; + } + }); + + field.find('.input').droppable({ + hoverClass: 'over', + accept: $('.sidebar .draggable-file'), + drop: function(e, ui) { + $(this).find('select').val(ui.draggable.data('helper')).trigger('change'); + } + }); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/image/image.php b/panel/app/fields/image/image.php new file mode 100644 index 0000000..400aeb7 --- /dev/null +++ b/panel/app/fields/image/image.php @@ -0,0 +1,94 @@ +icon = 'image'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-image'); + $element->data('field', 'imagefield'); + return $element; + } + + public function image() { + return $this->page->image($this->value()); + } + + public function preview() { + + $figure = new Brick('figure'); + + if($image = $this->image()) { + $figure->attr('style', 'background-image: url(' . $image->crop(75)->url() . ')'); + $url = $image->url('edit'); + } else { + $figure->attr('style', 'background-image: url(' . $this->value() . ')'); + $url = ''; + } + + return '
' . $figure . ''; + + } + + public function input() { + return $this->preview() . parent::input(); + } + + public function option($filename, $image, $selected = false) { + + if($image == '') { + return new Brick('option', '', array( + 'value' => '', + 'selected' => $selected + )); + } else { + return new Brick('option', $image->filename(), array( + 'value' => $filename, + 'selected' => $selected, + 'data-url' => $image->url('edit'), + 'data-thumb' => $image->crop(75)->url() + )); + } + + } + + public function options() { + + $options = []; + + foreach($this->images() as $image) { + $options[$image->filename()] = $image; + } + + return $options; + + } + + public function images() { + + $images = $this->page->images(); + + if(!empty($this->extension)) { + + if(!is_array($this->extension)) { + $extensions = [$this->extension]; + } else { + $extensions = $this->extension; + } + + $images = $images->filter(function($image) use($extensions) { + return in_array(strtolower($image->extension()), $extensions); + }); + + } + + return $images; + + } + +} diff --git a/panel/app/fields/info/info.php b/panel/app/fields/info/info.php new file mode 100644 index 0000000..3f13ff3 --- /dev/null +++ b/panel/app/fields/info/info.php @@ -0,0 +1,21 @@ +addClass('field-with-icon'); + return $element; + } + + public function input() { + return '
' . kirbytext($this->i18n($this->text())) . '
'; + } + +} \ No newline at end of file diff --git a/panel/app/fields/input/input.php b/panel/app/fields/input/input.php new file mode 100644 index 0000000..73a8ce2 --- /dev/null +++ b/panel/app/fields/input/input.php @@ -0,0 +1,37 @@ +addClass('input'); + $input->attr(array( + 'type' => $this->type(), + 'value' => '', + 'required' => $this->required(), + 'name' => $this->name(), + 'autocomplete' => $this->autocomplete() === false ? 'off' : 'on', + 'autofocus' => $this->autofocus(), + 'placeholder' => $this->i18n($this->placeholder()), + 'readonly' => $this->readonly(), + 'disabled' => $this->disabled(), + 'id' => $this->id() + )); + + if(!is_array($this->value())) { + $input->val(html($this->value(), false)); + } + + if($this->readonly()) { + $input->attr('tabindex', '-1'); + $input->addClass('input-is-readonly'); + } + + return $input; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/inputlist/inputlist.php b/panel/app/fields/inputlist/inputlist.php new file mode 100644 index 0000000..9ca8dc9 --- /dev/null +++ b/panel/app/fields/inputlist/inputlist.php @@ -0,0 +1,83 @@ +removeClass('input'); + return $input; + } + + public function options() { + return fieldoptions::build($this); + } + + public function item($value, $text) { + + $input = $this->input($value); + + $label = new Brick('label', $this->i18n($text)); + $label->addClass('input'); + $label->attr('data-focus', 'true'); + $label->prepend($input); + + if($this->readonly) { + $label->addClass('input-is-readonly'); + } + + return $label; + + } + + public function content() { + + $html = '
    '; + + switch($this->columns()) { + case 2: + $width = ' field-grid-item-1-2'; + break; + case 3: + $width = ' field-grid-item-1-3'; + break; + case 4: + $width = ' field-grid-item-1-4'; + break; + case 5: + $width = ' field-grid-item-1-5'; + break; + default: + $width = ''; + break; + } + + foreach($this->options() as $key => $value) { + $html .= '
  • '; + $html .= $this->item($key, $value); + $html .= '
  • '; + } + + $html .= '
'; + + $content = new Brick('div'); + $content->addClass('field-content'); + $content->append($html); + + return $content; + + } + + public function validate() { + if(is_array($this->value())) { + foreach($this->value() as $v) { + if(!array_key_exists($v, $this->options())) return false; + } + return true; + } else { + return array_key_exists($this->value(), $this->options()); + } + } + +} diff --git a/panel/app/fields/line/line.php b/panel/app/fields/line/line.php new file mode 100644 index 0000000..b789b47 --- /dev/null +++ b/panel/app/fields/line/line.php @@ -0,0 +1,19 @@ +'; + } + + public function element() { + $element = parent::element(); + $element->addClass('field-with-line'); + return $element; + } + +} \ No newline at end of file diff --git a/panel/app/fields/number/number.php b/panel/app/fields/number/number.php new file mode 100644 index 0000000..2e8c838 --- /dev/null +++ b/panel/app/fields/number/number.php @@ -0,0 +1,39 @@ +type = 'number'; + $this->label = l::get('fields.number.label', 'Number'); + $this->placeholder = l::get('fields.number.placeholder', '#'); + $this->step = 1; + $this->min = false; + $this->max = false; + + } + + public function input() { + $input = parent::input(); + $input->attr('step', $this->step); + $input->attr('min', $this->min); + $input->attr('max', $this->max); + return $input; + } + + public function validate() { + + if(!v::num($this->result())) return false; + + if($this->validate and is_array($this->validate)) { + return parent::validate(); + } else { + if(is_numeric($this->min) and !v::min($this->result(), $this->min)) return false; + if(is_numeric($this->max) and !v::max($this->result(), $this->max)) return false; + } + + return true; + + } + +} diff --git a/panel/app/fields/page/page.php b/panel/app/fields/page/page.php new file mode 100644 index 0000000..00435bf --- /dev/null +++ b/panel/app/fields/page/page.php @@ -0,0 +1,25 @@ +icon = 'chain'; + $this->label = l::get('fields.page.label', 'Page'); + $this->placeholder = l::get('fields.page.placeholder', 'path/to/page'); + + } + + public function input() { + + $input = parent::input(); + $input->data(array( + 'field' => 'autocomplete', + 'url' => panel()->urls()->api() . '/autocomplete/uris' + )); + + return $input; + + } + +} \ No newline at end of file diff --git a/panel/app/fields/password/password.php b/panel/app/fields/password/password.php new file mode 100644 index 0000000..9ed257a --- /dev/null +++ b/panel/app/fields/password/password.php @@ -0,0 +1,40 @@ +type = 'password'; + $this->icon = 'key'; + $this->label = l('fields.password.label', 'Password'); + + } + + public function input() { + + $input = parent::input(); + + if($this->suggestion) { + $input->data(array( + 'field' => 'passwordSuggestion' + )); + } + + return $input; + + } + + public function help() { + if($this->suggestion and !$this->readonly) { + $this->help = $this->suggestion(); + } + return parent::help(); + } + + public function suggestion() { + return ''; + } + +} \ No newline at end of file diff --git a/panel/app/fields/radio/radio.php b/panel/app/fields/radio/radio.php new file mode 100644 index 0000000..39c4d0c --- /dev/null +++ b/panel/app/fields/radio/radio.php @@ -0,0 +1,41 @@ +options(); + if(is_array($options)) { + reset($options); + $value = key($options); + } + } + return $value; + } + + public function input() { + + $val = func_get_arg(0); + $input = parent::input(); + $input->addClass('radio'); + $input->attr('type', 'radio'); + $input->val($val); + + if($this->readonly) { + $input->attr('disabled', true); + } + + $input->attr('checked', $val == $this->value()); + return $input; + + } + + public function item($value, $text) { + $item = parent::item($value, $text); + $item->addClass('input-with-radio'); + return $item; + } + +} diff --git a/panel/app/fields/select/select.php b/panel/app/fields/select/select.php new file mode 100644 index 0000000..b1403b4 --- /dev/null +++ b/panel/app/fields/select/select.php @@ -0,0 +1,74 @@ +type = 'select'; + $this->options = array(); + $this->icon = 'chevron-down'; + + } + + public function options() { + return FieldOptions::build($this); + } + + public function option($value, $text, $selected = false) { + return new Brick('option', $this->i18n($text), array( + 'value' => $value, + 'selected' => $selected + )); + } + + public function input() { + + $select = new Brick('select'); + $select->addClass('selectbox'); + $select->attr(array( + 'name' => $this->name(), + 'id' => $this->id(), + 'required' => $this->required(), + 'autocomplete' => $this->autocomplete(), + 'autofocus' => $this->autofocus(), + 'readonly' => $this->readonly(), + 'disabled' => $this->disabled(), + )); + + $default = $this->default(); + + if(!$this->required()) { + $select->append($this->option('', '', $this->value() == '')); + } + + if($this->readonly()) { + $select->attr('tabindex', '-1'); + } + + foreach($this->options() as $value => $text) { + $select->append($this->option($value, $text, $this->value() == $value)); + } + + $inner = new Brick('div'); + $inner->addClass('selectbox-wrapper'); + $inner->append($select); + + $wrapper = new Brick('div'); + $wrapper->addClass('input input-with-selectbox'); + $wrapper->append($inner); + + if($this->readonly()) { + $wrapper->addClass('input-is-readonly'); + } else { + $wrapper->attr('data-focus', 'true'); + } + + return $wrapper; + + } + + public function validate() { + return array_key_exists($this->value(), $this->options()); + } + +} diff --git a/panel/app/fields/structure/assets/css/structure.css b/panel/app/fields/structure/assets/css/structure.css new file mode 100644 index 0000000..ab59695 --- /dev/null +++ b/panel/app/fields/structure/assets/css/structure.css @@ -0,0 +1,97 @@ +.structure { + padding-bottom: .5em; +} +.structure-entry { + background: #fff; + border: 2px solid #ddd; + margin-bottom: .5em; +} +.structure-readonly .structure-entry { + background: #efefef; + color: #777; +} +.structure-entry:last-child { + margin-bottom: 0; +} +.structure-entry-content { + padding: 1em 1.5em; + border-bottom: 1px solid #efefef; +} +.structure[data-sortable=true] .structure-entry-content { + cursor: move; +} +.structure-entry-options .btn { + padding: .75em 1.5em; + width: 50%; + float: left; + border-right: 1px solid #efefef; +} +.structure-empty { + padding: 1.5em; + background: #ddd; +} +.fileview-sidebar .structure-empty { + background: none; + border-radius: 5px; + border: 1px dashed #ddd; + padding: 1rem 1.5rem 1.25rem; +} +.structure-empty a { + border-bottom: 2px solid #aaa; + margin-left: .5em; +} +.fileview-sidebar .structure-empty a { + display: inline-block; + margin-left: 0; +} +.structure-empty a:hover { + border-color: #000; +} +.structure-add-button { + cursor: pointer; +} + + +/* Table */ +.structure-table { + width: 100%; + border-spacing: 0; + border: 2px solid #ddd; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + table-layout: fixed; +} +.structure-table td, .structure-table th { + background: #fff; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + text-align: left; + vertical-align: top; +} +.structure-table th { + padding: .5em; + font-weight: 400; + color: #777; + font-style: italic; +} +.structure-table td a { + display: block; + padding: .5em; + overflow: hidden; + width: 100%; + text-overflow: ellipsis; + cursor: move; +} +.structure-table-options { + width: 3rem; + text-align: center; +} +.structure-table .structure-table-options a { + text-align: center; + cursor: pointer; +} + +.structure-sortable-helper { + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} diff --git a/panel/app/fields/structure/assets/js/structure.js b/panel/app/fields/structure/assets/js/structure.js new file mode 100644 index 0000000..dd6559e --- /dev/null +++ b/panel/app/fields/structure/assets/js/structure.js @@ -0,0 +1,53 @@ +(function($) { + + var Structure = function(el) { + + var element = $(el); + var style = element.data('style'); + var api = element.data('api'); + var sortable = element.data('sortable'); + var entries = style == 'table' ? element.find('.structure-table tbody') : element.find('.structure-entries'); + + if(sortable === false) return false; + + entries.sortable({ + helper: function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui.addClass('structure-sortable-helper'); + }, + update: function() { + + var ids = []; + + $.each($(this).sortable('toArray'), function(i, id) { + ids.push(id.replace('structure-entry-', '')); + }); + + $.post(api, {ids: ids}, function() { + app.content.reload(); + }); + + } + }); + + }; + + $.fn.structure = function() { + + return this.each(function() { + + if($(this).data('structure')) { + return $(this); + } else { + var structure = new Structure(this); + $(this).data('structure', structure); + return $(this); + } + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/structure/controller.php b/panel/app/fields/structure/controller.php new file mode 100644 index 0000000..c38ab73 --- /dev/null +++ b/panel/app/fields/structure/controller.php @@ -0,0 +1,93 @@ +model(); + $structure = $this->structure($model); + $modalsize = $this->field()->modalsize(); + $form = $this->form('add', array($model, $structure), function($form) use($model, $structure, $self) { + + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $structure->add($form->serialize()); + $self->redirect($model); + + }); + + return $this->modal('add', compact('form', 'modalsize')); + + } + + public function update($entryId) { + + $self = $this; + $model = $this->model(); + $structure = $this->structure($model); + $entry = $structure->find($entryId); + + if(!$entry) { + return $this->modal('error', array( + 'text' => l('fields.structure.entry.error') + )); + } + + $modalsize = $this->field()->modalsize(); + $form = $this->form('update', array($model, $structure, $entry), function($form) use($model, $structure, $self, $entryId) { + + // run the form validator + $form->validate(); + + if(!$form->isValid()) { + return false; + } + + $structure->update($entryId, $form->serialize()); + $self->redirect($model); + + }); + + return $this->modal('update', compact('form', 'modalsize')); + + } + + public function delete($entryId) { + + $self = $this; + $model = $this->model(); + $structure = $this->structure($model); + $entry = $structure->find($entryId); + + if(!$entry) { + return $this->modal('error', array( + 'text' => l('fields.structure.entry.error') + )); + } + + $form = $this->form('delete', $model, function() use($self, $model, $structure, $entryId) { + $structure->delete($entryId); + $self->redirect($model); + }); + + return $this->modal('delete', compact('form')); + + } + + public function sort() { + $model = $this->model(); + $structure = $this->structure($model); + $structure->sort(get('ids')); + $this->redirect($model); + } + + protected function structure($model) { + return $model->structure()->forField($this->fieldname()); + } + +} \ No newline at end of file diff --git a/panel/app/fields/structure/forms/add.php b/panel/app/fields/structure/forms/add.php new file mode 100644 index 0000000..0e1b7b4 --- /dev/null +++ b/panel/app/fields/structure/forms/add.php @@ -0,0 +1,11 @@ +fields(), array(), $structure->field()); + $form->cancel($model); + $form->buttons->submit->value = l('add'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/forms/delete.php b/panel/app/fields/structure/forms/delete.php new file mode 100644 index 0000000..6edc57e --- /dev/null +++ b/panel/app/fields/structure/forms/delete.php @@ -0,0 +1,17 @@ + array( + 'label' => 'fields.structure.delete.label', + 'type' => 'info', + ) + )); + + $form->style('delete'); + $form->cancel($model); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/forms/update.php b/panel/app/fields/structure/forms/update.php new file mode 100644 index 0000000..cef432a --- /dev/null +++ b/panel/app/fields/structure/forms/update.php @@ -0,0 +1,12 @@ +fields(), $entry->toArray(), $structure->field()); + + $form->cancel($model); + $form->buttons->submit->value = l('ok'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/structure/structure.php b/panel/app/fields/structure/structure.php new file mode 100644 index 0000000..659d99e --- /dev/null +++ b/panel/app/fields/structure/structure.php @@ -0,0 +1,162 @@ + array( + 'structure.js' + ), + 'css' => array( + 'structure.css' + ) + ); + + public $fields = array(); + public $entry = null; + public $structure = null; + public $style = 'items'; + public $modalsize = 'medium'; + + public function routes() { + + return array( + array( + 'pattern' => 'add', + 'method' => 'get|post', + 'action' => 'add' + ), + array( + 'pattern' => 'sort', + 'method' => 'post', + 'action' => 'sort', + ), + array( + 'pattern' => '(:any)/update', + 'method' => 'get|post', + 'action' => 'update' + ), + array( + 'pattern' => '(:any)/delete', + 'method' => 'get|post', + 'action' => 'delete', + ) + ); + } + + public function modalsize() { + $sizes = array('small', 'medium', 'large'); + return in_array($this->modalsize, $sizes) ? $this->modalsize : 'medium'; + } + + public function style() { + $styles = array('table', 'items'); + return in_array($this->style, $styles) ? $this->style : 'items'; + } + + public function structure() { + if(!is_null($this->structure)) { + return $this->structure; + } else { + return $this->structure = $this->model->structure()->forField($this->name); + } + } + + public function fields() { + + $output = array(); + + foreach($this->structure->fields() as $k => $v) { + $v['name'] = $k; + $v['value'] = '{{' . $k . '}}'; + $output[] = $v; + } + + return $output; + + } + + public function entries() { + return $this->structure()->data(); + } + + public function result() { + /** + * Users store their data as plain yaml. + * So we need this hacky solution to give data + * as an array to the form serializer in case + * of users, in order to not mess up their data + */ + if(is_a($this->model, 'Kirby\\Panel\\Models\\User')) { + return $this->structure()->toArray(); + } else { + return $this->structure()->toYaml(); + } + } + + public function entry($data) { + + if(is_null($this->entry) or !is_string($this->entry)) { + $html = array(); + foreach($this->fields as $name => $field) { + if(isset($data->$name)) { + $html[] = $data->$name; + } + } + return implode('
', $html); + } else { + + $text = $this->entry; + + foreach((array)$data as $key => $value) { + if(is_array($value)) { + $value = implode(', ', array_values($value)); + } + $text = str_replace('{{' . $key . '}}', $value, $text); + } + + return $text; + + } + + } + + public function label() { + return null; + } + + public function headline() { + + if(!$this->readonly) { + + $add = new Brick('a'); + $add->html('' . l('fields.structure.add')); + $add->addClass('structure-add-button label-option'); + $add->data('modal', true); + $add->attr('href', purl($this->model, 'field/' . $this->name . '/structure/add')); + + } else { + $add = null; + } + + // make sure there's at least an empty label + if(!$this->label) { + $this->label = ' '; + } + + $label = parent::label(); + $label->addClass('structure-label'); + $label->append($add); + + return $label; + + } + + public function content() { + return tpl::load(__DIR__ . DS . 'template.php', array('field' => $this)); + } + + public function url($action) { + return purl($this->model(), 'field/' . $this->name() . '/structure/' . $action); + } + +} \ No newline at end of file diff --git a/panel/app/fields/structure/styles/items.php b/panel/app/fields/structure/styles/items.php new file mode 100644 index 0000000..16bb511 --- /dev/null +++ b/panel/app/fields/structure/styles/items.php @@ -0,0 +1,18 @@ +entries() as $entry): ?> +
+
+ entry($entry) ?> +
+ readonly()): ?> + + +
+ \ No newline at end of file diff --git a/panel/app/fields/structure/styles/table.php b/panel/app/fields/structure/styles/table.php new file mode 100644 index 0000000..1d0aaa9 --- /dev/null +++ b/panel/app/fields/structure/styles/table.php @@ -0,0 +1,36 @@ + + + + fields() as $f): ?> + + + + + + + entries() as $entry): ?> + + fields() as $f): ?> + + + + + + +
+ i18n($f['label']), false) ?> + +   +
+ + {$f['name']})): ?> + {$f['name']}, false) ?> + +   + + + + + + +
\ No newline at end of file diff --git a/panel/app/fields/structure/template.php b/panel/app/fields/structure/template.php new file mode 100644 index 0000000..233c947 --- /dev/null +++ b/panel/app/fields/structure/template.php @@ -0,0 +1,20 @@ +
+ + headline() ?> + +
+ + entries()->count()): ?> +
+ +
+ + style() . '.php') ?> + +
+ +
\ No newline at end of file diff --git a/panel/app/fields/structure/views/add.php b/panel/app/fields/structure/views/add.php new file mode 100644 index 0000000..28805da --- /dev/null +++ b/panel/app/fields/structure/views/add.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/structure/views/delete.php b/panel/app/fields/structure/views/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/fields/structure/views/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/structure/views/update.php b/panel/app/fields/structure/views/update.php new file mode 100644 index 0000000..28805da --- /dev/null +++ b/panel/app/fields/structure/views/update.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/fields/tags/tags.php b/panel/app/fields/tags/tags.php new file mode 100644 index 0000000..d59cda9 --- /dev/null +++ b/panel/app/fields/tags/tags.php @@ -0,0 +1,52 @@ +icon = 'tag'; + $this->label = l::get('fields.tags.label', 'Tags'); + $this->index = 'siblings'; + $this->separator = ','; + $this->lower = false; + + } + + public function input() { + + $input = parent::input(); + $input->addClass('input-with-tags'); + $input->data(array( + 'field' => 'tags', + 'lowercase' => $this->lower ? 'true' : false, + 'separator' => $this->separator, + )); + + if(isset($this->data)) { + + $input->data('url', html(json_encode($this->data), false)); + + } else if($page = $this->page()) { + + $field = empty($this->field) ? $this->name() : $this->field; + $model = is_a($this->model, 'File') ? 'file' : 'page'; + + $query = array( + 'uri' => $page->id(), + 'index' => $this->index(), + 'field' => $field, + 'yaml' => $this->parentField, + 'model' => $model, + 'separator' => $this->separator(), + '_csrf' => panel()->csrf(), + ); + + $input->data('url', panel()->urls()->api() . '/autocomplete/field?' . http_build_query($query)); + + } + + return $input; + + } + +} diff --git a/panel/app/fields/tel/tel.php b/panel/app/fields/tel/tel.php new file mode 100644 index 0000000..61814a2 --- /dev/null +++ b/panel/app/fields/tel/tel.php @@ -0,0 +1,11 @@ +type = 'tel'; + $this->icon = 'phone'; + $this->label = l::get('fields.tel.label', 'Phone'); + } + +} \ No newline at end of file diff --git a/panel/app/fields/text/assets/css/counter.css b/panel/app/fields/text/assets/css/counter.css new file mode 100644 index 0000000..c4b0990 --- /dev/null +++ b/panel/app/fields/text/assets/css/counter.css @@ -0,0 +1,12 @@ +.field-counter { + position: absolute; + z-index: -1; + right: 0; + top: 0; + text-align: right; + font-size: .9em; + line-height: 1.66666666666667; +} +.field-counter.outside-range { + color: #b3000a; +} \ No newline at end of file diff --git a/panel/app/fields/text/assets/js/counter.js b/panel/app/fields/text/assets/js/counter.js new file mode 100644 index 0000000..2ef5458 --- /dev/null +++ b/panel/app/fields/text/assets/js/counter.js @@ -0,0 +1,34 @@ +(function($) { + + $.fn.counter = function() { + + return this.each(function() { + + var counter = $(this); + + if(counter.data('counter')) { + return counter; + } + + var field = counter.parent('.field').find('.input'); + var length = $.trim(field.val()).length; + var max = field.data('max'); + var min = field.data('min'); + + field.keyup(function() { + length = $.trim(field.val()).length; + counter.text(length + (max ? '/' + max : '')); + if((max && length > max) || (min && length < min)) { + counter.addClass('outside-range'); + } else { + counter.removeClass('outside-range'); + } + }).trigger('keyup'); + + counter.data('counter', true); + + }); + + }; + +}(jQuery)); \ No newline at end of file diff --git a/panel/app/fields/text/text.php b/panel/app/fields/text/text.php new file mode 100644 index 0000000..8150a7f --- /dev/null +++ b/panel/app/fields/text/text.php @@ -0,0 +1,76 @@ + 0, + 'max' => null + ); + + static public $assets = array( + 'js' => array( + 'counter.js' + ) + ); + + public function min() { + return isset($this->validate['min']) ? $this->validate['min'] : false; + } + + public function max() { + return isset($this->validate['max']) ? $this->validate['max'] : false; + } + + public function input() { + + $input = parent::input(); + + if(!$this->readonly() && ($this->min() || $this->max())) { + $input->data('max', $this->max())->data('min', $this->min()); + } + + return $input; + + } + + public function outsideRange($length) { + + if($this->min() && $length < $this->min()) return true; + if($this->max() && $length > $this->max()) return true; + + return false; + + } + + public function counter() { + + if(!$this->min() && !$this->max() || $this->readonly()) return null; + + $counter = new Brick('div'); + $counter->addClass('field-counter marginalia text'); + + $length = str::length($this->value()); + + if($this->outsideRange($length)) { + $counter->addClass('outside-range'); + } + + $counter->data('field', 'counter'); + $counter->html($length . ($this->max() ? '/' . $this->max() : '')); + + return $counter; + + } + + public function template() { + + return $this->element() + ->append($this->label()) + ->append($this->content()) + ->append($this->counter()) + ->append($this->help()); + + } + +} diff --git a/panel/app/fields/textarea/assets/js/editor.js b/panel/app/fields/textarea/assets/js/editor.js new file mode 100644 index 0000000..9f95dc1 --- /dev/null +++ b/panel/app/fields/textarea/assets/js/editor.js @@ -0,0 +1,64 @@ +(function($) { + + $.fn.editor = function() { + + return this.each(function() { + + if($(this).data('editor')) { + return $(this); + } + + var textarea = $(this); + var buttons = textarea.parent().find('.field-buttons'); + + // start autosizing + textarea.autosize(); + + buttons.find('.btn').on('click.editorButton', function(e) { + + textarea.focus(); + var button = $(this); + + if(button.data('action')) { + app.modal.open(button.data('action'), window.location.href); + } else { + + var sel = textarea.getSelection(); + var tpl = button.data('tpl'); + var text = button.data('text'); + + if(sel.length > 0) text = sel; + + var tag = tpl.replace('{text}', text); + + textarea.insertAtCursor(tag); + textarea.trigger('autosize.resize'); + + } + + return false; + + }); + + buttons.find('[data-editor-shortcut]').each(function(i, el) { + var key = $(this).data('editor-shortcut'); + var action = function(e) { + $(el).trigger('click'); + return false; + }; + + textarea.bind('keydown', key, action); + + if(key.match(/meta\+/)) { + textarea.bind('keydown', key.replace('meta+', 'ctrl+'), action); + } + + }); + + textarea.data('editor', true); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/textarea/buttons.php b/panel/app/fields/textarea/buttons.php new file mode 100644 index 0000000..c6750d8 --- /dev/null +++ b/panel/app/fields/textarea/buttons.php @@ -0,0 +1,90 @@ +textarea = $textarea; + + if(!is_array($buttons)) { + $this->buttons = array_keys(static::$setup); + } else { + $this->buttons = $buttons; + } + + } + + public function __toString() { + + $html = ''; + + return $html; + + } + +} + +buttons::$setup = array( + 'bold' => array( + 'label' => l::get('fields.textarea.buttons.bold.label'), + 'text' => l::get('fields.textarea.buttons.bold.text'), + 'shortcut' => 'meta+b', + 'template' => '**{text}**', + 'icon' => 'bold' + ), + 'italic' => array( + 'label' => l::get('fields.textarea.buttons.italic.label'), + 'text' => l::get('fields.textarea.buttons.italic.text'), + 'shortcut' => 'meta+i', + 'template' => '*{text}*', + 'icon' => 'italic' + ), + 'link' => array( + 'label' => l::get('fields.textarea.buttons.link.label'), + 'shortcut' => 'meta+shift+l', + 'action' => 'link', + 'icon' => 'chain' + ), + 'email' => array( + 'label' => l::get('fields.textarea.buttons.email.label'), + 'shortcut' => 'meta+shift+e', + 'action' => 'email', + 'icon' => 'envelope' + ), +); \ No newline at end of file diff --git a/panel/app/fields/textarea/controller.php b/panel/app/fields/textarea/controller.php new file mode 100644 index 0000000..2d3d21b --- /dev/null +++ b/panel/app/fields/textarea/controller.php @@ -0,0 +1,23 @@ +model(); + $form = $this->form('link', array($page, $this->fieldname())); + + return $this->modal('link', compact('form')); + + } + + public function email($textarea = null) { + + $page = $this->model(); + $form = $this->form('email', array($page, $this->fieldname())); + + return $this->modal('email', compact('form')); + + } + +} \ No newline at end of file diff --git a/panel/app/fields/textarea/forms/email.php b/panel/app/fields/textarea/forms/email.php new file mode 100644 index 0000000..f1e3f3c --- /dev/null +++ b/panel/app/fields/textarea/forms/email.php @@ -0,0 +1,27 @@ + array( + 'label' => 'editor.email.address.label', + 'type' => 'email', + 'placeholder' => 'editor.email.address.placeholder', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.email.text.label', + 'type' => 'text', + 'help' => 'editor.email.text.help', + 'icon' => 'font' + ) + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/textarea/forms/link.php b/panel/app/fields/textarea/forms/link.php new file mode 100644 index 0000000..27feca6 --- /dev/null +++ b/panel/app/fields/textarea/forms/link.php @@ -0,0 +1,28 @@ + array( + 'label' => 'editor.link.url.label', + 'type' => 'text', + 'placeholder' => 'http://', + 'autofocus' => 'true', + 'required' => 'true', + 'icon' => 'chain' + ), + 'text' => array( + 'label' => 'editor.link.text.label', + 'type' => 'text', + 'help' => 'editor.link.text.help', + 'icon' => 'font' + ), + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/fields/textarea/textarea.php b/panel/app/fields/textarea/textarea.php new file mode 100644 index 0000000..c751b8d --- /dev/null +++ b/panel/app/fields/textarea/textarea.php @@ -0,0 +1,94 @@ + array( + 'editor.js' + ) + ); + + public function __construct() { + $this->label = l::get('fields.textarea.label', 'Text'); + $this->buttons = true; + $this->min = 0; + $this->max = false; + } + + public function routes() { + return array( + array( + 'pattern' => 'link', + 'action' => 'link', + 'method' => 'get|post' + ), + array( + 'pattern' => 'email', + 'action' => 'email', + 'method' => 'get|post' + ), + ); + } + + public function input() { + + $input = parent::input(); + $input->tag('textarea'); + $input->removeAttr('type'); + $input->removeAttr('value'); + $input->html($this->value() ? htmlentities($this->value(), ENT_NOQUOTES, 'UTF-8') : false); + $input->data('field', 'editor'); + + return $input; + + } + + public function result() { + // Convert all line-endings to UNIX format + return str_replace(array("\r\n", "\r"), "\n", parent::result()); + } + + public function element() { + + $element = parent::element(); + $element->addClass('field-with-textarea'); + + if($this->buttons and !$this->readonly) { + $element->addClass('field-with-buttons'); + } + + return $element; + + } + + public function content() { + + $content = parent::content(); + + if($this->buttons and !$this->readonly) { + $content->append($this->buttons()); + } + + return $content; + + } + + public function buttons() { + require_once(__DIR__ . DS . 'buttons.php'); + return new Buttons($this, $this->buttons); + } + + public function validate() { + + if($this->validate and is_array($this->validate)) { + return parent::validate(); + } else { + if($this->min and !v::min($this->result(), $this->min)) return false; + if($this->max and !v::max($this->result(), $this->max)) return false; + } + + return true; + + } + +} diff --git a/panel/app/fields/textarea/views/email.php b/panel/app/fields/textarea/views/email.php new file mode 100644 index 0000000..df5b0a1 --- /dev/null +++ b/panel/app/fields/textarea/views/email.php @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/panel/app/fields/textarea/views/link.php b/panel/app/fields/textarea/views/link.php new file mode 100644 index 0000000..a61317b --- /dev/null +++ b/panel/app/fields/textarea/views/link.php @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/panel/app/fields/time/time.php b/panel/app/fields/time/time.php new file mode 100644 index 0000000..84de37f --- /dev/null +++ b/panel/app/fields/time/time.php @@ -0,0 +1,69 @@ +icon = 'clock-o'; + $this->interval = 60; + $this->format = 24; + } + + public function interval() { + if($this->interval <= 0) { + $this->interval = 60; + } + return $this->interval; + } + + public function value() { + + if($this->override()) { + $value = $this->default(); + } else { + $value = parent::value(); + } + + if(!empty($value)) { + + if($value == 'now') { + $value = date($this->format(), time()); + } + + $time = round((strtotime($value) - strtotime('00:00')) / ($this->interval() * 60)) * ($this->interval() * 60) + strtotime('00:00'); + $value = date($this->format(), $time); + + } + + return $value; + + } + + public function format() { + return $this->format == 12 ? 'h:i A' : 'H:i'; + } + + public function options() { + + $time = strtotime('00:00'); + $end = strtotime('23:59'); + $options = array(); + $format = $this->format(); + + while($time < $end) { + + $now = date($format, $time); + $time += 60 * $this->interval(); + + $options[$now] = $now; + + } + + return $options; + + } + +} diff --git a/panel/app/fields/title/title.php b/panel/app/fields/title/title.php new file mode 100644 index 0000000..236da5b --- /dev/null +++ b/panel/app/fields/title/title.php @@ -0,0 +1,40 @@ +label = l::get('fields.title.label', 'Title'); + $this->icon = 'font'; + $this->required = true; + + } + + public function help() { + + if($this->page and !$this->page->isSite()) { + + if(!empty($this->help)) { + $this->help = $this->i18n($this->help); + $this->help .= '
'; + } + + // build a readable version of the page slug + $slug = ltrim($this->page->parent()->slug() . '/', '/') . $this->page->slug(); + + // TODO: move this to the css file + $style = 'padding-left: .5rem; color: #777; border:none'; + + if($this->page->canChangeUrl()) { + $this->help .= '→' . $slug . ''; + } else { + $this->help .= '→' . $slug . ''; + } + + } + + return parent::help(); + + } + +} diff --git a/panel/app/fields/toggle/toggle.php b/panel/app/fields/toggle/toggle.php new file mode 100644 index 0000000..411524d --- /dev/null +++ b/panel/app/fields/toggle/toggle.php @@ -0,0 +1,38 @@ +text())) { + case 'yes/no': + $true = l::get('fields.toggle.yes'); + $false = l::get('fields.toggle.no'); + break; + case 'on/off': + $true = l::get('fields.toggle.on'); + $false = l::get('fields.toggle.off'); + break; + } + + return array( + 'true' => $true, + 'false' => $false + ); + + } + + public function value() { + $value = parent::value(); + + if(in_array($value, array('yes', 'true', true, 1, 'on'), true)) { + return 'true'; + } else { + return 'false'; + } + + } + +} diff --git a/panel/app/fields/url/assets/js/url.js b/panel/app/fields/url/assets/js/url.js new file mode 100644 index 0000000..600a684 --- /dev/null +++ b/panel/app/fields/url/assets/js/url.js @@ -0,0 +1,38 @@ +(function($) { + + $.fn.urlfield = function() { + + return this.each(function() { + + var $this = $(this); + + if($this.data('urlfield')) { + return; + } else { + $this.data('urlfield', true); + } + + var $icon = $this.next('.field-icon'); + + $icon.css({ + 'cursor': 'pointer', + 'pointer-events': 'auto' + }); + + $icon.on('click', function() { + + var url = $.trim($this.val()); + + if(url !== '' && $this.is(':valid')) { + window.open(url); + } else { + $this.focus(); + } + + }); + + }); + + }; + +})(jQuery); \ No newline at end of file diff --git a/panel/app/fields/url/url.php b/panel/app/fields/url/url.php new file mode 100644 index 0000000..06b07f7 --- /dev/null +++ b/panel/app/fields/url/url.php @@ -0,0 +1,30 @@ + array( + 'url.js' + ) + ); + + public function __construct() { + + $this->type = 'url'; + $this->icon = 'chain'; + $this->label = l::get('fields.url.label', 'URL'); + $this->placeholder = 'http://'; + + } + + public function validate() { + return v::url($this->value()); + } + + public function input() { + $input = parent::input(); + $input->data('field', 'urlfield'); + return $input; + } + +} \ No newline at end of file diff --git a/panel/app/fields/user/user.php b/panel/app/fields/user/user.php new file mode 100644 index 0000000..1a215ec --- /dev/null +++ b/panel/app/fields/user/user.php @@ -0,0 +1,22 @@ +type = 'text'; + $this->icon = 'user'; + $this->label = l::get('fields.user.label', 'User'); + $this->options = array(); + + foreach(kirby()->site()->users() as $user) { + $this->options[$user->username()] = $user->username(); + } + + } + + public function value() { + $value = parent::value(); + return empty($value) ? site()->user()->username() : parent::value(); + } + +} diff --git a/panel/app/forms/auth/login.php b/panel/app/forms/auth/login.php new file mode 100644 index 0000000..04622f4 --- /dev/null +++ b/panel/app/forms/auth/login.php @@ -0,0 +1,30 @@ + array( + 'label' => 'login.username.label', + 'type' => 'text', + 'icon' => 'user', + 'required' => true, + 'autofocus' => true, + 'default' => s::get('username') + ), + 'password' => array( + 'label' => 'login.password.label', + 'type' => 'password', + 'required' => true + ) + )); + + $form->attr('autocomplete', 'off'); + $form->data('autosubmit', 'native'); + $form->style('centered'); + + $form->buttons->submit->value = l('login.button'); + + return $form; + +}; + diff --git a/panel/app/forms/avatars/delete.php b/panel/app/forms/avatars/delete.php new file mode 100644 index 0000000..946337f --- /dev/null +++ b/panel/app/forms/avatars/delete.php @@ -0,0 +1,19 @@ + array( + 'type' => 'info' + ) + )); + + $form->fields->image->text = '(image: ' . $avatar->url() . ' class: avatar avatar-full avatar-centered)'; + $form->centered = true; + $form->style('delete'); + + return $form; + +}; + + diff --git a/panel/app/forms/editor/email.php b/panel/app/forms/editor/email.php new file mode 100644 index 0000000..77d0ace --- /dev/null +++ b/panel/app/forms/editor/email.php @@ -0,0 +1,26 @@ + array( + 'label' => 'editor.email.address.label', + 'type' => 'email', + 'placeholder' => 'editor.email.address.placeholder', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.email.text.label', + 'type' => 'text', + 'help' => 'editor.email.text.help', + ) + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page, 'show'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/editor/link.php b/panel/app/forms/editor/link.php new file mode 100644 index 0000000..a2e5002 --- /dev/null +++ b/panel/app/forms/editor/link.php @@ -0,0 +1,26 @@ + array( + 'label' => 'editor.link.url.label', + 'type' => 'text', + 'placeholder' => 'http://', + 'autofocus' => 'true', + 'required' => 'true', + ), + 'text' => array( + 'label' => 'editor.link.text.label', + 'type' => 'text', + 'help' => 'editor.link.text.help', + ), + )); + + $form->data('textarea', 'form-field-' . $textarea); + $form->style('editor'); + $form->cancel($page, 'show'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/files/delete.php b/panel/app/forms/files/delete.php new file mode 100644 index 0000000..e67771e --- /dev/null +++ b/panel/app/forms/files/delete.php @@ -0,0 +1,20 @@ + array( + 'label' => 'files.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $file->filename() + ) + )); + + $form->style('delete'); + $form->cancel($file); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/files/edit.php b/panel/app/forms/files/edit.php new file mode 100644 index 0000000..120f512 --- /dev/null +++ b/panel/app/forms/files/edit.php @@ -0,0 +1,47 @@ +type(); + $info[] = $file->niceSize(); + + if((string)$file->dimensions() != '0 x 0') { + $info[] = $file->dimensions(); + } + + // setup the default fields + $fields = array( + '_name' => array( + 'label' => 'files.show.name.label', + 'type' => 'filename', + 'extension' => $file->extension(), + 'required' => true, + 'default' => $file->name(), + ), + '_info' => array( + 'label' => 'files.show.info.label', + 'type' => 'text', + 'readonly' => true, + 'icon' => 'info', + 'default' => implode(' / ', $info), + ), + '_link' => array( + 'label' => 'files.show.link.label', + 'type' => 'text', + 'readonly' => true, + 'icon' => 'chain', + 'default' => $file->url() + ) + ); + + $form = new Kirby\Panel\Form(array_merge($fields, $file->getFormFields()), $file->getFormData()); + + $form->centered = true; + $form->buttons->cancel = ''; + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/installation/check.php b/panel/app/forms/installation/check.php new file mode 100644 index 0000000..ca9ff52 --- /dev/null +++ b/panel/app/forms/installation/check.php @@ -0,0 +1,35 @@ + array( + 'type' => 'info' + ) + )); + + if(count($problems) > 1) { + $info = new Brick('ol'); + foreach($problems as $problem) { + $info->append('
  • ' . $problem . '
  • '); + } + } else { + $info = new Brick('p'); + foreach($problems as $problem) { + $info->append($problem); + } + } + + // add the list of problems to the info field + $form->fields->info->text = (string)$info; + + // setup the retry button + $form->buttons->submit->value = l('installation.check.retry'); + $form->buttons->submit->autofocus = true; + + $form->style('centered'); + $form->alert(l('installation.check.text')); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/installation/signup.php b/panel/app/forms/installation/signup.php new file mode 100644 index 0000000..681931d --- /dev/null +++ b/panel/app/forms/installation/signup.php @@ -0,0 +1,58 @@ +translations() as $translation) { + $translations[$translation->code()] = $translation->title(); + } + + $form = new Kirby\Panel\Form(array( + + 'username' => array( + 'label' => 'installation.signup.username.label', + 'type' => 'text', + 'icon' => 'user', + 'placeholder' => 'installation.signup.username.placeholder', + 'required' => true, + 'autocomplete' => false, + 'autofocus' => true, + ), + + 'password' => array( + 'label' => 'installation.signup.password.label', + 'type' => 'password', + 'required' => true, + 'autocomplete' => false, + 'suggestion' => true, + ), + + 'email' => array( + 'label' => 'installation.signup.email.label', + 'placeholder' => 'installation.signup.email.placeholder', + 'type' => 'email', + 'required' => true, + 'autocomplete' => false, + ), + + 'language' => array( + 'label' => 'installation.signup.language.label', + 'type' => 'select', + 'required' => true, + 'autocomplete' => false, + 'default' => kirby()->option('panel.language', 'en'), + 'options' => $translations + ) + + )); + + $form->attr('autocomplete', 'off'); + $form->data('autosubmit', 'native'); + $form->style('centered'); + + $form->buttons->submit->value = l('installation.signup.button'); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/add.php b/panel/app/forms/pages/add.php new file mode 100644 index 0000000..25a0490 --- /dev/null +++ b/panel/app/forms/pages/add.php @@ -0,0 +1,44 @@ +blueprint()->pages()->template() as $template) { + $options[$template->name()] = $template->title(); + } + + $form = new Kirby\Panel\Form(array( + 'title' => array( + 'label' => 'pages.add.title.label', + 'type' => 'title', + 'placeholder' => 'pages.add.title.placeholder', + 'autocomplete' => false, + 'autofocus' => true, + 'required' => true + ), + 'uid' => array( + 'label' => 'pages.add.url.label', + 'type' => 'text', + 'icon' => 'chain', + 'autocomplete' => false, + 'required' => true, + ), + 'template' => array( + 'label' => 'pages.add.template.label', + 'type' => 'select', + 'options' => $options, + 'default' => key($options), + 'required' => true, + 'readonly' => count($options) == 1 ? true : false, + 'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down', + ) + )); + + $form->cancel($page->isSite() ? '/' : $page); + + $form->buttons->submit->val(l('add')); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/delete.php b/panel/app/forms/pages/delete.php new file mode 100644 index 0000000..f791f97 --- /dev/null +++ b/panel/app/forms/pages/delete.php @@ -0,0 +1,21 @@ + array( + 'label' => 'pages.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $page->title(), + 'help' => $page->id(), + ) + )); + + $form->style('delete'); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/edit.php b/panel/app/forms/pages/edit.php new file mode 100644 index 0000000..e2359a9 --- /dev/null +++ b/panel/app/forms/pages/edit.php @@ -0,0 +1,45 @@ +getFormFields(), $page->getFormData()); + + // add the blueprint name as css class + $form->addClass('form-blueprint-' . $page->blueprint()->name()); + + // center the submit button + $form->centered = true; + + // set the keep api + $form->data('keep', $page->url('keep')); + + // set the autofocus on the title field + $form->fields->title->autofocus = true; + + // add the changes alert + if($page->changes()->differ()) { + + // display unsaved changes + $alert = new Brick('div'); + $alert->addClass('text'); + $alert->append('' . l('pages.show.changes.text') . ''); + + $form->buttons->prepend('changes', $alert); + $form->buttons->cancel->attr('href', $page->url('discard')); + $form->buttons->cancel->html(l('pages.show.changes.button')); + + // add wide buttons + $form->buttons->cancel->addClass('btn-wide'); + $form->buttons->submit->addClass('btn-wide'); + + } else { + // remove the cancel button + $form->buttons->cancel = ''; + } + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/template.php b/panel/app/forms/pages/template.php new file mode 100644 index 0000000..bdaa1cc --- /dev/null +++ b/panel/app/forms/pages/template.php @@ -0,0 +1,36 @@ +parent()->blueprint()->pages()->template() as $template) { + $options[$template->name()] = $template->title(); + } + + // create the form + $form = new Kirby\Panel\Form(array( + 'template' => array( + 'label' => 'pages.template.select.label', + 'type' => 'select', + 'options' => $options, + 'default' => key($options), + 'required' => true, + 'readonly' => count($options) == 1 ? true : false, + 'icon' => count($options) == 1 ? $page->blueprint()->pages()->template()->first()->icon() : 'chevron-down', + 'autofocus' => true + ), + 'disclaimer' => array( + 'type' => 'info', + 'text' => '' + ) + ), array( + 'template' => $page->intendedTemplate() + )); + + $form->buttons->submit->val(l('change')); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/toggle.php b/panel/app/forms/pages/toggle.php new file mode 100644 index 0000000..08cdffe --- /dev/null +++ b/panel/app/forms/pages/toggle.php @@ -0,0 +1,52 @@ +parent(); + $blueprint = $parent->blueprint(); + $siblings = $parent->children()->visible(); + + // sorting needed + if($blueprint->pages()->num()->mode() == 'default' and $siblings->count() > 0) { + + $options = array('' => l('pages.toggle.invisible'), '-' => '-'); + $n = 1; + + foreach($siblings as $sibling) { + $options[$n] = $n; + $n++; + } + + if($page->isInvisible()) { + $options[$n] = $n; + } + + $form = new Kirby\Panel\Form(array( + 'position' => array( + 'label' => l('pages.toggle.position'), + 'type' => 'select', + 'required' => true, + 'default' => $page->num(), + 'options' => $options + ) + )); + + } else { + + $form = new Kirby\Panel\Form(array( + 'confirmation' => array( + 'type' => 'info', + 'text' => $page->isVisible() ? l('pages.toggle.hide') : l('pages.toggle.publish') + ) + )); + + } + + $form->buttons->submit->value = l('change'); + $form->buttons->submit->autofocus = true; + + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/pages/url.php b/panel/app/forms/pages/url.php new file mode 100644 index 0000000..110569e --- /dev/null +++ b/panel/app/forms/pages/url.php @@ -0,0 +1,34 @@ + 'btn btn-icon label-option', + 'href' => '#', + 'data-title' => str::slug($page->title()) + )); + + // url preview + $preview = new Brick('div', '', array('class' => 'uid-preview')); + $preview->append(ltrim($page->parent()->uri() . '/', '/')); + $preview->append('' . $page->slug() . ''); + + // create the form + $form = new Kirby\Panel\Form(array( + 'uid' => array( + 'label' => l('pages.url.uid.label') . $option, + 'type' => 'text', + 'icon' => 'chain', + 'autofocus' => true, + 'help' => $preview, + 'default' => $page->slug() + ) + )); + + $form->buttons->submit->val(l('change')); + $form->cancel($page); + + return $form; + +}; \ No newline at end of file diff --git a/panel/app/forms/users/delete.php b/panel/app/forms/users/delete.php new file mode 100644 index 0000000..150b7ea --- /dev/null +++ b/panel/app/forms/users/delete.php @@ -0,0 +1,22 @@ + array( + 'label' => 'users.delete.headline', + 'type' => 'text', + 'readonly' => true, + 'icon' => false, + 'default' => $user->username(), + 'help' => $user->email(), + ) + )); + + $form->style('delete'); + $form->cancel($user, 'edit'); + + return $form; + +}; + diff --git a/panel/app/forms/users/user.php b/panel/app/forms/users/user.php new file mode 100644 index 0000000..5aca291 --- /dev/null +++ b/panel/app/forms/users/user.php @@ -0,0 +1,120 @@ +data() : array(); + $translations = array(); + $roles = array(); + + // make sure the password is never shown in the form + unset($content['password']); + + // add all languages + foreach(panel()->translations() as $code => $translation) { + $translations[$code] = $translation->title(); + } + + // add all roles + foreach(site()->roles() as $role) { + $roles[$role->id()] = $role->name(); + } + + // the default set of fields + $fields = array( + + 'username' => array( + 'label' => 'users.form.username.label', + 'type' => 'text', + 'icon' => 'user', + 'autofocus' => $mode != 'edit', + 'required' => true, + 'help' => $mode == 'edit' ? 'users.form.username.readonly' : 'users.form.username.help', + 'readonly' => $mode == 'edit', + ), + + 'firstName' => array( + 'label' => 'users.form.firstname.label', + 'autofocus' => $mode == 'edit', + 'type' => 'text', + 'width' => '1/2', + ), + + 'lastName' => array( + 'label' => 'users.form.lastname.label', + 'type' => 'text', + 'width' => '1/2', + ), + + 'email' => array( + 'label' => 'users.form.email.label', + 'type' => 'email', + 'required' => true, + 'autocomplete' => false + ), + + 'password' => array( + 'label' => $mode == 'edit' ? 'users.form.password.new.label' : 'users.form.password.label', + 'required' => $mode == 'add', + 'type' => 'password', + 'width' => '1/2', + 'suggestion' => true, + ), + + 'passwordConfirmation' => array( + 'label' => $mode == 'edit' ? 'users.form.password.new.confirm.label' : 'users.form.password.confirm.label', + 'required' => $mode == 'add', + 'type' => 'password', + 'width' => '1/2', + ), + + 'language' => array( + 'label' => 'users.form.language.label', + 'type' => 'select', + 'required' => true, + 'width' => '1/2', + 'default' => kirby()->option('panel.language', 'en'), + 'options' => $translations + ), + + 'role' => array( + 'label' => 'users.form.role.label', + 'type' => 'select', + 'required' => true, + 'width' => '1/2', + 'default' => site()->roles()->findDefault()->id(), + 'options' => $roles, + 'readonly' => (!panel()->user()->isAdmin() or ($user and $user->isLastAdmin())) + ), + + ); + + if($user) { + + // add all custom fields + foreach($user->blueprint()->fields()->toArray() as $name => $field) { + + if(array_key_exists($name, $fields)) { + continue; + } + + $fields[$name] = $field; + + } + + } + + // setup the form with all fields + $form = new Kirby\Panel\Form($fields, $content); + + // setup the url for the cancel button + $form->cancel('users'); + + if($mode == 'add') { + $form->buttons->submit->value = l('add'); + } + + return $form; + +}; + diff --git a/panel/app/helpers.php b/panel/app/helpers.php new file mode 100644 index 0000000..cb8bc53 --- /dev/null +++ b/panel/app/helpers.php @@ -0,0 +1,75 @@ +'; +} + +function i($icon, $position = null) { + echo icon($icon, $position); +} + +function __($var) { + echo htmlspecialchars($var); +} + +function _l($key, $default = null) { + echo htmlspecialchars(l($key, $default)); +} + +function i18n($value) { + + if(empty($value)) { + return null; + } else if(is_array($value)) { + $translation = a::get($value, panel()->translation()->code()); + + if(empty($translation)) { + // try to fallback to the default language at least + $translation = a::get($value, kirby()->option('panel.language'), $this->name()); + } + + return $translation; + } else if(is_string($value) and $translation = l::get($value)) { + return $translation; + } else { + return $value; + } + +} + +function _u($obj = '', $action = false) { + echo purl($obj, $action); +} + +function purl($obj = '/', $action = false) { + + if(empty($obj) or is_string($obj)) { + $base = panel()->urls()->index(); + return ($obj == '/' or empty($obj)) ? $base . '/' : rtrim($base . '/' . $obj, '/'); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\Site')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\Page')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\File')) { + return $obj->url(!$action ? 'edit' : $action); + } else if(is_a($obj, 'Kirby\\Panel\\Models\\User')) { + return $obj->url(!$action ? 'edit' : $action); + } + +} + +function slugTable() { + $table = array(); + foreach(str::$ascii as $key => $value) { + $key = trim($key, '/'); + foreach(str::split($key, '|') as $needle) { + $table[$needle] = $value; + } + } + + return json_encode($table, JSON_UNESCAPED_UNICODE); +} \ No newline at end of file diff --git a/panel/app/layouts/app.php b/panel/app/layouts/app.php new file mode 100644 index 0000000..1193fcb --- /dev/null +++ b/panel/app/layouts/app.php @@ -0,0 +1,31 @@ + + + + + + + <?php __($title) ?> + + + + + + option('panel.stylesheet')): ?> + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/panel/app/layouts/base.php b/panel/app/layouts/base.php new file mode 100644 index 0000000..d968aff --- /dev/null +++ b/panel/app/layouts/base.php @@ -0,0 +1,21 @@ + + + + + + + <?php __($title) ?> + + + + option('panel.stylesheet')): ?> + + + + + + + + + + \ No newline at end of file diff --git a/panel/app/layouts/fatal.php b/panel/app/layouts/fatal.php new file mode 100644 index 0000000..fd6e049 --- /dev/null +++ b/panel/app/layouts/fatal.php @@ -0,0 +1,32 @@ + + + + Kirby Panel + + + + + + +

    Panel Error:

    +

    + +

    +

    + Find more info on: getkirby.com +

    + + \ No newline at end of file diff --git a/panel/app/snippets/breadcrumb.php b/panel/app/snippets/breadcrumb.php new file mode 100644 index 0000000..461820c --- /dev/null +++ b/panel/app/snippets/breadcrumb.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/panel/app/snippets/languages.php b/panel/app/snippets/languages.php new file mode 100644 index 0000000..d91995f --- /dev/null +++ b/panel/app/snippets/languages.php @@ -0,0 +1,21 @@ +count() > 1): ?> + +
    + + + code()) ?> + + + + +
    + + diff --git a/panel/app/snippets/menu.php b/panel/app/snippets/menu.php new file mode 100644 index 0000000..ecf362d --- /dev/null +++ b/panel/app/snippets/menu.php @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/panel/app/snippets/meta.php b/panel/app/snippets/meta.php new file mode 100644 index 0000000..d6b4cb7 --- /dev/null +++ b/panel/app/snippets/meta.php @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar.php b/panel/app/snippets/pages/sidebar.php new file mode 100644 index 0000000..03b68ff --- /dev/null +++ b/panel/app/snippets/pages/sidebar.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/file.php b/panel/app/snippets/pages/sidebar/file.php new file mode 100644 index 0000000..e7ab043 --- /dev/null +++ b/panel/app/snippets/pages/sidebar/file.php @@ -0,0 +1,6 @@ +
  • + canHavePreview(), ' data-url="' . $file->crop(75)->url() . '"') ?> data-helper="filename()) ?>" data-text="dragText()) ?>" href="url('edit')) ?>"> + icon() . __($file->filename()) ?> + + +
  • \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/files.php b/panel/app/snippets/pages/sidebar/files.php new file mode 100644 index 0000000..bf8fbca --- /dev/null +++ b/panel/app/snippets/pages/sidebar/files.php @@ -0,0 +1,27 @@ +

    + + isSite(), l('metatags.files'), l('pages.show.files.title')) ?> + + + + + + + canHaveMoreFiles()) : ?> + + + + + + +

    + +count()): ?> + + +

    + \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/subpage.php b/panel/app/snippets/pages/sidebar/subpage.php new file mode 100644 index 0000000..78e36e6 --- /dev/null +++ b/panel/app/snippets/pages/sidebar/subpage.php @@ -0,0 +1,7 @@ +
  • + + icon() ?>title()) ?> + displayNum()) ?> + + +
  • \ No newline at end of file diff --git a/panel/app/snippets/pages/sidebar/subpages.php b/panel/app/snippets/pages/sidebar/subpages.php new file mode 100644 index 0000000..aa9ab9d --- /dev/null +++ b/panel/app/snippets/pages/sidebar/subpages.php @@ -0,0 +1,42 @@ +

    + + + + + + + + + + modal(), ' data-modal') ?> href="url()) ?>"> + + + + + + + +

    + +count()): ?> + + + + + +

    + + + modal(), ' data-modal') ?> href="url()) ?>"> + + + + + + +

    + \ No newline at end of file diff --git a/panel/app/snippets/pagination.php b/panel/app/snippets/pagination.php new file mode 100644 index 0000000..3ffeb5a --- /dev/null +++ b/panel/app/snippets/pagination.php @@ -0,0 +1,14 @@ +pages() > 1): ?> + + \ No newline at end of file diff --git a/panel/app/snippets/search.php b/panel/app/snippets/search.php new file mode 100644 index 0000000..d7b83b4 --- /dev/null +++ b/panel/app/snippets/search.php @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/panel/app/snippets/subpages/subpage.php b/panel/app/snippets/subpages/subpage.php new file mode 100644 index 0000000..b680a91 --- /dev/null +++ b/panel/app/snippets/subpages/subpage.php @@ -0,0 +1,29 @@ +
    +
    +
    + title()) ?> +
    +
    + +
    \ No newline at end of file diff --git a/panel/app/snippets/template.php b/panel/app/snippets/template.php new file mode 100644 index 0000000..2aab7f5 --- /dev/null +++ b/panel/app/snippets/template.php @@ -0,0 +1,27 @@ + +
    +

    + + +

    + + +

    + + + +

    + + +

    + + + +

    + + +

    + + +
    + \ No newline at end of file diff --git a/panel/app/snippets/uploader.php b/panel/app/snippets/uploader.php new file mode 100644 index 0000000..ff5be50 --- /dev/null +++ b/panel/app/snippets/uploader.php @@ -0,0 +1,20 @@ + isset($multiple) ? $multiple : true, + 'accept' => isset($accept) ? $accept : false +)); + +?> + + + \ No newline at end of file diff --git a/panel/app/src/panel.php b/panel/app/src/panel.php new file mode 100644 index 0000000..7a66812 --- /dev/null +++ b/panel/app/src/panel.php @@ -0,0 +1,559 @@ + '5.4.0', + 'toolkit' => '2.2.2', + 'kirby' => '2.2.1' + ); + + static public $instance; + + public $kirby; + public $site; + public $path; + public $roots; + public $routes = array(); + public $router = null; + public $route = null; + public $translation = null; + public $translations = null; + public $csrf = null; + + static public function instance() { + return static::$instance; + } + + static public function version() { + return static::$version; + } + + public function defaults() { + + return array( + 'panel.language' => 'en', + 'panel.stylesheet' => null, + 'panel.kirbytext' => true, + 'panel.session.timeout' => 1440, + 'panel.session.lifetime' => 0, + 'panel.info.license' => true, + 'panel.info.versions' => true, + 'panel.widgets' => array( + 'pages' => true, + 'site' => true, + 'account' => true, + 'history' => true + ), + ); + + } + + public function __construct($kirby, $root) { + + // check requirements + $this->requirements(); + + // store the instance as a singleton + static::$instance = $this; + + $this->kirby = $kirby; + $this->roots = new \Kirby\Panel\Roots($this, $root); + $this->urls = new \Kirby\Panel\Urls($this, $root); + + // add the panel default options + $this->kirby->options = array_merge($this->defaults(), $this->kirby->options); + + // setup the blueprints roots + UserBlueprint::$root = $this->kirby->roots()->blueprints() . DS . 'users'; + PageBlueprint::$root = $this->kirby->roots()->blueprints(); + + // load the site object + $this->site = $this->site(); + + // setup the session + $this->session(); + + // setup the multilang site stuff + $this->multilang(); + + // load all Kirby extensions (methods, tags, smartypants) + $this->kirby->extensions(); + $this->kirby->plugins(); + + // setup the form plugin + form::$root = array( + 'default' => $this->roots->fields, + 'custom' => $this->kirby->roots()->fields() + ); + + // force ssl if set in config + if($this->kirby->option('ssl') and !r::secure()) { + // rebuild the current url with https + go(url::build(array('scheme' => 'https'))); + } + + // load all available routes + $this->routes = array_merge($this->routes, require($this->roots->config . DS . 'routes.php')); + + // start the router + $this->router = new Router($this->routes); + + // register router filters + $this->router->filter('auth', function() use($kirby) { + try { + $user = panel()->user(); + } catch(Exception $e) { + panel()->redirect('login'); + } + }); + + // check for a completed installation + $this->router->filter('isInstalled', function() use($kirby) { + $installer = new Installer(); + if(!$installer->isCompleted()) { + panel()->redirect('install'); + } + }); + + // check for valid csrf tokens. Can be used for get requests + // since all post requests are blocked anyway + $this->router->filter('csrf', function() { + panel()->csrfCheck(); + }); + + // csrf protection for every post request + if(r::is('post')) { + $this->csrfCheck(); + } + + } + + public function session() { + + // setup the session + s::$timeout = $this->kirby->option('panel.session.timeout', 120); + s::$cookie['lifetime'] = $this->kirby->option('panel.session.lifetime', 0); + + // start the session + s::start(); + + } + + public function requirements() { + + if(!version_compare(PHP_VERSION, static::$requires['php'], '>=')) { + throw new Exception('Your PHP version is too old. Please upgrade to ' . static::$requires['php'] . ' or newer.'); + } + + if(!detect::mbstring()) { + throw new Exception('The mbstring extension must be installed'); + } + + if(!version_compare(toolkit::version(), static::$requires['toolkit'], '>=')) { + throw new Exception('Your Toolkit version is too old. Please upgrade to ' . static::$requires['toolkit'] . ' or newer.'); + } + + if(!version_compare(kirby::version(), static::$requires['kirby'], '>=')) { + throw new Exception('Your Kirby version is too old. Please upgrade to ' . static::$requires['kirby'] . ' or newer.'); + } + + } + + public function csrf() { + + if(!is_null($this->csrf)) return $this->csrf; + + // see if there's a token in the session + $token = s::get('csrf'); + + // create a new csrf token if not available yet + if(str::length($token) !== 32) { + $token = str::random(32); + } + + // store the new token in the session + s::set('csrf', $token); + + // create a new csrf token + return $this->csrf = $token; + + } + + public function csrfCheck() { + + $csrf = get('csrf'); + + if(empty($csrf) or $csrf !== s::get('csrf')) { + + try { + $this->user()->logout(); + } catch(Exception $e) {} + + $this->redirect('login'); + + } + + } + + public function kirby() { + return $this->kirby; + } + + public function site() { + + // return the site object if it has already been stored + if(!is_null($this->site)) return $this->site; + + // load the original site first to load all branch files + $this->kirby->site(); + + // create a new panel site object + return $this->site = new Site($this->kirby); + + } + + public function multilang() { + + if(!$this->site->multilang()) { + $language = null; + } else if($language = get('language') or $language = s::get('lang')) { + // $language is already set + } else { + $language = null; + } + + // set the path and lang for the original site object + $this->kirby->site()->visit('/', $language); + + // set the path and lang for the panel site object + $this->site->visit('/', $language); + + // store the language code + if($this->site->multilang()) { + s::set('lang', $this->site->language()->code()); + } + + } + + public function page($id) { + if($page = (empty($id) or $id == '/') ? $this->site() : $this->site()->find($id)) { + return $page; + } else { + throw new Exception(l('pages.error.missing')); + } + } + + public function roots() { + return $this->roots; + } + + public function routes($routes = null) { + if(is_null($routes)) return $this->routes; + return $this->routes = array_merge($this->routes, (array)$routes); + } + + public function urls() { + return $this->urls; + } + + public function form($id, $data = array(), $submit = null) { + + if(file_exists($id)) { + $file = $id; + } else { + $file = $this->roots->forms . DS . $id . '.php'; + } + + if(!file_exists($file)) { + throw new Exception(l('form.error.missing')); + } + + $callback = require($file); + + if(!is_callable($callback)) { + throw new Exception(l('form.construct.error.invalid')); + } + + $form = call($callback, $data); + + if(is_callable($submit)) { + $form->on('submit', $submit); + } + + return $form; + + } + + public function translations() { + + if(!is_null($this->translations)) return $this->translations; + + $this->translations = new Collection; + + foreach(dir::read($this->roots()->translations()) as $dir) { + // filter out everything but directories + if(!is_dir($this->roots()->translations() . DS . $dir)) continue; + + // create the translation object + $translation = new Translation($this, $dir); + $this->translations->append($translation->code(), $translation); + } + + return $this->translations; + + } + + public function translation() { + + if(!is_null($this->translation)) return $this->translation; + + // get the default language code from the options + $lang = $this->kirby()->option('panel.language', 'en'); + $user = $this->site()->user(); + + if($user && $user->language()) { + $lang = $user->language(); + } + + return $this->translation = new Translation($this, $lang); + + } + + public function language() { + return $this->translation; + } + + public function direction() { + return $this->translation->direction(); + } + + public function launch($path = null) { + + // set the timezone for all date functions + date_default_timezone_set($this->kirby->options['timezone']); + + // load the current translation + $this->translation()->load(); + + $this->path = $this->kirby->path(); + $this->route = $this->router->run($this->path); + + // set the current url + $this->urls->current = rtrim($this->urls->index() . '/' . $this->path, '/'); + + ob_start(); + + try { + + // react on invalid routes + if(!$this->route) { + throw new Exception(l('routes.error.invalid')); + } + + if(is_callable($this->route->action())) { + $response = call($this->route->action(), $this->route->arguments()); + } else { + $response = $this->response(); + } + + } catch(Exception $e) { + require_once($this->roots->controllers . DS . 'error.php'); + $controller = new ErrorController(); + $response = $controller->index($e->getMessage(), $e); + } + + // check for a valid response object + if(is_a($response, 'Response')) { + echo $response; + } else { + echo new Response($response); + } + + ob_end_flush(); + + } + + public function response() { + + // let's find the controller and controller action + $controllerParts = str::split($this->route->action(), '::'); + $controllerUri = $controllerParts[0]; + $controllerAction = $controllerParts[1]; + $controllerFile = $this->roots->controllers . DS . strtolower(str_replace('Controller', '', $controllerUri)) . '.php'; + $controllerName = basename($controllerUri); + + // react on missing controllers + if(!file_exists($controllerFile)) { + throw new Exception(l('controller.error.invalid')); + } + + // load the controller + require_once($controllerFile); + + // check for the called action + if(!method_exists($controllerName, $controllerAction)) { + throw new Exception(l('controller.error.action')); + } + + // run the controller + $controller = new $controllerName; + + // call the action and pass all arguments from the router + return call(array($controller, $controllerAction), $this->route->arguments()); + + } + + public function license() { + + $key = c::get('license'); + $type = 'trial'; + + /** + * Hey stranger, + * + * So this is the mysterious place where the panel checks for + * valid licenses. As you can see, this is not reporting + * back to any server and the license keys are rather simple to + * hack. If you really feel like removing the warning in the panel + * or tricking Kirby into believing you bought a valid license even + * if you didn't, go for it! But remember that literally thousands of + * hours of work have gone into Kirby in order to make your + * life as a developer, designer, publisher, etc. easier. If this + * doesn't mean anything to you, you are probably a lost case anyway. + * + * Have a great day! + * + * Bastian + */ + if(str::startsWith($key, 'K2-PRO') and str::length($key) == 39) { + $type = 'Kirby 2 Professional'; + } else if(str::startsWith($key, 'K2-PERSONAL') and str::length($key) == 44) { + $type = 'Kirby 2 Personal'; + } else if(str::startsWith($key, 'MD-') and str::length($key) == 35) { + $type = 'Kirby 1'; + } else if(str::startsWith($key, 'BETA') and str::length($key) == 9) { + $type = 'Kirby 1'; + } else if(str::length($key) == 32) { + $type = 'Kirby 1'; + } else { + $key = null; + } + + return new Obj(array( + 'key' => $key, + 'local' => $this->isLocal(), + 'type' => $type, + )); + + } + + public function isLocal() { + $localhosts = array('::1', '127.0.0.1', '0.0.0.0'); + return (in_array(server::get('SERVER_ADDR'), $localhosts) || server::get('SERVER_NAME') == 'localhost'); + } + + public function notify($text) { + s::set('message', array( + 'type' => 'notification', + 'text' => $text, + )); + } + + public function alert($text) { + s::set('message', array( + 'type' => 'error', + 'text' => $text, + )); + } + + public function redirect($obj = '/', $action = false, $force = false) { + + if($force === false and $redirect = get('_redirect')) { + $url = purl($redirect); + } else { + $url = purl($obj, $action); + } + + if(r::ajax()) { + + $user = $this->site()->user(); + + die(response::json(array( + 'direction' => $this->direction(), + 'user' => $user ? $user->username() : false, + 'url' => $url + ))); + + } else { + go($url); + } + + } + + public function users() { + return $this->site()->users(); + } + + public function user($username = null) { + if($user = $this->site()->user($username)) { + return $user; + } else { + throw new Exception(l('users.error.missing')); + } + } + + public static function fatal($e, $root) { + + $message = $e->getMessage() ? $e->getMessage() : 'Error without a useful message :('; + $where = implode('
    ', [ + '', + '', + 'It happened here:', + 'File: ' . str_replace($root, '/panel', $e->getFile()) . '', + 'Line: ' . $e->getLine() . '' + ]); + + // load the fatal screen + return tpl::load($root . DS . 'app' . DS . 'layouts' . DS . 'fatal.php', [ + 'css' => url::index() . '/assets/css/panel.css', + 'content' => $message . $where + ]); + + } + +} diff --git a/panel/app/src/panel/autocomplete.php b/panel/app/src/panel/autocomplete.php new file mode 100644 index 0000000..2460759 --- /dev/null +++ b/panel/app/src/panel/autocomplete.php @@ -0,0 +1,119 @@ +panel = $panel; + $this->site = $panel->site(); + $this->method = $method; + $this->params = $params; + } + + public function result() { + + $method = 'autocomplete' . $this->method; + + if(!method_exists($this, $method)) { + throw new Exception(l('autocomplete.method.error')); + } + + $result = array_values((array)$this->$method($this->params)); + + // sort results alphabetically + sort($result); + + return $result; + + } + + public function autocompleteUsernames() { + return $this->panel->users()->map(function($user) { + return $user->username(); + })->toArray(); + } + + public function autocompleteEmails() { + return $this->panel->users()->map(function($user) { + return $user->email(); + })->toArray(); + } + + public function autocompleteUris() { + return $this->site->index()->map(function($page) { + return $page->id(); + })->toArray(); + } + + public function autocompleteField($params = array()) { + + $defaults = array( + 'index' => 'siblings', + 'uri' => '/', + 'field' => 'tags', + 'yaml' => false, + 'model' => 'page', + 'separator' => true + ); + + $options = array_merge($defaults, $params); + $page = $this->panel->page($options['uri']); + $pages = $this->pages($page, $options['index'], $options); + $yaml = $options['yaml']; + + if($yaml or $options['model'] == 'file') { + $result = array(); + foreach($pages as $p) { + if($yaml) { + $index = $p->$yaml()->toStructure(); + } elseif($options['model'] == 'file') { + $index = $p->files(); + } + $values = $index->pluck($options['field'], $options['separator'], true); + $result = array_merge($result, $values); + } + $result = array_unique($result); + } else { + $result = $pages->pluck($options['field'], $options['separator'], true); + } + + return $result; + + } + + public function pages($page, $index, $params = array()) { + + switch($index) { + case 'self': + return new Pages(array($page)); + break; + case 'siblings': + case 'children': + return $page->$index(); + break; + case 'template': + $template = a::get($params, 'template', $page->template()); + return $this->site->index()->filterBy('template', $template); + break; + case 'pages': + case 'all': + return $this->site->index(); + break; + default: + return $page->children(); + break; + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/collections/children.php b/panel/app/src/panel/collections/children.php new file mode 100644 index 0000000..6b1ee83 --- /dev/null +++ b/panel/app/src/panel/collections/children.php @@ -0,0 +1,129 @@ +reset(); + + $inventory = $page->inventory(); + + foreach($inventory['children'] as $dirname) { + $child = new Page($page, $dirname); + $this->data[$child->id()] = $child; + } + + $sort = $page->blueprint()->pages()->sort(); + + switch($sort) { + case 'flip': + $cloned = $this->flip(); + $this->data = $cloned->data; + break; + default; + $parts = str::split($sort, ' '); + if(count($parts) > 0) { + $cloned = call(array($this, 'sortBy'), $parts); + $this->data = $cloned->data; + } + break; + } + + } + + public function create($uid, $template, $content = array()) { + + if(empty($template)) { + throw new Exception(l('pages.add.error.template')); + } + + $uid = empty($uid) ? str::random(32) : $uid; + $blueprint = new Blueprint($template); + $data = array(); + + foreach($blueprint->fields(null) as $key => $field) { + $data[$key] = $field->default(); + } + + $data = array_merge($data, $content); + + // create the new page and convert it to a page model + $page = new Page($this->page, parent::create($uid, $template, $data)->dirname()); + + if(!$page) { + throw new Exception(l('pages.add.error.create')); + } + + kirby()->trigger('panel.page.create', $page); + + // subpage builder + foreach((array)$page->blueprint()->pages()->build() as $build) { + $missing = a::missing($build, array('title', 'template', 'uid')); + if(!empty($missing)) continue; + $subpage = $page->children()->create($build['uid'], $build['template'], array('title' => $build['title'])); + if(isset($build['num'])) $subpage->sort($build['num']); + } + + return $page; + + } + + public function paginated($mode = 'sidebar') { + + if($limit = $this->page->blueprint()->pages()->limit()) { + + $hash = sha1($this->page->id()); + + switch($mode) { + case 'sidebar': + $id = 'pages.' . $hash; + $var = 'page'; + break; + case 'subpages/visible': + $id = 'subpages.visible.' . $hash; + $var = 'visible'; + break; + case 'subpages/invisible': + $id = 'subpages.invisible.' . $hash; + $var = 'invisible'; + break; + } + + // filter out hidden pages + $children = $this->filter(function($child) { + return $child->blueprint()->hide() === false; + }); + + $children = $children->paginate($limit, array( + 'page' => get($var, s::get($id)), + 'omitFirstPage' => false, + 'variable' => $var, + 'method' => 'query', + 'redirect' => false + )); + + // store the last page + s::set($id, $children->pagination()->page()); + + return $children; + + } else { + return $this; + } + + } + + +} diff --git a/panel/app/src/panel/collections/files.php b/panel/app/src/panel/collections/files.php new file mode 100644 index 0000000..777590c --- /dev/null +++ b/panel/app/src/panel/collections/files.php @@ -0,0 +1,51 @@ +kirby = $page->kirby; + $this->site = $page->site; + $this->page = $page; + + // make sure the inventory is always fresh + $this->page->reset(); + + $inventory = $page->inventory(); + + foreach($inventory['files'] as $filename) { + $file = new File($this, $filename); + $this->data[strtolower($file->filename())] = $file; + } + + if($this->page->canSortFiles()) { + $sorted = $this->sortBy('sort', 'asc'); + $this->data = $sorted->data; + } + + if($this->page->blueprint()->files()->sort() == 'flip') { + $flipped = $this->flip(); + $this->data = $flipped->data; + } + + } + + public function topbar($topbar) { + + $page = $this->page(); + + if($page->isSite()) { + $topbar->append(purl('options'), l('metatags')); + } + + $page->topbar($topbar); + + $topbar->append($page->url('files'), l('files')); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/collections/users.php b/panel/app/src/panel/collections/users.php new file mode 100644 index 0000000..e9f819a --- /dev/null +++ b/panel/app/src/panel/collections/users.php @@ -0,0 +1,39 @@ +map(function($user) { + return new User($user->username()); + }); + + } + + public function topbar($topbar) { + $topbar->append(purl('users'), l('users')); + } + + public function create($data) { + + if($data['password'] !== $data['passwordconfirmation']) { + throw new Exception(l('users.form.error.password.confirm')); + } + + unset($data['passwordconfirmation']); + + $user = parent::create($data); + kirby()->trigger('panel.user.create', $user); + return new User($user->username()); + + } + + +} \ No newline at end of file diff --git a/panel/app/src/panel/controllers/base.php b/panel/app/src/panel/controllers/base.php new file mode 100644 index 0000000..1c6b864 --- /dev/null +++ b/panel/app/src/panel/controllers/base.php @@ -0,0 +1,121 @@ +redirect($obj, $action, $force); + } + + public function notify($message) { + panel()->notify($message); + } + + public function alert($message) { + panel()->alert($message); + } + + public function form($id, $data = array(), $submit = null) { + return panel()->form($id, $data, $submit); + } + + public function page($id) { + return panel()->page($id); + } + + public function user($username = null) { + return panel()->user($username); + } + + public function layout($type, $data = array()) { + + $version = panel()->version(); + $base = panel()->urls()->index(); + $cssbase = panel()->urls()->css(); + $jsbase = panel()->urls()->js(); + + $defaults = array( + 'title' => panel()->site()->title() . ' | Panel', + 'direction' => panel()->direction(), + 'meta' => $this->snippet('meta'), + 'css' => css($cssbase . '/panel.min.css?v=' . $version), + 'js' => js($jsbase . '/dist/panel.min.js?v=' . $version), + 'content' => '', + 'bodyclass' => '', + ); + + switch($type) { + case 'app': + $defaults['topbar'] = ''; + $defaults['csrf'] = panel()->csrf(); + $defaults['formcss'] = css($cssbase . '/form.min.css?v=' . $version); + $defaults['formjs'] = js($jsbase . '/dist/form.min.js?v=' . $version); + $defaults['appjs'] = js($jsbase . '/dist/app.min.js?v=' . $version); + + // plugin stuff + $defaults['pluginscss'] = css($base . '/plugins/css?v=' . $version); + $defaults['pluginsjs'] = js($base . '/plugins/js?v=' . $version); + + break; + case 'base': + break; + } + + $data = array_merge($defaults, $data); + + if(r::ajax() and $type == 'app') { + $panel = panel(); + $user = $panel->site()->user(); + $response = array( + 'user' => $user ? $user->username() : false, + 'direction' => $panel->direction(), + 'title' => $data['title'], + 'content' => $data['topbar'] . $data['content'] + ); + return response::json($response); + } else { + return new Layout($type, $data); + } + + } + + public function view($file, $data = array()) { + return new View($file, $data); + } + + public function snippet($file, $data = array()) { + return new Snippet($file, $data); + } + + public function topbar($view, $input) { + return new Topbar($view, $input); + } + + public function screen($view, $topbar = null, $data = array()) { + return $this->layout('app', array( + 'topbar' => is_a($topbar, 'Kirby\\Panel\\Topbar') ? $topbar : $this->topbar($view, $topbar), + 'content' => is_a($data, 'Kirby\\Panel\\View') ? $data : $this->view($view, $data) + )); + } + + public function modal($view, $data = array()) { + if($view === 'error') $view = 'error/modal'; + return $this->layout('app', array('content' => $this->view($view, $data))); + } + + public function json($data = array()) { + return response::json($data); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/controllers/field.php b/panel/app/src/panel/controllers/field.php new file mode 100644 index 0000000..53073fb --- /dev/null +++ b/panel/app/src/panel/controllers/field.php @@ -0,0 +1,47 @@ +model = $model; + $this->field = $field; + $this->fieldname = $field->name(); + } + + public function form($id, $data = array(), $submit = null) { + $file = $this->field->root() . DS . 'forms' . DS . $id . '.php'; + return panel()->form($file, $data, $submit); + } + + public function view($file, $data = array()) { + + $view = new View($file, $data); + $root = $this->field->root() . DS . 'views'; + + if(file_exists($root . DS . $file . '.php')) { + $view->_root = $root; + } + + return $view; + + } + + public function snippet($file, $data = array()) { + + $snippet = new Snippet($file, $data); + $root = $this->field->root() . DS . 'snippets'; + + if(file_exists($root . DS . $file . '.php')) { + $snippet->_root = $root; + } + + return $snippet; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/form.php b/panel/app/src/panel/form.php new file mode 100644 index 0000000..e489deb --- /dev/null +++ b/panel/app/src/panel/form.php @@ -0,0 +1,340 @@ +fields = new Collection; + + // if Form is part of a structureField, set structureField name + $this->parentField = $parent; + + // initialize all field plugins + $this->plugins = new Plugins(); + + $this->values($values); + $this->fields($fields); + $this->buttons(); + $this->attr('method', 'post'); + $this->attr('action', panel()->urls()->current()); + $this->addClass('form'); + + } + + public function method($method = null) { + return $this->attr('method', $method); + } + + public function action($action = null) { + return $this->attr('action', $action); + } + + public function fields($fields = null) { + + if(is_null($fields)) return $this->fields; + + // get the site object + $site = panel()->site(); + + // check if untranslatable fields should be deactivated + $translated = $site->multilang() && !$site->language()->default(); + + foreach($fields as $name => $field) { + + $name = str_replace('-','_', str::lower($name)); + + $field['name'] = $name; + $field['default'] = a::get($field, 'default', null); + $field['value'] = a::get($this->values(), $name, $field['default']); + + // Pass through parent field name (structureField) + $field['parentField'] = $this->parentField; + + // Check for untranslatable fields + if($translated and isset($field['translate']) and $field['translate'] === false) { + $field['readonly'] = true; + $field['disabled'] = true; + } + + $this->fields->append($name, static::field($field['type'], $field)); + + } + + return $this; + + } + + public function values($values = null) { + if(is_null($values)) return array_merge($this->values, r::data()); + $this->values = array_merge($this->values, $values); + return $this; + } + + public function value($name) { + return a::get($this->values(), $name, null); + } + + public function validate() { + + $site = panel()->site(); + $translated = $site->multilang() && !$site->language()->default(); + $errors = array(); + + foreach($this->fields() as $field) { + + // don't validate fields, which are not translatable + if($translated and $field->translate() === false) continue; + + $name = $field->name(); + $value = $this->value($name); + + if($field->required() and $value == '') { + $field->error = true; + } else if($value !== '' and $field->validate() == false) { + $field->error = true; + } + + } + + } + + public function isValid() { + return $this->fields()->filterBy('error', true)->count() == 0; + } + + public function message($type, $text) { + + $this->message = new Brick('div'); + $this->message->addClass('message'); + + if($type == 'error') { + $this->message->addClass('message-is-alert'); + } else { + $this->message->addClass('message-is-notice'); + } + + $this->message->append(function() use($text) { + + $content = new Brick('span'); + $content->addClass('message-content'); + $content->text($text); + + return $content; + + }); + + return $this->message; + + } + + public function alert($text) { + $this->message('error', $text); + } + + public function notify($text) { + $this->message('success', $text); + } + + public function serialize() { + + $data = array(); + $site = panel()->site(); + $fields = $this->fields(); + + foreach($fields as $field) { + $result = $field->result(); + if(!is_null($result)) $data[$field->name()] = $result; + } + + // unset untranslatable fields in all languages but the default lang + if($site->multilang() and $site->language() != $site->defaultLanguage()) { + foreach($fields as $field) { + if($field->translate() === false) { + $data[$field->name()] = null; + } + } + } + + return $data; + + } + + public function toArray() { + return $this->serialize(); + } + + public function plugins() { + return $this->plugins; + } + + public function style($style) { + + switch($style) { + case 'centered': + $this->centered = true; + $this->buttons->cancel = ''; + break; + case 'upload': + $this->centered = true; + $this->buttons->submit = ''; + $this->attr('enctype', 'multipart/form-data'); + break; + case 'delete': + $this->buttons->submit->addClass('btn-negative'); + $this->buttons->submit->attr('autofocus', true); + $this->buttons->submit->val(l('delete')); + break; + case 'editor': + + $kirbytext = kirby()->option('panel.kirbytext', true); + + $this->data('textarea', get('textarea')); + $this->data('autosubmit', 'false'); + $this->data('kirbytext', r($kirbytext, 'true', 'false')); + $this->buttons->submit->val(l('insert')); + break; + } + + } + + public function redirect() { + return get('_redirect'); + } + + public function cancel() { + if($redirect = $this->redirect()) { + $this->buttons->cancel->href = purl($redirect); + } else { + $this->buttons->cancel->href = call('purl', func_get_args()); + } + } + + static public function field($type, $options = array()) { + + $class = $type . 'field'; + + if(!class_exists($class)) { + throw new Exception('The ' . $type . ' field is missing. Please add it to your installed fields or remove it from your blueprint'); + } + + $field = new $class; + + foreach($options as $key => $value) { + $field->$key = $value; + } + + return $field; + + } + + public function buttons() { + + if(!is_null($this->buttons)) return $this->buttons; + + $this->buttons = new Collection(); + + $button = new Brick('input', null); + $button->addClass('btn btn-rounded'); + + $cancel = clone $button; + $cancel->tag('a'); + $cancel->addClass('btn-cancel'); + $cancel->attr('href', '#cancel'); + $cancel->text(l('cancel')); + + $this->buttons->append('cancel', $cancel); + + $submit = clone $button; + $submit->attr('type', 'submit'); + $submit->addClass('btn-submit'); + $submit->data('saved', l('saved')); + $submit->val(l('save')); + + $this->buttons->append('submit', $submit); + + return $this->buttons; + + } + + public function on($action, $callback) { + + // auto-trigger the submit event when the form is being echoed + if(r::is('post')) { + $callback($this); + } + + $this->fields->append('csrf', static::field('hidden', array( + 'name' => 'csrf', + 'value' => panel()->csrf() + ))); + + } + + public function toHTML() { + + if($this->message) { + $this->append($this->message); + } + + $fieldset = new Brick('fieldset'); + $fieldset->addClass('fieldset field-grid cf'); + + foreach($this->fields() as $field) $fieldset->append($field); + + // pass the redirect url + $redirectField = new Brick('input'); + $redirectField->type = 'hidden'; + $redirectField->name = '_redirect'; + $redirectField->value = $this->redirect(); + $fieldset->append($redirectField); + + $this->append($fieldset); + + $buttons = new Brick('fieldset'); + $buttons->addClass('fieldset buttons'); + + if($this->centered) { + $buttons->addClass('buttons-centered'); + } + + foreach($this->buttons() as $button) $buttons->append($button); + + $this->append($buttons); + + return $this; + + } + + public function __toString() { + + $this->toHTML(); + return parent::__toString(); + + } + +} diff --git a/panel/app/src/panel/form/fieldoptions.php b/panel/app/src/panel/form/fieldoptions.php new file mode 100644 index 0000000..fb17ace --- /dev/null +++ b/panel/app/src/panel/form/fieldoptions.php @@ -0,0 +1,256 @@ +toArray(); + } + + public function __construct($field) { + + $this->field = $field; + + if(is_array($this->field->options)) { + $this->options = $this->field->options; + } else if($this->isUrl($this->field->options)) { + $this->options = $this->optionsFromApi($this->field->options); + } else if($this->field->options == 'query') { + $this->options = $this->optionsFromQuery($this->field->query); + } else if($this->field->options == 'field') { + $this->options = $this->optionsFromField($this->field->field); + } else { + $this->options = $this->optionsFromPageMethod($this->field->page, $this->field->options); + } + + // sorting + $this->options = $this->sort($this->options, !empty($this->field->sort) ? $this->field->sort : null); + + } + + public function optionsFromPageMethod($page, $method) { + + if($page && $items = $this->items($page, $method)) { + $options = array(); + foreach($items as $item) { + if(is_a($item, 'Page')) { + $options[$item->uid()] = (string)$item->title(); + } else if(is_a($item, 'File')) { + $options[$item->filename()] = (string)$item->filename(); + } + } + return $options; + } else { + return array(); + } + + } + + public function optionsFromApi($url) { + $response = remote::get($url); + $options = @json_decode($response->content(), true); + return is_array($options) ? $options : array(); + } + + public function optionsFromField($field) { + + // default field parameters + $defaults = array( + 'page' => $this->field->page ? ($this->field->page->isSite() ? '/' : $this->field->page->id()) : '', + 'name' => 'tags', + 'separator' => ',', + ); + + // sanitize the query + if(!is_array($field)) { + $field = array(); + } + + // merge the default parameters with the actual query + $field = array_merge($defaults, $field); + + // dynamic page option + // ../ + // ../../ etc. + $page = $this->page($field['page']); + $items = $page->{$field['name']}()->split($field['separator']); + $options = array(); + + foreach($items as $item) { + $options[$item] = $item; + } + + return $options; + + } + + public function optionsFromQuery($query) { + + // default query parameters + $defaults = array( + 'page' => $this->field->page ? ($this->field->page->isSite() ? '/' : $this->field->page->id()) : '', + 'fetch' => 'children', + 'value' => '{{uid}}', + 'text' => '{{title}}', + 'flip' => false, + 'template' => false + ); + + // sanitize the query + if(!is_array($query)) { + $query = array(); + } + + // merge the default parameters with the actual query + $query = array_merge($defaults, $query); + + // dynamic page option + // ../ + // ../../ etc. + $page = $this->page($query['page']); + $items = $this->items($page, $query['fetch']); + $options = array(); + + if($query['template']) { + $items = $items->filter(function($item) use($query) { + return in_array(str::lower($item->intendedTemplate()), array_map('str::lower', (array)$query['template'])); + }); + } + + if($query['flip']) { + $items = $items->flip(); + } + + foreach($items as $item) { + $value = $this->tpl($query['value'], $item); + $text = $this->tpl($query['text'], $item); + + $options[$value] = $text; + } + + return $options; + + } + + public function page($uri) { + + if(str::startsWith($uri, '../')) { + if($currentPage = $this->field->page) { + $path = $uri; + while(str::startsWith($path, '../')) { + if($parent = $currentPage->parent()) { + $currentPage = $parent; + } else { + $currentPage = site(); + } + $path = str::substr($path, 3); + } + if(!empty($path)) { + $currentPage = $currentPage->find($path); + } + $page = $currentPage; + } else { + $page = null; + } + } else if($uri == '/') { + $page = site(); + } else { + $page = page($uri); + } + + return $page; + + } + + public function sort($options, $sort) { + + if(empty($sort)) return $options; + + switch(strtolower($sort)) { + case 'asc': + asort($options); + break; + case 'desc': + arsort($options); + break; + } + + return $options; + + } + + public function tpl($string, $obj) { + return preg_replace_callback('!\{\{(.*?)\}\}!', function($item) use($obj) { + return (string)$obj->{$item[1]}(); + }, $string); + } + + public function isUrl($url) { + return + v::url($url) or + str::contains($url, '://localhost') or + str::contains($url, '://127.0.0.1'); + } + + public function items($page, $method) { + + if(!$page) return new Collection(); + + switch($method) { + case 'visibleChildren': + $items = $page->children()->visible(); + break; + case 'invisibleChildren': + $items = $page->children()->invisible(); + break; + case 'siblings': + $items = $page->siblings()->not($page); + break; + case 'visibleSiblings': + $items = $page->siblings()->not($page)->visible(); + break; + case 'invisibleSiblings': + $items = $page->siblings()->not($page)->invisible(); + break; + case 'pages': + $items = site()->index(); + $items = $items->sortBy('title', 'asc'); + break; + case 'index': + $items = $page->index(); + $items = $items->sortBy('title', 'asc'); + break; + case 'children': + case 'grandchildren': + case 'files': + case 'images': + case 'documents': + case 'videos': + case 'audio': + case 'code': + case 'archives': + $items = $page->{$method}(); + break; + default: + $items = new Collection(); + } + + return $items; + + } + + public function toArray() { + return $this->options; + } + +} diff --git a/panel/app/src/panel/form/plugins.php b/panel/app/src/panel/form/plugins.php new file mode 100644 index 0000000..4ff4e42 --- /dev/null +++ b/panel/app/src/panel/form/plugins.php @@ -0,0 +1,113 @@ +find(); + $this->load(); + } + + public function find() { + + $kirby = kirby(); + + // store all fields coming from plugins and load + // them between the default fields and the custom fields + $pluginfields = $kirby->get('field'); + + // load the default panel fields first, because they can be overwritten + foreach(dir::read(form::$root['default']) as $name) { + $kirby->set('field', $name, form::$root['default'] . DS . $name); + } + + // load the plugin fields again. A bit hacky, but works + foreach($pluginfields as $name => $field) { + $kirby->set('field', $name, $field->root()); + } + + // load all custom fields, which can overwrite all the others + foreach(dir::read(form::$root['custom']) as $name) { + $kirby->set('field', $name, form::$root['custom'] . DS . $name); + } + + } + + public function load() { + + $fields = kirby()->get('field'); + $classes = []; + + foreach($fields as $name => $field) { + $classes[$field->class()] = $field->file(); + } + + // start the autoloader + load($classes); + + foreach($fields as $name => $field) { + + $classname = $field->class(); + + if(!class_exists($classname)) { + throw new Exception('The field class is missing for: ' . $classname); + } + + if(method_exists($classname, 'setup')) { + call(array($classname, 'setup')); + } + + } + + } + + public function assets($type) { + + $output = []; + $defaultRoot = panel()->roots()->fields(); + + foreach(kirby()->get('field') as $name => $field) { + + $root = $field->root(); + $base = dirname($root); + + // only fetch assets for custom fields + if($base == $defaultRoot) { + continue; + } + + $classname = $field->class(); + + if(!class_exists($classname)) { + throw new Exception('The field class is missing for: ' . $classname); + } + + if(!isset($classname::$assets) || !isset($classname::$assets[$type])) { + continue; + } + + foreach($classname::$assets[$type] as $filename) { + $output[] = f::read($field->root() . DS . 'assets' . DS . $type . DS . $filename); + } + + } + + return implode(PHP_EOL . PHP_EOL, $output); + + } + + public function css() { + return $this->assets('css'); + } + + public function js() { + return $this->assets('js'); + } + +} diff --git a/panel/app/src/panel/installer.php b/panel/app/src/panel/installer.php new file mode 100644 index 0000000..e0ca048 --- /dev/null +++ b/panel/app/src/panel/installer.php @@ -0,0 +1,78 @@ +users()->count() > 0 && is_writable(kirby()->roots()->accounts())); + } + + public function problems() { + + $checks = array('allowed', 'accounts', 'thumbs', 'blueprints', 'content', 'avatars'); + $problems = array(); + + foreach($checks as $c) { + $method = 'check' . $c; + + if(!$this->$method()) { + $problems[] = l('installation.check.error.' . $c); + } + + } + + return empty($problems) ? false : $problems; + + } + + protected function checkAllowed() { + return (panel()->isLocal() || kirby()->option('panel.install') === true); + } + + protected function checkAccounts() { + + $root = kirby()->roots()->accounts(); + + // try to create the accounts folder + dir::make($root); + + return is_writable($root); + + } + + protected function checkThumbs() { + + $root = kirby()->roots()->thumbs(); + + // try to create the thumbs folder + dir::make($root); + + return is_writable($root); + + } + + protected function checkBlueprints() { + return is_dir(kirby()->roots()->blueprints()); + } + + protected function checkContent() { + $folder = new Folder(kirby()->roots()->content()); + return $folder->isWritable(true); + } + + protected function checkAvatars() { + + $root = kirby()->roots()->avatars(); + + // try to create the avatars folder + dir::make($root); + + return is_writable($root); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/layout.php b/panel/app/src/panel/layout.php new file mode 100644 index 0000000..1aea276 --- /dev/null +++ b/panel/app/src/panel/layout.php @@ -0,0 +1,14 @@ +_root = panel::instance()->roots()->layouts(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/login.php b/panel/app/src/panel/login.php new file mode 100644 index 0000000..5f816e3 --- /dev/null +++ b/panel/app/src/panel/login.php @@ -0,0 +1,246 @@ +kirby = kirby(); + $this->logfile = $this->kirby->roots()->accounts() . DS . '.logins'; + + $this->setup(); + + } + + /** + * Setup and check the logfile + */ + protected function setup() { + + // make sure the logroot exists + if(!is_writable(dirname($this->logfile))) { + throw new Exception(l('users.form.error.permissions.title')); + } + + // create the logfile if not there yet + touch($this->logfile); + + // make sure the logroot exists + if(!is_writable($this->logfile)) { + throw new Exception(l('login.log.error.permissions')); + } + + } + + /** + * Run an attempt to login + * + * @param string $username + * @param string $password + */ + public function attempt($username, $password) { + + $this->username = str::lower($username); + $this->password = $password; + + try { + + if($this->isInvalidUsername() || $this->isInvalidPassword()) { + throw new Exception(l('login.error')); + } + + $user = $this->user(); + + if(!$user->login($this->password)) { + throw new Exception(l('login.error')); + } + + $this->clearLog($this->visitorId()); + return true; + + } catch(Exception $e) { + + $this->log(); + $this->pause(); + + throw $e; + + } + + } + + /** + * Checks if the login form can be + * bypassed, because the user is already + * authenticated + * + * @return boolean + */ + public function isAuthenticated() { + try { + panel()->user(); + return true; + } catch(Exception $e) { + return false; + } + } + + /** + * Checks if a brute force attack has + * probably been executed + * + * @return boolean + */ + public function isBlocked() { + return $this->attempts() > $this->maxUntrustedAttempts; + } + + /** + * Fetch the user for the entered username + * + * @return User + */ + protected function user() { + return panel()->user($this->username); + } + + /** + * Returns all logdata in an array + * + * @return array + */ + protected function logdata() { + if(!is_null($this->logdata)) { + return $this->logdata; + } else { + + $data = (array)data::read($this->logfile, 'json'); + $login = $this; + + // remove old entries + $data = array_filter($data, function($entry) use($login) { + return ($entry['time'] > (time() - $login->logexpiry)); + }); + + return $this->logdata = $data; + } + } + + /** + * Stores a new login attempt to + * make it trackable later + * + * The store contains a sha1 hash of the ip + * + * @return boolean + */ + protected function log() { + + // get the latest logdata + $data = $this->logdata(); + + // store a new attempt + $data[] = array( + 'time' => time(), + 'id' => $this->visitorId(), + ); + + // write it to the logfile + return data::write($this->logfile, $data, 'json'); + + } + + /** + * Return a hashed version of the visitor ip + * + * @return string + */ + protected function visitorId() { + return sha1(visitor::ip()); + } + + /** + * Returns the number of attempts for + * the current visitor + * + * @return int + */ + protected function attempts() { + + $data = $this->logdata(); + $login = $this; + $data = array_filter($data, function($entry) use($login) { + return $login->visitorId() === $entry['id']; + }); + + return count($data); + + } + + /** + * Checks if an invalid username has been entered + * + * @return boolean + */ + protected function isInvalidUsername() { + return !preg_match('!^[a-z0-9._-]{1,}$!', $this->username); + } + + /** + * Checks if an invalid password has been entered + * + * @return boolean + */ + protected function isInvalidPassword() { + return empty($this->password); + } + + /** + * Create a random pause between 0 and 3 + * seconds to make it harder for attackers + * to execute many sequent attacks + */ + protected function pause() { + sleep(rand(1, 3)); + } + + /** + * Delete log entries by visitor id + */ + protected function clearLog($id) { + + $data = array_filter($this->logdata(), function($entry) use($id) { + return $entry['id'] !== $id; + }); + + data::write($this->logfile, $data, 'json'); + + // reset the logdata cache + $this->logdata = null; + + return $this->logdata(); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/file.php b/panel/app/src/panel/models/file.php new file mode 100644 index 0000000..0974be6 --- /dev/null +++ b/panel/app/src/panel/models/file.php @@ -0,0 +1,250 @@ +page()->uri('file') . '/' . $this->encodedFilename() . '/' . $action; + } + } + + public function encodedFilename() { + if(php_sapi_name() == 'cli-server') { + $filename = str_replace('.', '․', $this->filename()); + } else { + $filename = $this->filename(); + } + return rawurlencode($filename); + } + + public static function decodeFilename($filename) { + $filename = rawurldecode($filename); + if(php_sapi_name() == 'cli-server') { + $filename = str_replace('․', '.', $filename); + } + return $filename; + } + + public function url($action = null) { + if(empty($action)) { + return parent::url(); + } else if($action == 'preview') { + return parent::url() . '?' . $this->modified(); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + } + + public function menu() { + return new Menu($this); + } + + public function form($action, $callback) { + return panel()->form('files/' . $action, $this, $callback); + } + + public function filterInput($input) { + return $input; + } + + public function getBlueprintFields() { + return $this->blueprint()->files()->fields($this); + } + + public function getFormFields() { + return $this->getBlueprintFields()->toArray(); + } + + public function getFormData() { + return $this->meta()->toArray(); + } + + public function canHavePreview() { + return $this->isWebImage() or $this->extension() == 'svg'; + } + + public function isWebImage() { + $images = array('image/jpeg', 'image/gif', 'image/png'); + return in_array($this->mime(), $images); + } + + public function canHaveThumb() { + if(!$this->isWebImage()) { + return false; + } else if(kirby()->option('thumbs.driver') == 'gd') { + if($this->width() > 2048 or $this->height() > 2048) { + return false; + } else { + return true; + } + } else { + return true; + } + } + + public function rename($name, $safeName = true) { + + // keep the old state of the file object + $old = clone $this; + + if($name == $this->name()) return true; + + // check if the name should be sanitized + $safeName = $this->page()->blueprint()->files()->sanitize(); + + // rename and get the new filename + $filename = parent::rename($name, $safeName); + + // clean the thumbs folder + $this->page()->removeThumbs(); + + // trigger the rename hook + kirby()->trigger('panel.file.rename', array($this, $old)); + + } + + public function update($data = array(), $sort = null, $trigger = true) { + + if($data == 'sort') { + parent::update(array('sort' => $sort)); + kirby()->trigger('panel.file.sort', $this); + return true; + } + + // rename the file if necessary + if(!empty($data['_name'])) { + $filename = $this->rename($data['_name']); + } + + // remove the name url and info + unset($data['_name']); + unset($data['_info']); + unset($data['_link']); + + if(!empty($data)) { + parent::update($data); + } + + if($trigger) { + kirby()->trigger('panel.file.update', $this); + } + + } + + public function replace() { + new Uploader($this->page, $this); + } + + public function delete() { + + parent::delete(); + + // clean the thumbs folder + $this->page()->removeThumbs(); + + kirby()->trigger('panel.file.delete', $this); + + } + + public function icon($position = 'left') { + + switch($this->type()) { + case 'image': + return icon('file-image-o', $position); + break; + case 'document': + switch($this->extension()) { + case 'pdf': + return icon('file-pdf-o', $position); + break; + case 'doc': + case 'docx': + return icon('file-word-o', $position); + break; + case 'xls': + return icon('file-excel-o', $position); + break; + default: + return icon('file-text-o', $position); + break; + } + break; + case 'code': + return icon('file-code-o', $position); + break; + case 'audio': + return icon('file-audio-o', $position); + break; + case 'video': + return icon('file-video-o', $position); + break; + default: + return icon('file-archive-o', $position); + break; + } + + } + + public function dragText() { + if(kirby()->option('panel.kirbytext') === false) { + switch($this->type()) { + case 'image': + return '![' . $this->name() . '](' . parent::url() . ')'; + break; + default: + return '[' . $this->filename() . '](' . parent::url() . ')'; + break; + } + } else { + switch($this->type()) { + case 'image': + return '(image: ' . $this->filename() . ')'; + break; + default: + return '(file: ' . $this->filename() . ')'; + break; + } + } + } + + public function topbar($topbar) { + + $this->files()->topbar($topbar); + + $topbar->append($this->url('edit'), $this->filename()); + + } + + public function createMeta($triggerUpdateHook = true) { + + // save default meta + $meta = array(); + + foreach($this->page()->blueprint()->files()->fields($this) as $field) { + $meta[$field->name()] = $field->default(); + } + + $this->update($meta, null, $triggerUpdateHook); + + return $this; + + } + + public function blueprint() { + return $this->page->blueprint(); + } + + public function structure() { + return new Structure($this, 'file_' . $this->page()->id() . '_' . $this->filename() . '_' . $this->site()->lang()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/file/menu.php b/panel/app/src/panel/models/file/menu.php new file mode 100644 index 0000000..b6a6bf1 --- /dev/null +++ b/panel/app/src/panel/models/file/menu.php @@ -0,0 +1,69 @@ +page = $file->page(); + $this->file = $file; + } + + public function item($icon, $label, $attr = array()) { + + $a = new Brick('a', '', $attr); + $a->append(icon($icon, 'left')); + $a->append(l($label)); + + $li = new Brick('li'); + $li->append($a); + + return $li; + + } + + public function previewOption() { + return $this->item('play-circle-o', 'files.show.open', array( + 'href' => $this->file->url('preview'), + 'target' => '_blank' + )); + } + + public function editOption() { + return $this->item('pencil', 'files.index.edit', array( + 'href' => $this->file->url('edit'), + )); + } + + public function deleteOption() { + return $this->item('trash-o', 'files.show.delete', array( + 'href' => $this->file->url('delete'), + 'data-modal' => true, + )); + } + + public function html() { + + $list = new Brick('ul'); + $list->addClass('nav nav-list'); + $list->addClass('dropdown-list'); + + $list->append($this->previewOption()); + $list->append($this->editOption()); + $list->append($this->deleteOption()); + + return ''; + + } + + public function __toString() { + return (string)$this->html(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page.php b/panel/app/src/panel/models/page.php new file mode 100644 index 0000000..1be4764 --- /dev/null +++ b/panel/app/src/panel/models/page.php @@ -0,0 +1,644 @@ +cache['blueprint'])) return $this->cache['blueprint']; + + $blueprint = $this->intendedTemplate(); + + if(!Blueprint::exists($blueprint)) { + $blueprint = $this->template(); + } + + return $this->cache['blueprint'] = new Blueprint($blueprint); + + } + + public function createNum($to = null) { + + $parent = $this->parent(); + $params = $parent->blueprint()->pages()->num(); + + switch($params->mode()) { + case 'zero': + return 0; + break; + case 'date': + if($to = $this->date($params->format(), $params->field())) { + return $to; + } else { + return date($params->format()); + } + break; + default: + + $visibleSiblings = $parent->children()->visible(); + + if($to == 'last') { + $to = $visibleSiblings->count() + 1; + } else if($to == 'first') { + $to = 1; + } else if(is_null($to)) { + $to = $this->num(); + } + + if(!v::num($to)) return false; + + if($to <= 0) return 1; + + if($this->isInvisible()) { + $limit = $visibleSiblings->count() + 1; + } else { + $limit = $visibleSiblings->count(); + } + + if($limit < $to) { + $to = $limit; + } + + return intval($to); + break; + } + + } + + public function uri($action = null) { + if(empty($action)) { + return parent::uri(); + } else { + return 'pages/' . $this->id() . '/' . $action; + } + } + + public function url($action = null) { + if(empty($action)) { + return parent::url(); + } else if($action == 'preview') { + if($previewSetting = $this->blueprint()->preview()) { + switch($previewSetting) { + case 'parent': + return $this->parent() ? $this->parent()->url() : $this->url(); + break; + case 'first-child': + return $this->children()->first() ? $this->children()->first()->url() : false; + break; + case 'last-child': + return $this->children()->last() ? $this->children()->last()->url() : false; + break; + default: + return $this->url(); + break; + } + } else { + return false; + } + } else if($this->site->multilang() and $lang = $this->site->language($action)) { + return parent::url($lang->code()); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + } + + public function form($action, $callback) { + return panel()->form('pages/' . $action, $this, $callback); + } + + public function structure() { + return new Structure($this, 'page_' . $this->id() . '_' . $this->site->lang()); + } + + public function getFormData() { + + // get the latest content from the text file + $data = $this->content()->toArray(); + + // make sure the title is always there + $data['title'] = $this->title(); + + // add the changes to the content array + $data = array_merge($data, $this->changes()->get()); + + return $data; + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function getFormFields() { + + $fields = $this->getBlueprintFields()->toArray(); + + // add the title as hidden field + if(!isset($fields['title'])) { + $fields['title'] = array( + 'type' => 'hidden', + 'name' => 'title' + ); + } else { + // make sure the title field always has the type title + $fields['title']['type'] = 'title'; + } + + return $fields; + + } + + public function children() { + return new Children($this); + } + + public function canSortFiles() { + return $this->blueprint()->files()->sortable(); + } + + public function files() { + return new Files($this); + } + + public function addButton() { + try { + return new AddButton($this); + } catch(Exception $e) { + return false; + } + } + + public function menu($position = 'sidebar') { + return new Menu($this, $position); + } + + public function filterInput($input) { + return $input; + } + + public function changes() { + return new Changes($this); + } + + public function maxSubpages() { + $max = $this->blueprint()->pages()->max(); + // if max subpages is null, use the biggest 32bit integer + // will never be reached anyway. Kirby is not made for that scale :) + return is_null($max) ? 2147483647 : $max; + } + + public function maxFiles() { + $max = $this->blueprint()->files()->max(); + // see: maxSubpages + return is_null($max) ? 2147483647 : $max; + } + + public function canHaveSubpages() { + return $this->maxSubpages() !== 0; + } + + public function canShowSubpages() { + return ($this->blueprint()->pages()->hide() !== true and $this->canHaveSubpages()); + } + + public function canHaveFiles() { + return $this->maxFiles() !== 0; + } + + public function canShowFiles() { + return ($this->blueprint()->files()->hide() !== true and $this->canHaveFiles()); + } + + public function canHaveMoreSubpages() { + if(!$this->canHaveSubpages()) { + return false; + } else if($this->children()->count() >= $this->maxSubpages()) { + return false; + } else { + return true; + } + } + + public function canHaveMoreFiles() { + if(!$this->canHaveFiles()) { + return false; + } else if($this->files()->count() >= $this->maxFiles()) { + return false; + } else { + return true; + } + } + + public function canShowPreview() { + return $this->blueprint()->options()->preview(); + } + + public function canChangeStatus() { + return (!$this->isErrorPage() and $this->blueprint()->options()->status()) ? true : false; + } + + public function canChangeUrl() { + if($this->isHomePage() or $this->isErrorPage() or $this->blueprint()->options()->url() === false) { + return false; + } else { + return true; + } + } + + public function canChangeTemplate() { + if($this->isHomePage() or $this->isErrorPage() or $this->blueprint()->options()->template() === false) { + return false; + } else { + return $this->parent()->blueprint()->pages()->template()->count() > 1; + } + } + + public function move($uid) { + + $old = clone($this); + + if(!$this->canChangeUrl()) { + throw new Exception(l('pages.url.error.rights')); + } + + $site = panel()->site(); + $changes = $this->changes()->get(); + + $this->changes()->discard(); + + if($site->multilang() and $site->language()->code() != $site->defaultLanguage()->code()) { + parent::update(array( + 'URL-Key' => $uid + )); + } else { + parent::move($uid); + } + + $this->changes()->update($changes); + + // remove all thumbs for the old id + $old->removeThumbs(); + + // hit the hook + kirby()->trigger('panel.page.move', array($this, $old)); + + } + + public function _sort($to) { + if(is_dir($this->root())) { + return parent::sort($to); + } else { + return false; + } + } + + public function sort($to = null) { + + if($this->isErrorPage()) { + return $this->num(); + } + + // don't sort pages without permission to change the status + if($this->isInvisible() && !$this->canChangeStatus()) { + return false; + } + + // store the old number + $oldNum = $this->num(); + + // run the sorter + $this->sorter()->to($to); + + // run the hook if the number changed + if($oldNum != $this->num()) { + // hit the hook + kirby()->trigger('panel.page.sort', $this); + } + + return $this->num(); + + } + + public function sorter() { + return new Sorter($this); + } + + public function hide() { + + // don't hide pages, which are not allowed to change their status + if(!$this->canChangeStatus()) { + return false; + } + + parent::hide(); + $this->sorter()->hide(); + kirby()->trigger('panel.page.hide', $this); + + } + + public function toggle($position) { + + $mode = $this->parent()->blueprint()->pages()->num()->mode(); + $position = intval($position); + + if(($mode == 'default' && $position > 0) || !$this->isVisible()) { + $this->sort($position); + } else { + $this->hide(); + } + + } + + public function hasNoTitleField() { + $fields = $this->getFormFields(); + return empty($fields['title']); + } + + public function isHidden() { + return $this->blueprint()->hide() === true; + } + + public function isDeletable($exception = false) { + + if($this->isHomePage()) { + $error = 'pages.delete.error.home'; + } else if($this->isErrorPage()) { + $error = 'pages.delete.error.error'; + } else if($this->hasChildren()) { + $error = 'pages.delete.error.children'; + } else if(!$this->blueprint()->deletable() or !$this->blueprint()->options()->delete()) { + $error = 'pages.delete.error.blocked'; + } else { + return true; + } + + if($exception) { + throw new Exception($error); + } else { + return false; + } + + } + + public function sidebar() { + return new Sidebar($this); + } + + public function addToHistory() { + panel()->user()->history()->add($this); + } + + public function updateNum() { + + // make sure that the sorting number is correct + if($this->isVisible()) { + $this->sort($this->num()); + } + + return $this->num(); + + } + + public function updateUid() { + + // auto-update the uid if the sorting mode is set to zero + if($this->parent()->blueprint()->pages()->num()->mode() == 'zero') { + $uid = str::slug($this->title()); + $this->move($uid); + } + return $this->uid(); + + } + + public function update($data = array(), $lang = null) { + + $this->changes()->discard(); + + parent::update($data, $lang); + + // update the number if the date field + // changed for example + $this->updateNum(); + + kirby()->trigger('panel.page.update', $this); + + // add the page to the history + $this->addToHistory(); + + } + + public function upload() { + new Uploader($this); + } + + public function delete($force = false) { + + // delete the page + parent::delete(); + + // resort the siblings + $this->sorter()->delete(); + + // remove unsaved changes + $this->changes()->discard(); + + // delete all associated thumbs + $this->removeThumbs(); + + // hit the hook + kirby()->trigger('panel.page.delete', $this); + + } + + public function icon($position = 'left') { + return icon($this->blueprint()->icon(), $position); + } + + public function dragText() { + if(c::get('panel.kirbytext') === false) { + return '[' . $this->title() . '](' . $this->url() . ')'; + } else { + return '(link: ' . $this->uri() . ' text: ' . $this->title() . ')'; + } + } + + public function displayNum() { + + if($this->isInvisible()) { + return '—'; + } else { + + $numberSettings = $this->parent()->blueprint()->pages()->num(); + + switch($numberSettings->mode()) { + case 'zero': + if($numberSettings->display()) { + // customer number display + return $this->{$numberSettings->display()}(); + } else { + // alphabetic display numbers + return str::substr($this->title(), 0, 1); + } + break; + case 'date': + return $this->date($numberSettings->display(), $numberSettings->field()); + break; + default: + if($numberSettings->display()) { + // customer number display + return $this->{$numberSettings->display()}(); + } else { + // regular number display + return intval($this->num()); + } + break; + } + + } + + } + + public function topbar(Topbar $topbar) { + + foreach($this->parents()->flip() as $item) { + $topbar->append($item->url('edit'), $item->title()); + } + + $topbar->append($this->url('edit'), $this->title()); + + if($topbar->view == 'subpages/index') { + $topbar->append($this->url('subpages'), l('subpages')); + } + + $topbar->html .= new Snippet('languages', array( + 'languages' => $this->site()->languages(), + 'language' => $this->site()->language(), + )); + + } + + public function changeTemplate($newTemplate) { + + $oldTemplate = $this->intendedTemplate(); + + if($newTemplate == $oldTemplate) return true; + + if($this->site()->multilang()) { + + foreach($this->site()->languages() as $lang) { + $old = $this->textfile(null, $lang->code()); + $new = $this->textfile($newTemplate, $lang->code()); + f::move($old, $new); + $this->reset(); + $this->updateForNewTemplate($oldTemplate, $newTemplate, $lang->code()); + } + + } else { + $old = $this->textfile(); + $new = $this->textfile($newTemplate); + f::move($old, $new); + $this->reset(); + $this->updateForNewTemplate($oldTemplate, $newTemplate); + } + + return true; + + } + + public function prepareForNewTemplate($oldTemplate, $newTemplate, $language = null) { + + $data = array(); + $incompatible = array(); + $content = $this->content($language); + $oldBlueprint = new Blueprint($oldTemplate); + $oldFields = $oldBlueprint->fields($this); + $newBlueprint = new Blueprint($newTemplate); + $newFields = $newBlueprint->fields($this); + + // log + $removed = array(); + $replaced = array(); + $added = array(); + + // first overwrite everything + foreach($oldFields as $oldField) { + $data[$oldField->name()] = null; + } + + // now go through all new fileds and compare them to the old field types + foreach($newFields as $newField) { + + $oldField = $oldFields->{$newField->name()}; + + // only take data from fields with matching names and types + if($oldField and $oldField->type() == $newField->type()) { + $data[$newField->name()] = $content->get($newField->name())->value(); + } else { + $data[$newField->name()] = $newField->default(); + + if($oldField) { + $replaced[$newField->name()] = $newField->label(); + } else { + $added[$newField->name()] = $newField->label(); + } + + } + + } + + foreach($data as $name => $content) { + if(is_null($content)) $removed[$name] = $oldFields->{$name}->label(); + } + + return array( + 'data' => $data, + 'removed' => $removed, + 'replaced' => $replaced, + 'added' => $added + ); + + } + + public function updateForNewTemplate($oldTemplate, $newTemplate, $language = null) { + $prep = $this->prepareForNewTemplate($oldTemplate, $newTemplate, $language); + $this->update($prep['data'], $language); + } + + /** + * Clean the thumbs folder for the page + * + */ + public function removeThumbs() { + return dir::remove($this->kirby()->roots()->thumbs() . DS . $this->id()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/addbutton.php b/panel/app/src/panel/models/page/addbutton.php new file mode 100644 index 0000000..e66b1e8 --- /dev/null +++ b/panel/app/src/panel/models/page/addbutton.php @@ -0,0 +1,21 @@ +page = $page; + $this->modal = true; + $this->url = $this->page->url('add'); + + if(!$this->page->canHaveMoreSubpages()) { + throw new Exception(l('subpages.add.error.more')); + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint.php b/panel/app/src/panel/models/page/blueprint.php new file mode 100644 index 0000000..e988d37 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint.php @@ -0,0 +1,126 @@ +load($name); + + $this->title = a::get($this->yaml, 'title', 'Page'); + $this->preview = a::get($this->yaml, 'preview', 'page'); + $this->deletable = a::get($this->yaml, 'deletable', true); + $this->icon = a::get($this->yaml, 'icon', 'file-o'); + $this->hide = a::get($this->yaml, 'hide', false); + $this->type = a::get($this->yaml, 'type', 'page'); + $this->pages = new Pages(a::get($this->yaml, 'pages', true)); + $this->files = new Files(a::get($this->yaml, 'files', true)); + $this->options = new Options(a::get($this->yaml, 'options', array())); + + } + + public function load($name) { + + // make sure there's no path included in the name + $name = basename(strtolower($name)); + + if(isset(static::$cache[$name])) { + $this->file = static::$cache[$name]['file']; + $this->name = static::$cache[$name]['name']; + $this->yaml = static::$cache[$name]['yaml']; + return true; + } + + // find the matching blueprint file + $file = kirby()->get('blueprint', $name); + + if($file) { + + $this->file = $file; + $this->name = $name; + $this->yaml = data::read($this->file, 'yaml'); + + // remove the broken first line + unset($this->yaml[0]); + + static::$cache[$name] = array( + 'file' => $this->file, + 'name' => $this->name, + 'yaml' => $this->yaml + ); + + return true; + + } else if($name == 'default') { + throw new Exception(l('blueprints.error.default.missing')); + } else { + return $this->load('default'); + } + + } + + public function fields($model) { + $fields = a::get($this->yaml, 'fields', array()); + return new Fields($fields, $model); + } + + static public function exists($name) { + return kirby()->get('blueprint', $name) ? true : false; + } + + static public function all() { + + $files = dir::read(static::$root); + $result = array_keys(kirby()->get('blueprint')); + $home = kirby()->option('home', 'home'); + $error = kirby()->option('error', 'error'); + + foreach($files as $file) { + + $name = f::name($file); + + if($name != 'site' and $name != $home and $name != $error) { + $result[] = $name; + } + + } + + return $result; + + } + + public function __toString() { + return $this->name; + } + +} diff --git a/panel/app/src/panel/models/page/blueprint/field.php b/panel/app/src/panel/models/page/blueprint/field.php new file mode 100644 index 0000000..6bbd828 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/field.php @@ -0,0 +1,115 @@ +_extend($params); + } + + if(a::get($params, 'name') == 'title') { + $params['type'] = 'title'; + + if(!isset($params['required'])) { + $params['required'] = true; + } + } + + if(empty($params['type'])) { + $params['type'] = 'text'; + } + + // lowercase the type + $params['type'] = strtolower($params['type']); + + // register the parent model + $params['model'] = $model; + + // try to fetch the parent page from the model + if(is_a($model, 'Page')) { + $params['page'] = $model; + } else if(is_a($model, 'File')) { + $params['page'] = $model->page(); + } + + // create the default value + $params['default'] = $this->_default(a::get($params, 'default')); + + parent::__construct($params); + + } + + + public function _extend($params) { + + $extends = $params['extends']; + $snippet = f::resolve(kirby()->roots()->blueprints() . DS . 'fields' . DS . $extends, array('yml', 'php', 'yaml')); + + if(empty($snippet)) { + throw new Exception(l('fields.error.extended')); + } + + $yaml = data::read($snippet, 'yaml'); + $params = a::merge($yaml, $params); + + return $params; + + } + + public function _default($default) { + + if($default === true) { + return 'true'; + } else if($default === false) { + return 'false'; + } else if(empty($default) and strlen($default) == 0) { + return ''; + } else if(is_string($default)) { + return $default; + } else { + $type = a::get($default, 'type'); + + switch($type) { + case 'date': + $format = a::get($default, 'format', 'Y-m-d'); + return date($format); + break; + case 'datetime': + $format = a::get($default, 'format', 'Y-m-d H:i:s'); + return date($format); + break; + case 'user': + $user = isset($default['user']) ? site()->users()->find($default['user']) : site()->user(); + if(!$user) return ''; + return (isset($default['field']) and $default['field'] != 'password') ? $user->{$default['field']}() : $user->username(); + break; + case 'structure': + return "\n" . \data::encode(array($default), 'yaml') . "\n"; + break; + default: + return $default; + break; + } + + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/fields.php b/panel/app/src/panel/models/page/blueprint/fields.php new file mode 100644 index 0000000..e334a0a --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/fields.php @@ -0,0 +1,48 @@ + $field) { + + // sanitize the name + $name = str_replace('-','_', str::lower($name)); + + // import a field by name + if(is_string($field)) { + $field = array( + 'name' => $name, + 'extends' => $field + ); + } + + // add the name to the field + $field['name'] = $name; + + // create the field object + $field = new Field($field, $model); + + // append it to the collection + $this->append($name, $field); + + } + + } + + public function toArray($callback = null) { + $result = array(); + foreach($this->data as $field) { + $result[$field->name()] = $field->toArray(); + } + return $result; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/files.php b/panel/app/src/panel/models/page/blueprint/files.php new file mode 100644 index 0000000..02385a9 --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/files.php @@ -0,0 +1,57 @@ +params = $params; + + if($params === false) { + $this->fields = array(); + $this->type = array(); + $this->size = false; + $this->width = false; + $this->height = false; + $this->max = 0; + $this->hide = true; + $this->sortable = false; + + } else if(is_array($params)) { + $this->fields = a::get($params, 'fields', $this->fields); + $this->type = a::get($params, 'type', $this->type); + if (!is_array($this->type)) + $this->type = array($this->type); + $this->size = a::get($params, 'size', $this->size); + $this->width = a::get($params, 'width', $this->width); + $this->height = a::get($params, 'height', $this->height); + $this->max = a::get($params, 'max', $this->max); + $this->hide = a::get($params, 'hide', $this->hide); + $this->sort = a::get($params, 'sort', $this->sort); + $this->sortable = a::get($params, 'sortable', $this->sortable); + $this->sanitize = a::get($params, 'sanitize', true); + } + + } + + public function fields($file) { + return new Fields($this->fields, $file); + } + +} diff --git a/panel/app/src/panel/models/page/blueprint/options.php b/panel/app/src/panel/models/page/blueprint/options.php new file mode 100644 index 0000000..305c96e --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/options.php @@ -0,0 +1,46 @@ +preview = a::get($options, 'preview', true); + $this->status = a::get($options, 'status', true); + $this->template = a::get($options, 'template', true); + $this->url = a::get($options, 'url', true); + $this->delete = a::get($options, 'delete', true); + } + + public function preview() { + return $this->preview; + } + + public function status() { + return $this->status; + } + + public function template() { + return $this->template; + } + + public function url() { + return $this->url; + } + + public function delete() { + return $this->delete; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/blueprint/pages.php b/panel/app/src/panel/models/page/blueprint/pages.php new file mode 100644 index 0000000..9540c0e --- /dev/null +++ b/panel/app/src/panel/models/page/blueprint/pages.php @@ -0,0 +1,96 @@ +template = blueprint::all(); + } else if($params === false) { + $this->limit = 0; + $this->max = 0; + $this->sortable = false; + $this->hide = true; + } else if(is_array($params)) { + $template = a::get($params, 'template'); + if($template == false) { + $this->template = blueprint::all(); + } else if(is_array($template)) { + $this->template = $template; + } else { + $this->template = array($template); + } + $this->sort = a::get($params, 'sort', $this->sort); + $this->sortable = a::get($params, 'sortable', $this->sortable); + $this->limit = a::get($params, 'limit', $this->limit); + $this->num = a::get($params, 'num', $this->num); + $this->max = a::get($params, 'max', $this->max); + $this->hide = a::get($params, 'hide', $this->hide); + $this->build = a::get($params, 'build', $this->build); + } else if(is_string($params)) { + $this->template = array($params); + } + + } + + public function template() { + $result = array(); + foreach($this->template as $t) { + $result[$t] = new Blueprint($t); + } + return new Collection($result); + } + + public function num() { + + $obj = new Obj(); + + $obj->mode = 'default'; + $obj->field = null; + $obj->format = null; + $obj->display = null; + + if(is_array($this->num)) { + foreach($this->num as $k => $v) $obj->$k = $v; + } else if(!empty($this->num)) { + $obj->mode = $this->num; + } + + switch($obj->mode) { + case 'field': + isset($obj->field) or $obj->field = 'num'; + break; + case 'date': + // switch the default date format by configured handler + $defaultDateFormat = kirby()->option('date.handler') == 'strftime' ? '%Y%m%d' : 'Ymd'; + $defaultDisplayFormat = kirby()->option('date.handler') == 'strftime' ? '%Y/%m/%d' : 'Y/m/d'; + + // set the defaults + isset($obj->field) or $obj->field = 'date'; + isset($obj->format) or $obj->format = $defaultDateFormat; + isset($obj->display) or $obj->display = $defaultDisplayFormat; + break; + } + + return $obj; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/changes.php b/panel/app/src/panel/models/page/changes.php new file mode 100644 index 0000000..bae4bd2 --- /dev/null +++ b/panel/app/src/panel/models/page/changes.php @@ -0,0 +1,121 @@ +model = $model; + } + + public function data() { + return s::get('changes', array()); + } + + public function id() { + $site = panel()->site(); + if($site->multilang()) { + return $site->language()->code() . '-' . sha1($this->model->id()); + } else { + return sha1($this->model->id()); + } + } + + public function keep() { + + $blueprint = $this->model->blueprint(); + $fields = $blueprint->fields($this->model); + $form = new Form($fields->toArray()); + $data = $this->model->filterInput($form->serialize()); + $old = $this->model->content()->toArray(); + + if($data != $old) { + $this->update($data); + } + + } + + public function discard($field = null) { + + $store = $this->data(); + + if(is_null($field)) { + unset($store[$this->id()]); + } else { + unset($store[$this->id()][$field]); + } + + s::set('changes', $store); + + // remove all structures from the session as well + $this->model->structure()->reset(); + + return $store; + + } + + public function differ() { + + $data = $this->get(); + $changes = false; + + foreach($data as $field => $value) { + + $object = $this->model->{$field}(); + + if(!method_exists($object, '__toString')) { + continue; + } + + if((string)$object !== $value) { + $changes = true; + } + + } + + return $changes; + + } + + public function get($field = null) { + + $data = (array)a::get($this->data(), $this->id()); + + if(!is_null($field)) { + return a::get($data, $field); + } else { + return $data; + } + + } + + public function update($field, $data = null) { + + if(is_null($data) and is_array($field)) { + $store = $this->data(); + $store[$this->id()] = $field; + } else if(is_string($field)) { + $store = $this->data(); + if(!isset($store[$this->id()]) or !is_array($store[$this->id()])) { + $store[$this->id()] = array(); + } + $store[$this->id()][$field] = $data; + } + + s::set('changes', $store); + return $store; + + } + + public function flush() { + s::set('changes', array()); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/menu.php b/panel/app/src/panel/models/page/menu.php new file mode 100644 index 0000000..692d523 --- /dev/null +++ b/panel/app/src/panel/models/page/menu.php @@ -0,0 +1,166 @@ +page = $page; + $this->parent = $page->parent(); + $this->blueprint = $page->blueprint(); + $this->position = $position; + } + + public function item($icon, $label, $attr = array()) { + + $a = new Brick('a', '', $attr); + $a->append(icon($icon, 'left')); + $a->append(l($label) ?: $label); + + $li = new Brick('li'); + $li->append($a); + + return $li; + + } + + public function modalUrl($action) { + + if($this->position == 'context') { + if($this->parent->isSite()) { + $redirect = '/'; + } else { + $redirect = $this->parent->uri('edit'); + } + return $this->page->url($action) . '?_redirect=' . $redirect; + } else { + return $this->page->url($action); + } + + } + + public function previewOption() { + if($preview = $this->page->url('preview') and $this->page->canShowPreview()) { + return $this->item('play-circle-o', 'pages.show.preview', array( + 'href' => $preview, + 'target' => '_blank', + 'title' => 'p', + 'data-shortcut' => 'p', + )); + } else { + return false; + } + } + + public function editOption() { + if($this->position == 'context') { + return $this->item('pencil', 'pages.show.subpages.edit', array( + 'href' => $this->page->url('edit'), + )); + } + } + + public function statusOption() { + + if($this->page->canChangeStatus()) { + + if($this->page->isInvisible()) { + $icon = 'toggle-off'; + $label = 'pages.show.invisible'; + } else { + $icon = 'toggle-on'; + $label = 'pages.show.visible'; + } + + return $this->item($icon, $label, array( + 'href' => $this->modalUrl('toggle'), + 'data-modal' => true, + )); + + } else { + return false; + } + + } + + public function templateOption() { + if($this->page->canChangeTemplate()) { + return $this->item('file-code-o', l('pages.show.template') . ': ' . i18n($this->page->blueprint()->title()), array( + 'href' => $this->modalUrl('template'), + 'data-modal' => true, + 'data-shortcut' => 't', + )); + } else { + return false; + } + } + + public function urlOption() { + if($this->page->canChangeUrl()) { + return $this->item('chain', 'pages.show.changeurl', array( + 'href' => $this->modalUrl('url'), + 'title' => 'u', + 'data-shortcut' => 'u', + 'data-modal' => true, + )); + } else { + return false; + } + } + + public function deleteOption() { + if($this->page->isDeletable()) { + return $this->item('trash-o', 'pages.show.delete', array( + 'href' => $this->modalUrl('delete'), + 'title' => '#', + 'data-shortcut' => '#', + 'data-modal' => true, + )); + } else { + return false; + } + } + + public function html() { + + $list = new Brick('ul'); + $list->addClass('nav nav-list'); + + if($this->position == 'sidebar') { + $list->addClass('sidebar-list'); + } else { + $list->addClass('dropdown-list'); + } + + $list->append($this->previewOption()); + $list->append($this->editOption()); + $list->append($this->statusOption()); + $list->append($this->templateOption()); + $list->append($this->urlOption()); + $list->append($this->deleteOption()); + + if($this->position == 'context') { + return ''; + } else { + return $list; + } + + } + + public function __toString() { + try { + return (string)$this->html(); + } catch(Exception $e) { + return (string)$e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/sidebar.php b/panel/app/src/panel/models/page/sidebar.php new file mode 100644 index 0000000..4e6dee2 --- /dev/null +++ b/panel/app/src/panel/models/page/sidebar.php @@ -0,0 +1,78 @@ +page = $page; + $this->blueprint = $page->blueprint(); + } + + public function subpages() { + + if(!$this->page->canShowSubpages()) { + return null; + } + + // fetch all subpages in the right order + $children = $this->page->children()->paginated('sidebar'); + + // create the pagination snippet + $pagination = new Snippet('pagination', array( + 'pagination' => $children->pagination(), + 'nextUrl' => $children->pagination()->nextPageUrl(), + 'prevUrl' => $children->pagination()->prevPageUrl(), + )); + + // create the snippet and fill it with all data + return new Snippet('pages/sidebar/subpages', array( + 'title' => l('pages.show.subpages.title'), + 'page' => $this->page, + 'subpages' => $children, + 'addbutton' => $this->page->addButton(), + 'pagination' => $pagination, + )); + + } + + public function files() { + + if(!$this->page->canShowFiles()) { + return null; + } + + return new Snippet('pages/sidebar/files', array( + 'page' => $this->page, + 'files' => $this->page->files(), + )); + + } + + public function render() { + + // create the monster sidebar + return new Snippet('pages/sidebar', array( + 'page' => $this->page, + 'menu' => $this->page->menu('sidebar'), + 'subpages' => $this->subpages(), + 'files' => $this->files(), + )); + + } + + public function __toString() { + try { + return (string)$this->render(); + } catch(Exception $e) { + return $e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/sorter.php b/panel/app/src/panel/models/page/sorter.php new file mode 100644 index 0000000..a2aa0f0 --- /dev/null +++ b/panel/app/src/panel/models/page/sorter.php @@ -0,0 +1,123 @@ +page = $page; + $this->parent = $page->parent(); + $this->params = $this->parent->blueprint()->pages()->num(); + $this->siblings = $this->parent->children()->visible(); + + } + + protected function execute() { + + switch($this->params->mode()) { + case 'date': + $this->date(); + break; + case 'zero': + $this->zero(); + break; + default: + $this->num(); + break; + } + + } + + protected function zero() { + foreach($this->siblings as $sibling) { + $sibling->_sort(0); + } + } + + protected function date() { + + foreach($this->siblings as $sibling) { + + // get the date + $date = $sibling->date($this->params->format(), $this->params->field()); + + // take the current date if the date is missing + if(!$date) { + $handler = kirby()->option('date.handler'); + $date = $handler($this->params->format()); + } + + $sibling->_sort($date); + + } + + } + + protected function num() { + + // make sure the siblings are sorted correctly + $this->siblings = $this->siblings->not($this->page)->sortBy('num', 'asc'); + + // special keywords and sanitization + if($this->to == 'last') { + $this->to = $this->siblings->count() + 1; + } else if($this->to == 'first') { + $this->to = 1; + } else if($this->to === false) { + $this->to = false; + } else if($this->to < 1) { + $this->to = 1; + } + + // start the index + $n = 0; + + if($this->to === false) { + foreach($this->siblings as $sibling) { + $n++; $sibling->_sort($n); + } + } else { + + // go through all items before the selected page + foreach($this->siblings->slice(0, $this->to - 1) as $sibling) { + $n++; $sibling->_sort($n); + } + + // add the selected page + $n++; $this->page->_sort($n); + + // go through all the items after the selected page + foreach($this->siblings->slice($this->to - 1) as $sibling) { + $n++; $sibling->_sort($n); + } + + } + + } + + public function to($to) { + $this->siblings->data[$this->page->id()] = $this->page; + $this->to = $to; + $this->execute(); + } + + public function delete() { + $this->siblings = $this->siblings->not($this->page); + $this->to = false; + $this->execute(); + } + + public function hide() { + $this->siblings = $this->siblings->not($this->page); + $this->to = false; + $this->execute(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/page/uploader.php b/panel/app/src/panel/models/page/uploader.php new file mode 100644 index 0000000..09da27d --- /dev/null +++ b/panel/app/src/panel/models/page/uploader.php @@ -0,0 +1,188 @@ +page = $page; + $this->file = $file; + $this->blueprint = $page->blueprint(); + $this->filename = $this->blueprint->files()->sanitize() ? '{safeFilename}' : '{filename}'; + + if($this->file) { + $this->replace(); + } else { + $this->upload(); + } + + } + + public function upload() { + + // check if more files can be uploaded for the page + if(!$this->page->canHaveMoreFiles()) { + throw new Exception(l('files.add.error.max')); + } + + $upload = new Upload($this->page->root() . DS . $this->filename, array( + 'overwrite' => true, + 'accept' => function($file) { + + $callback = kirby()->option('panel.upload.accept'); + + if(is_callable($callback)) { + return call($callback, $file); + } else { + return true; + } + + } + )); + + $file = $this->move($upload); + + // create the initial meta file + // without triggering the update hook + $file->createMeta(false); + + // make sure that the file is being marked as updated + touch($file->root()); + + // clean the thumbs folder + $this->page->removeThumbs(); + + kirby()->trigger('panel.file.upload', $file); + + } + + public function replace() { + + $file = $this->file; + $upload = new Upload($file->root(), array( + 'overwrite' => true, + 'accept' => function($upload) use($file) { + if($upload->mime() != $file->mime()) { + throw new Error(l('files.replace.error.type')); + } + } + )); + + $file = $this->move($upload); + + // make sure that the file is being marked as updated + touch($file->root()); + + // clean the thumbs folder + $this->page->removeThumbs(); + + kirby()->trigger('panel.file.replace', $file); + + } + + public function move($upload) { + + // flush all cached files + $this->page->reset(); + + // get the file object from the upload + $uploaded = $upload->file(); + + // check if the upload worked + if(!$uploaded) { + throw new Exception($upload->error()->getMessage()); + } + + // check if the page has such a file + $file = $this->page->file($uploaded->filename()); + + // delete the upload if something went wrong + if(!$file) { + $uploaded->delete(); + throw new Exception(l('files.error.missing.file')); + } + + try { + // security checks + $this->checkUpload($file); + return $file; + } catch(Exception $e) { + $file->delete(); + throw $e; + } + + } + + public function checkUpload($file) { + + $filesettings = $this->blueprint->files(); + $forbiddenExtensions = array('php', 'html', 'htm', 'exe', kirby()->option('content.file.extension', 'txt')); + $forbiddenMimes = array_merge(f::$mimes['php'], array('text/html', 'application/x-msdownload')); + $extension = strtolower($file->extension()); + + // files without extension are not allowed + if(empty($extension)) { + throw new Exception(l('files.add.error.extension.missing')); + } + + // block forbidden extensions + if(in_array($extension, $forbiddenExtensions)) { + throw new Exception(l('files.add.error.extension.forbidden')); + } + + // especially block any connection that contains php + if(str::contains($extension, 'php')) { + throw new Exception(l('files.add.error.extension.forbidden')); + } + + // block forbidden mimes + if(in_array(strtolower($file->mime()), $forbiddenMimes)) { + throw new Exception(l('files.add.error.mime.forbidden')); + } + + // Block htaccess files + if(strtolower($file->filename()) == '.htaccess') { + throw new Exception(l('files.add.error.htaccess')); + } + + // Block invisible files + if(str::startsWith($file->filename(), '.')) { + throw new Exception(l('files.add.error.invisible')); + } + + // Files blueprint option 'type' + if(count($filesettings->type()) > 0 and !in_array($file->type(), $filesettings->type())) { + throw new Exception(l('files.add.blueprint.type.error') . implode(', ', $filesettings->type())); + } + + // Files blueprint option 'size' + if($filesettings->size() and f::size($file->root()) > $filesettings->size()) { + throw new Exception(l('files.add.blueprint.size.error') . f::niceSize($filesettings->size())); + } + + // Files blueprint option 'width' + if($file->type() == 'image' and $filesettings->width() and $file->width() > $filesettings->width()) { + throw new Exception('Page only allows image width of ' . $filesettings->width().'px'); + } + + // Files blueprint option 'height' + if($file->type() == 'image' and $filesettings->height() and $file->height() > $filesettings->height()) { + throw new Exception('Page only allows image height of ' . $filesettings->height().'px'); + } + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/site.php b/panel/app/src/panel/models/site.php new file mode 100644 index 0000000..e6de5b2 --- /dev/null +++ b/panel/app/src/panel/models/site.php @@ -0,0 +1,222 @@ +cache['blueprint'])) return $this->cache['blueprint']; + return $this->cache['blueprint'] = new Blueprint('site'); + } + + public function changes() { + return new Changes($this); + } + + public function uri($action = null) { + if(empty($action)) { + return parent::uri(); + } else { + return 'site/' . $action; + } + } + + public function url($action = null) { + + if(empty($action)) { + return parent::url(); + } else if($action == 'edit') { + return panel()->urls()->index() . '/options'; + } else if($action == 'preview') { + return parent::url(); + } else if($this->multilang() and in_array($action, $this->languages()->codes())) { + return parent::url($action); + } else { + return panel()->urls()->index() . '/' . $this->uri($action); + } + + } + + public function form($action, $callback) { + return panel()->form('pages/' . $action, $this, $callback); + } + + public function getFormData() { + + // get the latest content from the text file + $data = $this->content()->toArray(); + + // make sure the title is always there + $data['title'] = $this->title(); + + return $data; + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function getFormFields() { + return $this->getBlueprintFields()->toArray(); + } + + public function canSortFiles() { + return $this->blueprint()->files()->sortable(); + } + + public function files() { + return new Files($this); + } + + public function children() { + return new Children($this); + } + + public function filterInput($input) { + $data = array(); + foreach($this->content()->toArray() as $key => $value) { + $data[$key] = null; + } + return array_merge($data, $input); + } + + public function update($input = array(), $lang = null) { + + $data = $this->filterInput($input); + + $this->changes()->discard(); + + parent::update($data, $lang); + + kirby()->trigger('panel.site.update', $this); + + } + + public function sidebar() { + return new Sidebar($this); + } + + public function upload() { + return new Uploader($this); + } + + public function addButton() { + try { + return new AddButton($this); + } catch(Exception $e) { + return false; + } + } + + public function topbar(Topbar $topbar) { + + if($topbar->view == 'options/index') { + $topbar->append(purl('options'), l('metatags')); + } + + if($topbar->view == 'subpages/index') { + $topbar->append($this->url('subpages'), l('subpages')); + } + + $topbar->html .= new Snippet('languages', array( + 'languages' => $this->languages(), + 'language' => $this->language(), + )); + + } + + public function users() { + return new Users(); + } + + public function user($username = null) { + if(is_null($username)) return User::current(); + try { + return new User($username); + } catch(Exception $e) { + return null; + } + } + + public function delete($force = false) { + throw new Exception(l('site.delete.error')); + } + + public function maxSubpages() { + $max = $this->blueprint()->pages()->max(); + // if max subpages is null, use the biggest 32bit integer + // will never be reached anyway. Kirby is not made for that scale :) + return is_null($max) ? 2147483647 : $max; + } + + public function maxFiles() { + $max = $this->blueprint()->files()->max(); + // see: maxSubpages + return is_null($max) ? 2147483647 : $max; + } + + public function canHaveSubpages() { + return $this->maxSubpages() !== 0; + } + + public function canShowSubpages() { + return ($this->blueprint()->pages()->hide() !== true and $this->canHaveSubpages()); + } + + public function canHaveFiles() { + return $this->maxFiles() !== 0; + } + + public function canShowFiles() { + return ($this->blueprint()->files()->hide() !== true and $this->canHaveFiles()); + } + + public function canHaveMoreSubpages() { + if(!$this->canHaveSubpages()) { + return false; + } else if($this->children()->count() >= $this->maxSubpages()) { + return false; + } else { + return true; + } + } + + public function canHaveMoreFiles() { + if(!$this->canHaveFiles()) { + return false; + } else if($this->files()->count() >= $this->maxFiles()) { + return false; + } else { + return true; + } + } + + + public function structure() { + return new Structure($this, 'site_' . $this->lang()); + } + + public function lang() { + return $this->multilang() ? $this->language()->code() : false; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user.php b/panel/app/src/panel/models/user.php new file mode 100644 index 0000000..b897c15 --- /dev/null +++ b/panel/app/src/panel/models/user.php @@ -0,0 +1,151 @@ +username() . '/' . $action; + } + + public function url($action = 'edit') { + if(empty($action)) $action = 'edit'; + return panel()->urls()->index() . '/' . $this->uri($action); + } + + public function form($action, $callback) { + return panel()->form('users/' . $action, $this, $callback); + } + + public function update($data = array()) { + + if(!panel()->user()->isAdmin() and !$this->isCurrent()) { + throw new Exception(l('users.form.error.update.rights')); + } + + // users which are not an admin cannot change their role + if(!panel()->user()->isAdmin()) { + unset($data['role']); + } + + if(str::length(a::get($data, 'password')) > 0) { + if(a::get($data, 'password') !== a::get($data, 'passwordconfirmation')) { + throw new Exception(l('users.form.error.password.confirm')); + } + } else { + unset($data['password']); + } + + unset($data['passwordconfirmation']); + + if($this->isLastAdmin() and a::get($data, 'role') !== 'admin') { + // check the number of left admins to not convert the last one + throw new Exception(l('user.error.lastadmin')); + } + + parent::update($data); + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.user.update', $this); + + return $this; + + } + + public function isLastAdmin() { + if($this->isAdmin()) { + if(panel()->users()->filterBy('role', 'admin')->count() == 1) { + return true; + } + } else { + return false; + } + } + + public function delete() { + + if(!panel()->user()->isAdmin() and !$this->isCurrent()) { + throw new Exception(l('users.delete.error.permission')); + } + + if($this->isLastAdmin()) { + // check the number of left admins to not delete the last one + throw new Exception(l('users.delete.error.lastadmin')); + } + + parent::delete(); + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.user.delete', $this); + + } + + public function avatar($crop = null) { + if($crop === null) { + return new Avatar($this); + } else { + $avatar = $this->avatar(); + if($avatar->exists()) { + return $avatar->crop($crop); + } else { + return $avatar; + } + } + } + + public function isCurrent() { + return $this->is(panel()->user()); + } + + public function history() { + return new History($this); + } + + public function topbar($topbar) { + + $topbar->append(purl('users'), l('users')); + $topbar->append($this->url(), $this->username()); + + } + + public function getBlueprintFields() { + return $this->blueprint()->fields($this); + } + + public function blueprint() { + return new Blueprint($this); + } + + public function structure() { + return new Structure($this, 'user_' . $this->username()); + } + + static public function current() { + if($user = parent::current()) { + if($user->hasPanelAccess()) { + return $user; + } else { + $user->logout(); + return null; + } + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user/avatar.php b/panel/app/src/panel/models/user/avatar.php new file mode 100644 index 0000000..1a6e26d --- /dev/null +++ b/panel/app/src/panel/models/user/avatar.php @@ -0,0 +1,78 @@ +exists()) { + $this->root = $this->user->avatarRoot('{safeExtension}'); + $this->url = purl('assets/images/avatar.png'); + } + + } + + public function form($action, $callback) { + return panel()->form('avatars/' . $action, $this, $callback); + } + + public function upload() { + + if(!panel()->user()->isAdmin() and !$this->user->isCurrent()) { + throw new Exception(l('users.avatar.error.permission')); + } + + $root = $this->exists() ? $this->root() : $this->user->avatarRoot('{safeExtension}'); + + $upload = new Upload($root, array( + 'accept' => function($upload) { + if($upload->type() != 'image') { + throw new Error(l('users.avatar.error.type')); + } + } + )); + + if(!$upload->file()) { + throw $upload->error(); + } + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.avatar.upload', $this); + + } + + public function delete() { + + if(!panel()->user()->isAdmin() and !$this->user->isCurrent()) { + throw new Exception(l('users.avatar.delete.error.permission')); + } else if(!$this->exists()) { + return true; + } + + if(!parent::delete()) { + throw new Exception(l('users.avatar.delete.error')); + } + + // flush the cache in case if the user data is + // used somewhere on the site (i.e. for profiles) + kirby()->cache()->flush(); + + kirby()->trigger('panel.avatar.delete', $this); + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/models/user/blueprint.php b/panel/app/src/panel/models/user/blueprint.php new file mode 100644 index 0000000..ad09ca8 --- /dev/null +++ b/panel/app/src/panel/models/user/blueprint.php @@ -0,0 +1,59 @@ +user = $user; + + // load from yaml file + $this->load(); + + } + + public function load() { + + // get the user role and load the + // correspondant blueprint if available + $this->name = basename(strtolower($this->user->role())); + + // try to find a user blueprint + $file = kirby()->get('blueprint', 'users/' . $this->name); + + if($file) { + $this->file = $file; + $this->yaml = data::read($this->file, 'yaml'); + + // remove the broken first line + unset($this->yaml[0]); + } + + } + + public function fields() { + $fields = (array)a::get($this->yaml, 'fields', array()); + return new Fields($fields, $this->user); + } + + public function __toString() { + return $this->name; + } + +} diff --git a/panel/app/src/panel/models/user/history.php b/panel/app/src/panel/models/user/history.php new file mode 100644 index 0000000..9d9ea3d --- /dev/null +++ b/panel/app/src/panel/models/user/history.php @@ -0,0 +1,94 @@ +site()->user($user->username())) { + $this->user = $user; + } else { + throw new Exception('The user could not be found'); + } + } + + public function add($id) { + + if(is_a('Kirby\\Panel\\Models\\Page', $id)) { + $page = $id; + } else { + if(empty($id)) return false; + + try { + $page = panel()->page($id); + } catch(Exception $e) { + return false; + } + } + + $history = $this->get(); + + // remove existing entries + foreach($history as $key => $val) { + if($val->id() == $page->id()) unset($history[$key]); + } + + array_unshift($history, $page->id()); + $history = array_slice($history, 0, 5); + + try { + $this->user->update(array( + 'history' => $history + )); + } catch(Exception $e) { + + } + + } + + public function get() { + + $history = $this->user->__get('history'); + + if(empty($history) or !is_array($history)) { + return array(); + } + + $update = false; + $result = array(); + + foreach($history as $item) { + + try { + $result[] = panel()->page($item); + } catch(Exception $e) { + $update = true; + } + + } + + if($update) { + + $history = array_map(function($item) { + return $item->id(); + }, $result); + + try { + $this->user->update(array( + 'history' => $history + )); + } catch(Exception $e) { + + } + + } + + return $result; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/roots.php b/panel/app/src/panel/roots.php new file mode 100644 index 0000000..60d8a7a --- /dev/null +++ b/panel/app/src/panel/roots.php @@ -0,0 +1,34 @@ +panel = $panel; + $this->index = $root; + $this->app = $root . DS . 'app'; + $this->assets = $root . DS . 'assets'; + + $this->config = $this->app . DS . 'config'; + $this->controllers = $this->app . DS . 'controllers'; + $this->collections = $this->app . DS . 'collections'; + $this->models = $this->app . DS . 'models'; + $this->fields = $this->app . DS . 'fields'; + $this->forms = $this->app . DS . 'forms'; + $this->translations = $this->app . DS . 'translations'; + $this->widgets = $this->app . DS . 'widgets'; + $this->layouts = $this->app . DS . 'layouts'; + $this->lib = $this->app . DS . 'lib'; + $this->topbars = $this->app . DS . 'topbars'; + $this->snippets = $this->app . DS . 'snippets'; + $this->views = $this->app . DS . 'views'; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/search.php b/panel/app/src/panel/search.php new file mode 100644 index 0000000..f3f6f95 --- /dev/null +++ b/panel/app/src/panel/search.php @@ -0,0 +1,109 @@ +query = trim($query); + $this->pages = new Collection; + $this->users = new Collection; + + // temporary disable the search cache + $this->cache = cache::setup('mock'); + + // try { + // $root = kirby()->roots()->cache() . DS . 'search'; + // dir::make($root); + // $this->cache = cache::setup('file', array('root' => $root)); + // } catch(Exception $e) { + // $this->cache = cache::setup('session'); + // } + + $this->run(); + + } + + public function data() { + + $pages = $this->cache->get('pages'); + $users = $this->cache->get('users'); + + if(empty($pages)) { + $pages = array(); + foreach(panel()->site()->index() as $page) { + $pages[] = array( + 'title' => (string)$page->title(), + 'uri' => (string)$page->id(), + ); + } + $this->cache->set('pages', $pages); + } + + if(empty($users)) { + foreach(panel()->users() as $user) { + $users[] = array( + 'username' => $user->username(), + 'email' => $user->email() + ); + } + $this->cache->set('users', $users); + } + + return compact('pages', 'users'); + + } + + public function run() { + + if(empty($this->query) or str::length($this->query) <= 1) { + return false; + } + + $data = $this->data(); + + + foreach($data['pages'] as $page) { + if( + str::contains($page['title'], $this->query) or + str::contains($page['uri'], $this->query) + ) { + $this->pages->append($page['uri'], $page); + } + } + + foreach($data['users'] as $user) { + if( + str::contains($user['username'], $this->query) or + str::contains($user['email'], $this->query) + ) { + $this->users->append($user['username'], $user); + } + } + + $this->pages = $this->pages->limit(5); + $this->users = $this->users->limit(5); + + } + + public function pages() { + return $this->pages; + } + + public function users() { + return $this->users; + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/snippet.php b/panel/app/src/panel/snippet.php new file mode 100644 index 0000000..7ece8fe --- /dev/null +++ b/panel/app/src/panel/snippet.php @@ -0,0 +1,14 @@ +_root = panel::instance()->roots()->snippets(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/structure.php b/panel/app/src/panel/structure.php new file mode 100644 index 0000000..b57d3b2 --- /dev/null +++ b/panel/app/src/panel/structure.php @@ -0,0 +1,181 @@ +model = $model; + $this->id = 'structure_' . sha1($id); + $this->blueprint = $this->model->blueprint(); + + } + + public function forField($field) { + + if(method_exists($this->model, $field)) { + throw new Exception('The field name: ' . $field . ' cannot be used as it is reserved'); + } + + + $this->field = $field; + $this->config = $this->model->getBlueprintFields()->get($this->field); + + + if(is_a($this->model, 'Page')) { + $source = $this->model->content()->get($this->field); + $decode = true; + } else if(is_a($this->model, 'File')) { + $source = $this->model->meta()->get($this->field); + $decode = true; + } else if(is_a($this->model, 'User')) { + $source = $this->model->{$this->field}(); + $decode = false; + } else { + throw new Exception('Invalid model for structure field: ' . $this->field); + } + + $this->source = $decode ? (array)yaml::decode($source) : (array)$source; + $this->store = new Store($this, $this->source); + + return $this; + + } + + public function config() { + return $this->config; + } + + public function source() { + return $this->source; + } + + public function store() { + return $this->store; + } + + public function model() { + return $this->model; + } + + public function field() { + return $this->field; + } + + public function id() { + return $this->id; + } + + public function fields() { + + $fields = $this->config->fields(); + $fields = new Fields($fields, $this->model); + $fields = $fields->toArray(); + + // make sure that no unwanted options or fields + // are being included here + foreach($fields as $name => $field) { + + // remove all structure fields within structures + if($field['type'] == 'structure') { + unset($fields[$name]); + // convert title fields to normal text fields + } else if($field['type'] == 'title') { + $fields[$name]['type'] = 'text'; + // remove invalid buttons from textareas + } else if($field['type'] == 'textarea') { + $buttons = a::get($fields[$name], 'buttons'); + if(is_array($buttons)) { + foreach($buttons as $index => $value) { + if(in_array($value, array('link','email'))) { + unset($fields[$name]['buttons'][$index]); + } + } + } else if($buttons !== false) { + $fields[$name]['buttons'] = array('bold', 'italic'); + } + } + + } + + return $fields; + + } + + public function data() { + + $collection = new Collection($this->store()->data()); + $collection = $collection->map(function($item) { + return new Obj($item); + }); + + return $collection; + + } + + public function toObject($array) { + return is_array($array) ? new Obj($array) : false; + } + + public function find($id) { + return $this->toObject($this->store()->find($id)); + } + + public function reset() { + + if($this->field) { + return $this->store()->reset(); + } else { + foreach(s::get() as $key => $value) { + if(str::startsWith($key, $this->id)) { + s::remove($key); + } + } + } + + } + + public function delete($id = null) { + return $this->store()->delete($id); + } + + public function add($data = array()) { + return $this->store()->add($data); + } + + public function update($id, $data = array()) { + return $this->toObject($this->store()->update($id, $data)); + } + + public function sort($ids) { + return $this->store()->sort($ids); + } + + public function toArray() { + return $this->store()->toArray(); + } + + public function toYaml() { + return $this->store()->toYaml(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/structure/store.php b/panel/app/src/panel/structure/store.php new file mode 100644 index 0000000..5cb599e --- /dev/null +++ b/panel/app/src/panel/structure/store.php @@ -0,0 +1,185 @@ +structure = $structure; + $this->source = $source; + $this->id = $structure->id() . '_' . $structure->field(); + $this->age = time(); + + $this->sync(); + $this->init(); + } + + public function init() { + + $data = s::get($this->id()); + + if(!is_array($data)) { + $raw = (array)$this->source; + } else { + $raw = (array)s::get($this->id(), array()); + } + + $data = array(); + + foreach($raw as $row) { + + if(is_string($row)) { + continue; + } + + if(!isset($row['id'])) { + $row['id'] = str::random(32); + } + + $data[$row['id']] = $row; + + } + + $this->data = $data; + s::set($this->id, $this->data); + s::set($this->id . '_age', $this->age); + + } + + /** + * Resets store if necessary to stay in sync with content file + */ + public function sync() { + + $file = $this->structure->model()->textfile(); + $ageModel = f::exists($file) ? f::modified($file) : 0; + $ageStore = s::get($this->id() . '_age'); + + if($ageStore < $ageModel) { + $this->reset(); + $this->age = $ageModel; + } else { + $this->age = $ageStore; + } + + } + + public function id() { + return $this->id; + } + + public function data() { + return $this->data; + } + + public function find($id) { + return a::get($this->data, $id); + } + + public function add($data) { + + $data['id'] = str::random(32); + + $this->data[ $data['id'] ] = $data; + $this->save(); + + return $data['id']; + + } + + public function update($id, $data) { + + if($entry = a::get($this->data, $id)) { + + foreach($data as $key => $value) { + $entry[$key] = $value; + } + + $this->data[$id] = $entry; + $this->save(); + + return $entry; + + } else { + return false; + } + + } + + public function delete($id) { + + if(is_null($id)) { + $this->data = array(); + } else { + unset($this->data[$id]); + } + + $this->save(); + + return $this->data; + + } + + public function sort($ids) { + + $data = array(); + + foreach($ids as $id) { + if($item = $this->find($id)) { + $data[$id] = $item; + } + } + + $this->data = $data; + $this->save(); + + return $this->data; + + } + + public function toArray() { + $array = array_values($this->data); + $array = array_map(function($item) { + unset($item['id']); + return $item; + }, $array); + return $array; + } + + public function toYaml() { + return trim(yaml::encode($this->toArray())); + } + + public function save() { + + s::set($this->id, $this->data); + + // keep the changes for the page + if(is_a($this->structure->model(), 'page')) { + $this->structure->model()->changes()->update( + $this->structure->field(), + $this->toYaml() + ); + } + + } + + public function reset() { + return s::remove($this->id); + } + + +} \ No newline at end of file diff --git a/panel/app/src/panel/topbar.php b/panel/app/src/panel/topbar.php new file mode 100644 index 0000000..4f2169f --- /dev/null +++ b/panel/app/src/panel/topbar.php @@ -0,0 +1,117 @@ +view = $view; + + if(is_object($input) and method_exists($input, 'topbar')) { + $input->topbar($this); + } else { + + $class = is_object($input) ? str_replace('model', '', strtolower(get_class($input))) : (string)$input; + $file = panel()->roots()->topbars() . DS . str::lower($class) . '.php'; + + if(file_exists($file)) { + + $callback = require($file); + $callback($this, $input); + + } else { + throw new Exception(l('topbar.error.class.definition') . $class); + } + + } + + } + + public function append($url, $title) { + + $this->breadcrumb[] = array( + 'title' => $title, + 'url' => $url + ); + + } + + public function menu() { + return new Snippet('menu'); + } + + public function breadcrumb() { + return new Snippet('breadcrumb', array( + 'items' => $this->breadcrumb + )); + } + + public function message() { + + if($message = s::get('message') and is_array($message)) { + + $text = a::get($message, 'text'); + $type = a::get($message, 'type', 'notification'); + + $element = new Brick('div'); + $element->addClass('message'); + + if($type == 'error') { + $element->addClass('message-is-alert'); + } else { + $element->addClass('message-is-notice'); + } + + $element->append(function() use($text) { + $content = new Brick('span'); + $content->addClass('message-content'); + $content->text($text); + return $content; + }); + + $element->append(function() { + $toggle = new Brick('a'); + $toggle->attr('href', url::current()); + $toggle->addClass('message-toggle'); + $toggle->html('×'); + return $toggle; + }); + + s::remove('message'); + + return $element; + + } + + } + + public function render() { + + $element = new Brick('header', '', array('class' => 'topbar')); + $element->append($this->menu()); + $element->append($this->breadcrumb()); + $element->append($this->html); + $element->append(new Snippet('search')); + $element->append($this->message()); + + return $element; + + } + + public function __toString() { + return (string)$this->render(); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/translation.php b/panel/app/src/panel/translation.php new file mode 100644 index 0000000..04f0477 --- /dev/null +++ b/panel/app/src/panel/translation.php @@ -0,0 +1,80 @@ + 'es_419', + 'no_nb' => 'nb', + 'cz' => 'cs' + ); + + public function __construct($panel, $code) { + + $this->panel = $panel; + $this->code = basename($code); + + // convert old codes + if(isset($this->map[$this->code])) { + $this->code = $this->map[$this->code]; + } + + // set the root for the translation directory + $this->root = $this->panel->roots()->translations() . DS . $this->code; + + if(!is_dir($this->root)) { + throw new Exception('The translation does not exist: ' . $this->code); + } + + if(!is_file($this->root . DS . 'package.json')) { + throw new Exception('The package.json is missing for the translation with code: ' . $this->code); + } + + if(!is_file($this->root . DS . 'core.json')) { + throw new Exception('The core.json is missing for the translation with code: ' . $this->code); + } + + } + + public function code() { + return $this->code; + } + + public function root() { + return $this->root; + } + + public function load() { + return l::$data = data::read($this->root . DS . 'core.json'); + } + + public function info() { + if(!is_null($this->info)) return $this->info; + return $this->info = new Obj(data::read($this->root . DS . 'package.json')); + } + + public function direction() { + $direction = $this->info()->direction(); + return $direction ? $direction : 'ltr'; + } + + public function __call($method, $args) { + return $this->info()->{$method}(); + } + + public function __toString() { + return $this->code; + } + +} diff --git a/panel/app/src/panel/upload.php b/panel/app/src/panel/upload.php new file mode 100644 index 0000000..1ec93bb --- /dev/null +++ b/panel/app/src/panel/upload.php @@ -0,0 +1,22 @@ + 'The file is missing', + static::ERROR_MISSING_TMP_DIR => 'The /tmp directory is missing on your server', + static::ERROR_FAILED_UPLOAD => 'The upload failed', + static::ERROR_PARTIAL_UPLOAD => 'The file has been only been partially uploaded', + static::ERROR_UNALLOWED_OVERWRITE => 'The file exists and cannot be overwritten', + static::ERROR_MAX_SIZE => 'The file is too big. The maximum size is ' . f::niceSize($this->maxSize()), + static::ERROR_MOVE_FAILED => 'The file could not be moved', + static::ERROR_UNACCEPTED => 'The file is not accepted by the server' + ); + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/urls.php b/panel/app/src/panel/urls.php new file mode 100644 index 0000000..5341164 --- /dev/null +++ b/panel/app/src/panel/urls.php @@ -0,0 +1,35 @@ +panel = $panel; + + // base url + $this->index = rtrim($this->panel->kirby()->urls()->index(), '/') . '/' . basename($root); + + // assets + $this->assets = $this->index . '/assets'; + $this->css = $this->assets . '/css'; + $this->js = $this->assets . '/js'; + $this->images = $this->assets . '/images'; + + // enable urls without rewriting + if(kirby()->option('rewrite') === false) { + $this->index .= '/index.php'; + } + + // shortcuts + $this->api = $this->index . '/api'; + $this->login = $this->index . '/login'; + $this->logout = $this->index . '/logout'; + $this->error = $this->index . '/error'; + + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/view.php b/panel/app/src/panel/view.php new file mode 100644 index 0000000..d30f5bf --- /dev/null +++ b/panel/app/src/panel/view.php @@ -0,0 +1,40 @@ +_root = panel::instance()->roots()->views(); + $this->_file = $file; + $this->_data = $data; + } + + public function __set($key, $value) { + $this->_data[$key] = $value; + } + + public function render() { + $file = $this->_root . DS . str_replace('.', DS, $this->_file) . '.php'; + if(!file_exists($file)) throw new Exception(l('view.error.invalid') . $file); + return tpl::load($file, $this->_data); + } + + public function __toString() { + try { + return (string)$this->render(); + } catch(Exception $e) { + return $e->getMessage(); + } + } + +} \ No newline at end of file diff --git a/panel/app/src/panel/widgets.php b/panel/app/src/panel/widgets.php new file mode 100644 index 0000000..c73d825 --- /dev/null +++ b/panel/app/src/panel/widgets.php @@ -0,0 +1,92 @@ +order = kirby()->option('panel.widgets'); + + $this->defaults(); + $this->custom(); + $this->sort(); + + } + + public function load($name) { + + if(!isset($this->available[$name])) { + return false; + } + + $dir = $this->available[$name]; + $file = $dir . DS . $name . '.php'; + + if(!file_exists($file)) { + return false; + } + + $widget = require($file); + + if(is_array($widget)) { + $this->append($name, $widget); + return $widget; + } else { + return false; + } + + } + + public function defaults() { + + $kirby = kirby(); + $root = panel()->roots()->widgets(); + + foreach(dir::read($root) as $dir) { + $kirby->registry->set('widget', $dir, $root . DS . $dir); + } + + } + + public function custom() { + + $kirby = kirby(); + $root = $kirby->roots()->widgets(); + + foreach(dir::read($root) as $dir) { + $kirby->registry->set('widget', $dir, $root . DS . $dir); + } + + } + + public function sort() { + + // load all widgets from the registry + $this->available = kirby()->registry()->get('widget'); + + // the license warning must always be included + $this->order['license'] = true; + + // append ordered widgets + foreach($this->order as $name => $add) { + if($add) { + $this->load($name); + } + unset($this->available[$name]); + } + + // append the unsorted widgets + foreach($this->available as $name => $dir) { + $this->load($name); + } + + } + +} \ No newline at end of file diff --git a/panel/app/topbars/error.php b/panel/app/topbars/error.php new file mode 100644 index 0000000..123b6eb --- /dev/null +++ b/panel/app/topbars/error.php @@ -0,0 +1,5 @@ +append('', l('error.headline')); +}; \ No newline at end of file diff --git a/panel/app/topbars/user.php b/panel/app/topbars/user.php new file mode 100644 index 0000000..b6d2a48 --- /dev/null +++ b/panel/app/topbars/user.php @@ -0,0 +1,13 @@ +append(purl('users'), l('users')); + + if($user === 'user') { + $topbar->append(purl('users/add'), l('users.index.add')); + } else { + $topbar->append($user->url(), $user->username()); + } + +}; \ No newline at end of file diff --git a/panel/app/translations/ar/core.json b/panel/app/translations/ar/core.json new file mode 100644 index 0000000..1f09f80 --- /dev/null +++ b/panel/app/translations/ar/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "إلغاء", + "add": "إضافة", + "addit": "إضافة وتعديل", + "save": "حفظ", + "saved": "تم الحفظ!", + "change": "تغيير", + "delete": "حذف", + "insert": "إدراج", + "ok": "موافق", + "routes.error.invalid": "رابط غير صالح للوحة التحكم", + "controller.error.invalid": "مراقب غير صالح", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "عرض الخيارات", + "options.hide": "إخفاء الخيارات", + "installation": "التنصيب", + "installation.check.headline": "تنصيب لوحة كيربي", + "installation.check.text": "المشاكل التالية واجهت كيربي أثناء التنصيب…", + "installation.check.retry": "إعادة المحاولة", + "installation.check.error": "هناك بعض المشاكل!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "المجلد /site/accounts غير قابل للكتابة", + "installation.check.error.avatars": "المجلد /assets/avatars غير قابل للكتابة", + "installation.check.error.blueprints": "فضلا قم بإضافة المجلد /site/blueprints", + "installation.check.error.content": "مجلد المحتوى وجميع الملفات التي بداخله يجب أن تكون قابلة للكتابة.", + "installation.check.error.thumbs": "مجلد الصور المصغرة يجب أن يكون قابل للكتابة.", + "installation.signup.username.label": "أنشئ حسابك الأول", + "installation.signup.username.placeholder": "اسم المستخدم", + "installation.signup.email.label": "البريد الإلكتروني", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "كلمة المرور", + "installation.signup.language.label": "اللغة", + "installation.signup.button": "إنشاء الحساب", + "login": "تسجيل الدخول", + "login.welcome": "فضلاً سجل الدخول باستخدام حسابك", + "login.username.label": "اسم المستخدم", + "login.password.label": "كلمة المرور", + "login.error": "خطأ في اسم المستخدم أو كلمة المرور", + "login.button": "تسجيل الدخول", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "لوحة التحكم", + "dashboard.index.pages.title": "الصفحات", + "dashboard.index.pages.edit": "تعديل", + "dashboard.index.pages.add": "إضافة", + "dashboard.index.site.title": "وصلة موقعك", + "dashboard.index.account.title": "حسابك", + "dashboard.index.account.edit": "تعديل", + "dashboard.index.metatags.title": "متغيرات الموقع", + "dashboard.index.metatags.edit": "تعديل", + "dashboard.index.history.title": "آخر تحديثاتك", + "dashboard.index.history.text": "سيتم عرض آخر صفحاتك المعدلة هنا لتسهيل وصولك إليها مجدداً.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "متغيرات الموقع", + "metatags.info": "معلومات عن Kirby ", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "العودة إلى لوحة التحكم", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "إعدادات الصفحة", + "pages.show.preview": "عرض المعاينة", + "pages.show.template": "القالب", + "pages.show.changeurl": "تغيير الوصلة", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "حذف هذه الصفحة", + "pages.show.subpages.title": "الصفحات", + "pages.show.subpages.edit": "تعديل", + "pages.show.subpages.add": "إضافة", + "pages.show.subpages.empty": "هذه الصفحة لا تحتوي على صفحات فرعية", + "pages.show.files.title": "الملفات", + "pages.show.files.edit": "تعديل", + "pages.show.files.add": "إضافة", + "pages.show.files.empty": "هذه الصفحة لا تحتوي على ملفات", + "pages.show.error.permissions.title": "هذه الصفحة غير قابلة للكتابة", + "pages.show.error.permissions.text": "فضلاً قم بفحص جميع التصاريح على مجلد المحتوى وجميع الملفات التي بداخله.", + "pages.show.error.permissions.retry": "إعادة المحاولة", + "pages.show.error.notitle.title": "هذا المخطط لا يحتوي على حقل العنوان،", + "pages.show.error.notitle.text": "فضلاً قم بإضافة حقل العنوان والمحاولة مرة أخرى", + "pages.show.error.notitle.retry": "إعادة المحاولة", + "pages.show.error.form": "فضلاً قم بإكمال جميع الحقول بشكل صحيح", + "pages.add.title.label": "إضافة صفحة جديدة", + "pages.add.title.placeholder": "العنوان", + "pages.add.url.label": "الوصلة", + "pages.add.url.enter": "(اكتب عنوانك)", + "pages.add.url.close": "إغلاق", + "pages.add.url.help": "الصيغة: حروف صغيرة a-z، أرقام 0-9 وعلامة الشرطة -.", + "pages.add.template.label": "القالب", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "العنوان مفقود", + "pages.add.error.template": "القالب مفقود", + "pages.add.error.max.headline": "لا يُسمح بإضافة صفحات جديدة", + "pages.add.error.max.text": "تم استهلاك الحد الأقصى من الصفحات الفرعية لهذه الصفحة", + "pages.url.uid.label": "الوصلة", + "pages.url.uid.label.option": "إنشاء من العنوان", + "pages.url.error.exists": "توجد صفحة أخرى لها نفس الوصلة", + "pages.url.error.move": "لم يكن بالإمكان تغيير الوصلة", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "القالب", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "هل أنت متأكد من رغبتك في حذف هذه الصفحة؟", + "pages.delete.error.home.headline": "لا يمكن حذف الصفحة الرئيسة", + "pages.delete.error.home.text": "قمت بمحاولة حذف الصفحة الرئيسة. لا يمكن إتمام ذلك بسبب ما سيعقبه من آثار غير مرغوبة.", + "pages.delete.error.error.headline": "لا يمكن حذف صفحة الأخطاء", + "pages.delete.error.error.text": "قمت بمحاولة حذف صفحة الأخطاء. لا يمكن إتمام ذلك بسبب ما سيعقبه من آثار غير مرغوبة.", + "pages.delete.error.children.headline": "لا يمكن حذف هذه الصفحة", + "pages.delete.error.children.text": "لا يمكن حذف هذه الصفحة لإحتوائها على صفحات فرعية. لإتمام ذلك، عليك أن تحذف صفحاتها الفرعية أولاً..", + "pages.delete.error.blocked.headline": "لا يمكن حذف هذه الصفحة", + "pages.delete.error.blocked.text": "هذه الصفحة مقفلة ولا يمكن حذفها.", + "pages.search.help": "ابحث عن الصفحات باستخدام الوصلة. تنقل بين نتائج البحث باستخدام مفاتيح الأسهم لأعلى وأسفل، واضغط مفتاح الإدخال للإنتقال للصفحة المحددة.", + "pages.search.noresults": "لا توجد نتائج لما بحثت عنه. فضلاً حاول البحث مرة أخرى بوصلة مختلفة.", + "pages.error.missing": "لم يتم العثور على الصفحة.", + "subpages": "الصفحات", + "subpages.index.headline": "الصفحات في", + "subpages.index.back": "عودة", + "subpages.index.add": "إضافة صفحة جديدة", + "subpages.index.add.first.text": "هذه الصفحة لا تملك صفحات فرعية حالياً", + "subpages.index.add.first.button": "أضف الصفحة الأولى", + "subpages.index.visible": "الصفحات المرئية", + "subpages.index.visible.help": "اسحب الصفحات المخفية إلى هنا لترتيبها أو جعلها مرئية.", + "subpages.index.invisible": "الصفحات المخفية", + "subpages.index.invisible.help": "اسحب الصفحات المرئية إلى هنا لبعثرتها أو جعلها مخفية.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "لم يتم العثور على الصفحة", + "files": "Files", + "files.index.headline": "الملفات لـ", + "files.index.back": "عودة", + "files.index.upload": "رفع ملف جديد", + "files.index.upload.first.text": "هذه الصفحة لا تملك ملفات حالياً", + "files.index.upload.first.button": "ارفع الملف الأول", + "files.index.edit": "تعديل", + "files.index.delete": "حذف", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "اسم الملف", + "files.show.info.label": "النوع / الحجم / الأبعاد", + "files.show.link.label": "الوصلة العلنية", + "files.show.open": "عرض/تنزيل الملف", + "files.show.back": "عودة", + "files.show.replace": "استبدال", + "files.show.delete": "حذف", + "files.show.error.rename": "لم يكن بالإمكان تغيير اسم الملف", + "files.show.error.form": "فضلاً قم بتعبئة جميع الحقول بشكل صحيح", + "files.upload.drop": "اسقط الملفات هنا…", + "files.upload.click": "…او اضغط للرفع", + "files.replace.drop": "اسقط ملفاً هنا…", + "files.replace.click": "…او اضغط للاستبدال", + "files.replace.error.type": "الملف المرفوع يجب أن يكون من نفس النوع", + "files.delete.headline": "هل أنت متأكد من رغبتك في حذف هذا الملف؟", + "files.error.missing.page": "لم يتم العثور على الصفحة", + "files.error.missing.file": "لم يتم العثور على الملف", + "users": "المستخدمون", + "users.index.headline": "جميع المستخدمين", + "users.index.add": "إضافة مستخدم جديد", + "users.index.edit": "تعديل", + "users.index.delete": "حذف", + "users.form.username.label": "اسم المستخدم", + "users.form.username.placeholder": "اسم المستخدم الخاص بك", + "users.form.username.help": "الصيغة: حروف صغيرة a-z، أرقام 0-9 وعلامة الشرطة -.", + "users.form.username.readonly": "لا يمكن تغيير اسم المستخدم", + "users.form.firstname.label": "الاسم", + "users.form.lastname.label": "العائلة", + "users.form.email.label": "البريد الإلكتروني", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "كلمة المرور", + "users.form.password.confirm.label": "تأكيد كلمة المرور", + "users.form.password.new.label": "كلمة المرور الجديدة", + "users.form.password.new.confirm.label": "تأكيد كلمة المرور الجديدة", + "users.form.password.new.help": "اترك الحقل فارغاً لإبقاء كلمة المرور الحالية", + "users.form.language.label": "اللغة", + "users.form.role.label": "الوظيفة", + "users.form.options.headline": "خيارات الحساب", + "users.form.options.message": "إرسال بريد إلكتروني", + "users.form.options.delete": "حذف الحساب", + "users.form.avatar.headline": "الصورة الشخصية", + "users.form.avatar.upload": "رفع الصورة الشخصية", + "users.form.avatar.replace": "استبدال الصورة الشخصية", + "users.form.avatar.delete": "حذف الصورة الشخصية", + "users.form.back": "العودة إلى المستخدمين", + "users.form.error.password.confirm": "فضلاً قم بتأكيد كلمة المرور", + "users.form.error.update": "لم يكن بالإمكان تحديث المستخدم", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "لم يكن بالإمكان إنشاء المستخدم", + "users.form.error.permissions.title": "مجلد الحسابات غير قابل للكتابة", + "users.form.error.permissions.text": "تأكد أن المجلد /site/accounts موجود وقابل للكتابة.", + "users.delete.headline": "هل أنت متأكد من رغبتك في حذف هذا المستخدم؟", + "users.delete.error": "لم يكن بالإمكان حذف المستخدم", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "اسقط الصورة الشخصية هنا…", + "users.avatar.click": "…او اضغط للرفع", + "users.avatar.error.type": "بإمكانك رفع الصورة فقط بالصيغ JPG، أو PNG، أو GIF.", + "users.avatar.error.folder.headline": "مجلد الصور الشخصية غير قابل للكتابة", + "users.avatar.error.folder.text": "تأكد أن من إنشاء المجلد /assets/avatars وكونه قابل للكتابة لترفع الصور الشخصية.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "لم يكن بالإمكان حذف الصورة الشخصية", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "تم حذف الصورة الشخصية", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "لم يتم العثور على المستخدم", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "إلزامي", + "fields.date.label": "التاريخ", + "fields.date.months": [ + "يناير", + "فبراير", + "مارس", + "أبريل", + "مايو", + "يونيو", + "يوليو", + "أغسطس", + "سبتمبر", + "أكتوبر", + "نوڤمبر", + "ديسمبر" + ], + "fields.date.weekdays": [ + "الأحد", + "الإثنين", + "الثلاثاء", + "الأربعاء", + "الخميس", + "الجمعة", + "السبت" + ], + "fields.date.weekdays.short": [ + "أحد", + "أثنين", + "ثلاثاء", + "أربعاء", + "خميس", + "جمعة", + "سبت" + ], + "fields.email.label": "بريد إلكتروني", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "رقم", + "fields.number.placeholder": "#", + "fields.page.label": "صفحة", + "fields.page.placeholder": "محل/الصفحة/المرادة", + "fields.password.label": "كلمة مرور", + "fields.structure.add": "إضافة", + "fields.structure.add.first": "إضافة السجل الأول", + "fields.structure.empty": "لا توجد سجلات حالياً.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "إلغاء", + "fields.structure.save": "حفظ", + "fields.structure.edit": "تعديل", + "fields.structure.delete": "حذف", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "وسوم", + "fields.tel.label": "هاتف", + "fields.textarea.buttons.bold.label": "نص عريض", + "fields.textarea.buttons.bold.text": "نص عريض", + "fields.textarea.buttons.italic.label": "نص مائل", + "fields.textarea.buttons.italic.text": "نص مائل", + "fields.textarea.buttons.link.label": "وصلة", + "fields.textarea.buttons.email.label": "بريد إلكتروني", + "fields.textarea.buttons.image.label": "صورة", + "fields.textarea.buttons.file.label": "ملف", + "fields.toggle.yes": "نعم", + "fields.toggle.no": "لا", + "fields.toggle.on": "تشغيل", + "fields.toggle.off": "تعطيل", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "إضافة وصلة", + "editor.link.text.label": "نص الوصلة", + "editor.link.text.help": "نص الوصلة اختياري", + "editor.email.address.label": "أدخل بريد إلكتروني", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "نص الوصلة", + "editor.email.text.help": "نص الوصلة اختياري", + "editor.file.empty": "هذه الصفحة لا تملك ملفات", + "editor.image.empty": "هذه الصفحة لا تملك صوراً", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "خطأ", + "error.headline": "خطأ" +} \ No newline at end of file diff --git a/panel/app/translations/ar/package.json b/panel/app/translations/ar/package.json new file mode 100644 index 0000000..3765a0a --- /dev/null +++ b/panel/app/translations/ar/package.json @@ -0,0 +1,6 @@ +{ + "title": "العربية", + "direction": "rtl", + "author": "أحمد الحداد ", + "version": "1.0.0" +} \ No newline at end of file diff --git a/panel/app/translations/bg/core.json b/panel/app/translations/bg/core.json new file mode 100644 index 0000000..606f188 --- /dev/null +++ b/panel/app/translations/bg/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Откажи", + "add": "Добави", + "addit": "Добави & Редактирай", + "save": "Запиши", + "saved": "Записано", + "change": "Промени", + "delete": "Изтрий", + "insert": "Вмъкни", + "ok": "Ок", + "routes.error.invalid": "Невалиден URL на Панел", + "controller.error.invalid": "Невалиден контролер", + "controller.error.action": "Невалидно действие", + "view.error.invalid": "Невалиден изглед:", + "options.show": "Покажи Опции", + "options.hide": "Скрий Опции", + "installation": "Инсталация", + "installation.check.headline": "Инсталиране на Kirby Панел", + "installation.check.text": "Kirby откри следните проблеми по време на инсталацията...", + "installation.check.retry": "Опитай пак", + "installation.check.error": "Има няколко проблема!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "папката /site/accounts не е записваема", + "installation.check.error.avatars": "Папката assets/avatars не е записваема", + "installation.check.error.blueprints": "моля добавете папка /site/blueprints", + "installation.check.error.content": "Папката \"content\" и всички файлове в нея трябва да бъдат записваеми", + "installation.check.error.thumbs": "Папката \"thumbs\" трябва да бъде записваема.", + "installation.signup.username.label": "Създайте Вашия първи акаунт", + "installation.signup.username.placeholder": "Потребителско име", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Парола", + "installation.signup.language.label": "Език", + "installation.signup.button": "Създайте своя акаунт", + "login": "Подписване", + "login.welcome": "Please log in with your new account", + "login.username.label": "Потребителско име", + "login.password.label": "Парола", + "login.error": "Невалидно потребитебско име или парола", + "login.button": "Log in", + "login.log.error.permissions": "Лог файла за влизанията не е записваем", + "logout": "Log out", + "topbar.error.class.definition": "липсва topbar definition за клас:", + "dashboard": "Табло", + "dashboard.index.pages.title": "Страници", + "dashboard.index.pages.edit": "Редактирай", + "dashboard.index.pages.add": "Добави", + "dashboard.index.site.title": "URL на Вашия сайт", + "dashboard.index.account.title": "Вашия акаунт", + "dashboard.index.account.edit": "Редактирай", + "dashboard.index.metatags.title": "Опции за сайта", + "dashboard.index.metatags.edit": "Редактирай", + "dashboard.index.history.title": "Вашите последни промени", + "dashboard.index.history.text": "Вашите последно променяни страници ще се покажат тук, за да е по-лесно да ги намерите отново по-късно.", + "dashboard.index.license.title": "Лиценз за Kirby", + "dashboard.index.license.text": "Изглежда използвате Kirby на публичен сървър без да притежавате валиден лиценз\n\nМоля, подкрепете Kirby и (link: {buy} text: купете своя лиценз сега)\n\nАко вече имате лицензен ключ, просто го добавете във Вашия config файл (link: {docs} text: site/config/config.php)", + "metatags": "Опции за сайта", + "metatags.info": "Информация за Kirby", + "metatags.license": "Лиценз за Kirby", + "metatags.version.toolkit": "Toolkit версия", + "metatags.version.kirby": "Версия на Kirby", + "metatags.version.panel": "Версия на Панел", + "metatags.back": "Назад към Таблото", + "metatags.files": "Файлове на сайта", + "site.delete.error": "Сайтът не може да се изтрие", + "pages.show.settings": "Настройки за страницата", + "pages.show.preview": "Отвори предварителен преглед", + "pages.show.template": "Модел", + "pages.show.changeurl": "Промени URL", + "pages.show.invisible": "Статус: Невидимо", + "pages.show.visible": "Статус: Видимо", + "pages.show.changes.text": "Имате незаписани промени", + "pages.show.changes.button": "Отмени", + "pages.show.delete": "Изтрий тази страница", + "pages.show.subpages.title": "Страници", + "pages.show.subpages.edit": "Редактирай", + "pages.show.subpages.add": "Добави", + "pages.show.subpages.empty": "Тази страница няма под-страници", + "pages.show.files.title": "Файлове", + "pages.show.files.edit": "Редактирай", + "pages.show.files.add": "Добави", + "pages.show.files.empty": "Тази страница не съдържа файлове", + "pages.show.error.permissions.title": "Страницата не е записваема", + "pages.show.error.permissions.text": "Моля, проверете правата за content папката и всички файлове в нея", + "pages.show.error.permissions.retry": "Опитай пак", + "pages.show.error.notitle.title": "blueprint няма поле title", + "pages.show.error.notitle.text": "Моля добавете заглавно поле и опитайте отново", + "pages.show.error.notitle.retry": "Опитай пак", + "pages.show.error.form": "Моля, попълнете всички полета правилно", + "pages.add.title.label": "Добавете нова страница", + "pages.add.title.placeholder": "Заглавие", + "pages.add.url.label": "URL-добавка", + "pages.add.url.enter": "(въведете вашето заглавие)", + "pages.add.url.close": "Затвори", + "pages.add.url.help": "Формат: малки букви a-z, 0-9 и нормални тирета", + "pages.add.template.label": "Модел", + "pages.add.error.create": "Страницата не може да бъде създадена", + "pages.add.error.title": "Заглавието липсва", + "pages.add.error.template": "Моделът липсва", + "pages.add.error.max.headline": "Не са позволени нови страници", + "pages.add.error.max.text": "Максималният брой под-страници за тази страница е достигнат.", + "pages.url.uid.label": "URL-добавка", + "pages.url.uid.label.option": "Създайте от заглавието", + "pages.url.error.exists": "Страница със същата URL-добавка вече съществува", + "pages.url.error.move": "Добавката не може да се променя", + "pages.url.error.rights": "Не можете да смените URL за тази страница", + "pages.template.select.label": "Модел", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Позиция", + "pages.toggle.invisible": "Невидим", + "pages.toggle.publish": "Наистина ли искате да смените статуса на тази страница на **Видим?**", + "pages.toggle.hide": "Наистина ли искате да смените статуса на тази страница на **Невидим?**", + "pages.toggle.error.error": "Статуса на ERROR страницата не може да се променя", + "pages.delete.headline": "Наистина ли искате да изтриете тази страница?", + "pages.delete.error.home.headline": "HOME страницата не може да се изтрива", + "pages.delete.error.home.text": "Вие се опитвате да изтриете HOME страницата. Това не е възможно, защото би довело до нежелани резултати.", + "pages.delete.error.error.headline": "ERROR страницата не може да се изтрива", + "pages.delete.error.error.text": "Вие се опитвате да изтриете ERROR страницата. Това не е възможно, защото би довело до нежелани резултати.", + "pages.delete.error.children.headline": "Страницата не може да се изтрива", + "pages.delete.error.children.text": "Тази страница има под-страници и не може да се изтрие. Моля, изтрийте всички под-страници.", + "pages.delete.error.blocked.headline": "Страницата не може да се изтрие", + "pages.delete.error.blocked.text": "Тази страница е заключена и не може да се изтрие.", + "pages.search.help": "Търсете страници по URL. Използвайте клавишите Нагоре и Надолу за да обходите резултатите от търсенето. Използвайте Enter за да отидете в избраната страница.", + "pages.search.noresults": "Няма резултати от вашето търсене. Моля опитайте отново с различен URL.", + "pages.error.missing": "Страницата не може да бъде намерена", + "subpages": "Страници", + "subpages.index.headline": "Страници в", + "subpages.index.back": "Назад", + "subpages.index.add": "Добавете нова страница", + "subpages.index.add.first.text": "Тази страница все още няма под-страници", + "subpages.index.add.first.button": "Добавете първата страница", + "subpages.index.visible": "Видими страници", + "subpages.index.visible.help": "Влачете невидимите страници тук за да ги сортирате / направите видими.", + "subpages.index.invisible": "Невидими страници", + "subpages.index.invisible.help": "Влачете видимите страници тук за да не ги сортирате / да ги направите невидими.", + "subpages.add.error": "Тази страница не може да има под-страници", + "subpages.add.error.more": "Тази страница не може да има повече под-страници", + "subpages.error.missing": "Страницата не може да бъде намерена", + "files": "Файлове", + "files.index.headline": "Файлове за", + "files.index.back": "Назад", + "files.index.upload": "Качете нов файл", + "files.index.upload.first.text": "Тази страница все още няма файлове", + "files.index.upload.first.button": "Качете първия файл", + "files.index.edit": "Редактирай", + "files.index.delete": "Изтрий", + "files.index.error.disabled": "Тази страница не може да има файлове", + "files.add.error.max": "Максималният брой файлове за тази страница бе достигнат.", + "files.add.error.extension.missing": "Не можете да качвате файлове без разширение", + "files.add.error.extension.forbidden": "Забранено файлово разширение", + "files.add.error.mime.forbidden": "Забранен mime type", + "files.add.error.htaccess": "htaccess файл не може да се качи", + "files.add.error.invisible": "Невидимите файлове не могат да бъдат качени", + "files.add.blueprint.type.error": "Страницата позволява:", + "files.add.blueprint.size.error": "Страницата позволява големина на файла ", + "files.show.name.label": "Име на файла", + "files.show.info.label": "Тип / Големина / Размери", + "files.show.link.label": "Публична връзка", + "files.show.open": "Покажи / Свали файла", + "files.show.back": "Назад", + "files.show.replace": "Замести", + "files.show.delete": "Изтрий", + "files.show.error.rename": "Файлът не може да се преименува", + "files.show.error.form": "Моля, попълнете всички полета правилно", + "files.upload.drop": "Пуснете файловете тук...", + "files.upload.click": "... или кликнете за да качите", + "files.replace.drop": "Пуснете файл тук...", + "files.replace.click": "...или кликнете за да замените", + "files.replace.error.type": "Каченият файл трябва да има същия файлов тип", + "files.delete.headline": "Наистина ли искате да изтриете този файл?", + "files.error.missing.page": "Страницата не може да бъде намерена", + "files.error.missing.file": "Файлът не може да бъде намерен", + "users": "Потребители", + "users.index.headline": "Всички потребители", + "users.index.add": "Добавете нов потребител", + "users.index.edit": "Редактирай", + "users.index.delete": "Изтрий", + "users.form.username.label": "Потребителско име", + "users.form.username.placeholder": "Вашето потребителско име", + "users.form.username.help": "Позволени символи: малки букви a-zq 0-9 и тирета", + "users.form.username.readonly": "Потребителското име не може да се променя", + "users.form.firstname.label": "Първо име", + "users.form.lastname.label": "Фамилия", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Парола", + "users.form.password.confirm.label": "Потвърдете паролата", + "users.form.password.new.label": "Нова парола", + "users.form.password.new.confirm.label": "Потвърдете новата парола", + "users.form.password.new.help": "Оставете празна за да запазите текущата парола", + "users.form.language.label": "Език", + "users.form.role.label": "Роля", + "users.form.options.headline": "Опции на акаунта", + "users.form.options.message": "Изпратете email", + "users.form.options.delete": "Изтрийте акаунт", + "users.form.avatar.headline": "Профилна картинка", + "users.form.avatar.upload": "Качете профилна картинка", + "users.form.avatar.replace": "Заменете профилната картинка", + "users.form.avatar.delete": "Изтрийте профилната картинка", + "users.form.back": "Назад към потребители", + "users.form.error.password.confirm": "Моля, потвърдете паролата", + "users.form.error.update": "Потребителят не може да бъде актуализиран", + "users.form.error.update.rights": "Не ви е позволено да се актуализира този потребител", + "users.form.error.create": "Потребителят не може да бъде създаден", + "users.form.error.permissions.title": "Папката accounts не е записваема", + "users.form.error.permissions.text": "Моля, уверете се, че /site/accounts съществува и е записваема", + "users.delete.headline": "Наистина ли искате да изтриете този потребител?", + "users.delete.error": "Потребителят не може да бъде изтрит", + "users.delete.error.permission": "Не ви е позволено да изтривате потребители", + "users.delete.error.permission.single": "Не е позволено да изтривате този потребител", + "users.delete.error.lastadmin": "Не можете да изтриете последния администратор", + "users.avatar.drop": "Пуснете профилната снимка тук...", + "users.avatar.click": "... или кликнете за да качите", + "users.avatar.error.type": "Можете да качвате само JPG, PNG и GIF файлове.", + "users.avatar.error.folder.headline": "Папката за аватари трябва да е записваема", + "users.avatar.error.folder.text": "Моля създайте папка assets/avatar и я направете записваема за да качвате профилни снимки.", + "users.avatar.error.permission": "Не можете да сменяте профилната снимка", + "users.avatar.delete.error": "Профилната снимка не може да се изтрие", + "users.avatar.delete.error.permission": "Не можете да изтриете профилната снимка на този потребител.", + "users.avatar.delete.success": "Профилната снимка беше изтрита", + "users.avatar.missing": "Този потребител няма профилна снимка", + "users.error.missing": "Потребителят не може да бъде намерен.", + "user.error.lastadmin": "Вие сте само администратор. Това не може да се промени.", + "form.error.missing": "Формата не може да се намери", + "form.construct.error.invalid": "Невалиден form construction method", + "fields.required": "Задължително", + "fields.date.label": "Дата", + "fields.date.months": [ + "Януари", + "Февруари", + "Март", + "Април", + "Май", + "Юни", + "Юли", + "Август", + "Септември", + "Октомври", + "Ноември", + "Декември" + ], + "fields.date.weekdays": [ + "Неделя", + "Понеделник", + "Вторник", + "Сряда", + "Четвъртък", + "Петък", + "Събота" + ], + "fields.date.weekdays.short": [ + "Нд", + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Число", + "fields.number.placeholder": "№", + "fields.page.label": "Страница", + "fields.page.placeholder": "път/към/страницата", + "fields.password.label": "Парола", + "fields.structure.add": "Добави", + "fields.structure.add.first": "Добавете първото въвеждане", + "fields.structure.empty": "Няма все още въвеждания", + "fields.structure.entry.error": "Това не може да бъде намерено", + "fields.structure.cancel": "Откажи", + "fields.structure.save": "Ок", + "fields.structure.edit": "Редактирай", + "fields.structure.delete": "Изтрий", + "fields.structure.delete.label": "Наистина ли искате да изтриете това вписване?", + "fields.tags.label": "Етикети", + "fields.tel.label": "Телефон", + "fields.textarea.buttons.bold.label": "Получер шрифт", + "fields.textarea.buttons.bold.text": "Получер шрифт", + "fields.textarea.buttons.italic.label": "Наклонен шрифт", + "fields.textarea.buttons.italic.text": "Наклонен шрифт", + "fields.textarea.buttons.link.label": "Връзка", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Изображение", + "fields.textarea.buttons.file.label": "Файл", + "fields.toggle.yes": "Да", + "fields.toggle.no": "Не", + "fields.toggle.on": "Вкл.", + "fields.toggle.off": "Изкл.", + "fields.error.missing.controller": "field controller файлът липсва", + "fields.error.missing.class": "field controller класът липсва", + "fields.error.route.invalid": "Невалиден field route", + "fields.error.extended": "Полето не може да бъде extended", + "editor.link.url.label": "Вмъкни URL", + "editor.link.text.label": "Текст връзка", + "editor.link.text.help": "Текстовия линк е по избор", + "editor.email.address.label": "Вмъкни email адрес", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Текст връзка", + "editor.email.text.help": "Текстовия линк е по избор", + "editor.file.empty": "Тази страница не съдържа файлове", + "editor.image.empty": "Тази страница няма изображения", + "autocomplete.method.error": "Невалиден метод за автоматично довършване", + "blueprints.error.default.missing": "Липсва blueprint по подразбиране", + "error": "Грешка", + "error.headline": "Грешка" +} \ No newline at end of file diff --git a/panel/app/translations/bg/package.json b/panel/app/translations/bg/package.json new file mode 100644 index 0000000..dc59f74 --- /dev/null +++ b/panel/app/translations/bg/package.json @@ -0,0 +1,4 @@ +{ + "title": "Bulgarian", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ca/core.json b/panel/app/translations/ca/core.json new file mode 100644 index 0000000..5b0c208 --- /dev/null +++ b/panel/app/translations/ca/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancel·lar", + "add": "Afegir", + "addit": "Afegir i Editar", + "save": "Desar", + "saved": "Desat!", + "change": "Canviar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "Ok", + "routes.error.invalid": "URL del \"Panel\" incorrecte", + "controller.error.invalid": "\"Controller\" incorrecte", + "controller.error.action": "Acció incorrecte", + "view.error.invalid": "Vista incorrecte:", + "options.show": "Mostrar opcions", + "options.hide": "Ocultar opcions", + "installation": "Isntal·lació", + "installation.check.headline": "Instal·lació de Kirby Panel", + "installation.check.text": "Kirby ha trobat els següents problemes durant l'instal·lació…", + "installation.check.retry": "Reintentar", + "installation.check.error": "Hi ha alguns problemes!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no té permís d'escriptura", + "installation.check.error.avatars": "/assets/avatars no té permís d'escriptura", + "installation.check.error.blueprints": "Afegeix el directori /site/blueprints", + "installation.check.error.content": "El directori de contingut i tots els seus arxius i subdirectoris han de tenir permís d'escriptura.", + "installation.check.error.thumbs": "El directori de \"thumbs\" ha de tenir permís d'escriptura.", + "installation.signup.username.label": "Crea la teva primera compta", + "installation.signup.username.placeholder": "Nom d'usuari", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemple.com", + "installation.signup.password.label": "Contrasenya", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crea la teva compta", + "login": "Entrar", + "login.welcome": "Entra amb la teva nova compta", + "login.username.label": "Nom d'usuari", + "login.password.label": "Contrasenya", + "login.error": "Nom d'usuari o contrasenya incorrectes", + "login.button": "Entrar", + "login.log.error.permissions": "L'arxiu de log de Login no té permís d'escriptura", + "logout": "Tancar sessió", + "topbar.error.class.definition": "La definició del topbar no s'ha trobat per a la classe:", + "dashboard": "Escriptori", + "dashboard.index.pages.title": "Pàgines", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Afegir", + "dashboard.index.site.title": "URL de la web", + "dashboard.index.account.title": "La teva compta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Opcions de la web", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Últimes modificacions", + "dashboard.index.history.text": "Aquí es mostraran les últimes modificacions realitzades per facilitar el seu posterior accés.", + "dashboard.index.license.title": "Llicència Kirby", + "dashboard.index.license.text": "Sembla que estàs fent servir Kirby en un servidor públic sense una llicència vàlida!\n\nDona suport a Kirby i (link: {buy} text: compra una llicència ara)\n\nSi ja tens una llicència comprada, afegeix-la al arxiu de configuració: (link: {docs} text: site/config/config.php)", + "metatags": "Opcions de la web", + "metatags.info": "Informació Kirby", + "metatags.license": "Llicència Kirby", + "metatags.version.toolkit": "Versió del Toolkit", + "metatags.version.kirby": "Versió de Kirby", + "metatags.version.panel": "Versió del Panel", + "metatags.back": "Tornar a l'escriptori", + "metatags.files": "Arxius de la web", + "site.delete.error": "La web no es pot eliminar", + "pages.show.settings": "Configuració de la pàgina", + "pages.show.preview": "Obrir vista prèvia", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Canviar URL", + "pages.show.invisible": "Estat: invisible", + "pages.show.visible": "Estat: visible", + "pages.show.changes.text": "Hi ha canvis pendents de desar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar aquesta pàgina", + "pages.show.subpages.title": "Pàgines", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Afegir", + "pages.show.subpages.empty": "Aquesta pàgina no té cap subpàgina", + "pages.show.files.title": "Arxius", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Afegir", + "pages.show.files.empty": "Aquesta pàgina no té cap arxiu", + "pages.show.error.permissions.title": "Aquesta pàgina no té permís d'escriptura", + "pages.show.error.permissions.text": "Revisa els permisos del directori de contingut i de tots els seus arxius.", + "pages.show.error.permissions.retry": "Reintentar", + "pages.show.error.notitle.title": "El \"blueprint\" de la pàgina no té cap camp definit per al títol", + "pages.show.error.notitle.text": "Afegeix un camp de títol i torna a intentar-ho", + "pages.show.error.notitle.retry": "Reintentar", + "pages.show.error.form": "Completa tots els camps correctament.", + "pages.add.title.label": "Afegir una nova pàgina", + "pages.add.title.placeholder": "Títol", + "pages.add.url.label": "URL-apèndix", + "pages.add.url.enter": "(introdueix el teu títol)", + "pages.add.url.close": "Tancar", + "pages.add.url.help": "Format: minúscules a-z, 0-9 i guions regulars", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "La pàgina no pot crear", + "pages.add.error.title": "Falta el títol", + "pages.add.error.template": "La plantilla no s'ha trobat", + "pages.add.error.max.headline": "No es permés afegir noves pàgines", + "pages.add.error.max.text": "S'ha arribat al número màxim de subpàgines per aquesta pàgina.", + "pages.url.uid.label": "URL-apèndix", + "pages.url.uid.label.option": "Crear a partir del títol", + "pages.url.error.exists": "Ja existeix una altra pàgina amb el mateix apèndix", + "pages.url.error.move": "L'apèndix no es pot modificar", + "pages.url.error.rights": "No pots canviar la URL d'aquest pàgina", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "Els següents camps canviaran quan canviïs de plantilla", + "pages.template.warning.removed": "Eliminar camps", + "pages.template.warning.replaced": "Reemplaçar camps", + "pages.template.warning.added": "Camps afegits", + "pages.template.error": "La plantilla d'aquesta pàgina no es pot canviar", + "pages.toggle.position": "Posició", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Estàs segur de canviar l'estat de la pàgina a **visible**?", + "pages.toggle.hide": "Estàs segur de canviar l'estat de la pàgina a **invisible**?", + "pages.toggle.error.error": "L'estat de la pàgina d'error no es pot modificar", + "pages.delete.headline": "Estàs segur d'eliminar aquesta pàgina?", + "pages.delete.error.home.headline": "La pàgina d'inici no es pot eliminar", + "pages.delete.error.home.text": "Estàs intentant eliminar la pàgina d'inici. Això no és possible, i donaria lloc a efectes no desitjats.", + "pages.delete.error.error.headline": "La pàgina d'error no es pot eliminar", + "pages.delete.error.error.text": "Estàs intentant eliminar la pàgina d'error. Això no és possible, i donaria lloc a efectes no desitjats.", + "pages.delete.error.children.headline": "La pàgina no es pot eliminar", + "pages.delete.error.children.text": "Aquesta pàgina té subpàgines i no es pot eliminar. Per eliminar-la, cal eliminar primer totes les seves subpàgines.", + "pages.delete.error.blocked.headline": "La pàgina no es pot eliminar", + "pages.delete.error.blocked.text": "Aquesta pàgina esta bloquejada i no es pot eliminar.", + "pages.search.help": "Cercar pàgines per URL. Navega a través dels resultats de la cerca amb les tecles de fletxa cap amunt i cap avall i prem enter per saltar a la pàgina seleccionada.", + "pages.search.noresults": "No hi ha resultats de la cerca realitzada. Intenta-ho de nou amb una altra URL.", + "pages.error.missing": "La pàgina no s'ha trobat", + "subpages": "Pàgines", + "subpages.index.headline": "Pàgines a", + "subpages.index.back": "Tornar", + "subpages.index.add": "Afegir una nova pàgina", + "subpages.index.add.first.text": "Aquesta pàgina encara no té subpàgines", + "subpages.index.add.first.button": "Afegeix la primera pàgina", + "subpages.index.visible": "Pàgines visibles", + "subpages.index.visible.help": "Arrossega les pàgines invisibles aquí per ordenar-les i fer-les visibles.", + "subpages.index.invisible": "Pàgines invisibles", + "subpages.index.invisible.help": "Arrossega les pàgines visibles aquí per fer-les invisibles.", + "subpages.add.error": "Aquesta pàgina no pot tenir subpàgines", + "subpages.add.error.more": "Aquesta pàgina no pot tenir més subpàgines", + "subpages.error.missing": "La pàgina no s'ha trobat", + "files": "Arxius", + "files.index.headline": "Arxius per a", + "files.index.back": "Tornar", + "files.index.upload": "Carregar un nou arxiu", + "files.index.upload.first.text": "Aquesta pàgina encara no té cap arxiu", + "files.index.upload.first.button": "Carregar el primer arxiu", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "Aquesta pàgina no pot tenir cap arxiu", + "files.add.error.max": "S'ha arribat al número màxim d'arxius per aquesta pàgina.", + "files.add.error.extension.missing": "No pots carregar arxius sense extensió", + "files.add.error.extension.forbidden": "Extensión de l'arxiu prohibida", + "files.add.error.mime.forbidden": "Mime type de l'arxiu prohibit", + "files.add.error.htaccess": "L'arxiu htaccess no es pot carregar", + "files.add.error.invisible": "Els arxius invisibles no es pode carregar", + "files.add.blueprint.type.error": "La pàgina només accepta:", + "files.add.blueprint.size.error": "La pàgina només accepta arxius amb un volum de", + "files.show.name.label": "Nom de l'arxiu", + "files.show.info.label": "Tipus / Volum / Dimensions", + "files.show.link.label": "Enllaç públic", + "files.show.open": "Mostrar / Descarregar arxiu", + "files.show.back": "Tornar", + "files.show.replace": "Reemplaçar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "L'arxiu no pot ser renombrat", + "files.show.error.form": "Completa tots els camps correctament.", + "files.upload.drop": "Arrossega els arxius aquí…", + "files.upload.click": "… o clica per carregar-los", + "files.replace.drop": "Arrossega l'arxiu aquí…", + "files.replace.click": "… o clica per reeplaçar-lo", + "files.replace.error.type": "L'arxiu carregat ha de ser del mateix tipus", + "files.delete.headline": "Estàs segur d'eliminar aquest arxiu?", + "files.error.missing.page": "La pàgina no s'ha trobat", + "files.error.missing.file": "L'arxiu no s'ha trobat", + "users": "Usuaris", + "users.index.headline": "Tots els usuaris", + "users.index.add": "Afegir un nou usuari", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Nom d'usuari", + "users.form.username.placeholder": "Nom d'usuari", + "users.form.username.help": "Caràcters permesos: minúscules a-z, 0-9 i guions regulars", + "users.form.username.readonly": "El nom d'usuari no es pot canviar", + "users.form.firstname.label": "Nom", + "users.form.lastname.label": "Cognom", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemple.com", + "users.form.password.label": "Contrasenya", + "users.form.password.confirm.label": "Confirmar contrasenya", + "users.form.password.new.label": "Nova contrasenya", + "users.form.password.new.confirm.label": "Confirmar la nova contrasenya", + "users.form.password.new.help": "Deixa en blanc per mantenir la contrasenya actual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opcions de la compta", + "users.form.options.message": "Enviar correu electrònic", + "users.form.options.delete": "Eliminar compte", + "users.form.avatar.headline": "Imatge del perfil", + "users.form.avatar.upload": "Carregar imatge del perfil", + "users.form.avatar.replace": "Reemplaçar imatge del perfil", + "users.form.avatar.delete": "Eliminar imatge del perfil", + "users.form.back": "Tornar als usuaris", + "users.form.error.password.confirm": "Confirma la contrasenya", + "users.form.error.update": "L'usuari no s'ha pogut actualitzar", + "users.form.error.update.rights": "No pots actualitzar aquest usuari", + "users.form.error.create": "L'usuari no es pot crear", + "users.form.error.permissions.title": "El directori de comptes no té permís d'escriptura", + "users.form.error.permissions.text": "Assegure't de que el directori /site/accounts existeix i té permís d'escriptura", + "users.delete.headline": "Estàs segur d'eliminar aquest usuari?", + "users.delete.error": "L'usuari no es pot eliminar", + "users.delete.error.permission": "No pots eliminar usuaris", + "users.delete.error.permission.single": "No pots eliminar aquest usuari", + "users.delete.error.lastadmin": "No es pot eliminar l'últim administrador", + "users.avatar.drop": "Arrossega la imatge del perfil aquí…", + "users.avatar.click": "… o clica per carregar-la", + "users.avatar.error.type": "Només pot carregar arxius JPG, PNG i GIF", + "users.avatar.error.folder.headline": "La carpeta avatar no té permís d'escriptura", + "users.avatar.error.folder.text": "Si us plau, creï la carpeta /assets/avatars i doneu-li permisos d'escriptura per a pujar fotos del perfil.", + "users.avatar.error.permission": "No pots modificar l'avatar", + "users.avatar.delete.error": "La imatge del perfil no s'ha pogut eliminar", + "users.avatar.delete.error.permission": "No pots eliminar l'avatar d'aquest usuari", + "users.avatar.delete.success": "La imatge del perfil ha estat eliminada", + "users.avatar.missing": "Aquest usuari no té avatar", + "users.error.missing": "L'usuari no s'ha trobat", + "user.error.lastadmin": "Ets l'únic usuari administrador. No es pot modificar.", + "form.error.missing": "El formulari no s'ha trobat", + "form.construct.error.invalid": "Métode de construcció del formulari incorrecte", + "fields.required": "Obligatori", + "fields.date.label": "Data", + "fields.date.months": [ + "Gener", + "Febrer", + "Març", + "Abril", + "Maig", + "Juny", + "Juliol", + "Agost", + "Setembre", + "Octubre", + "Novembre", + "Desembre" + ], + "fields.date.weekdays": [ + "Diumenge", + "Dilluns", + "Dimarts", + "Dimecres", + "Dijous", + "Divendres", + "Dissabte" + ], + "fields.date.weekdays.short": [ + "dg.", + "dl.", + "dt.", + "dc.", + "dj.", + "dv.", + "ds." + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemple.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Pàgina", + "fields.page.placeholder": "ruta/a/pàgina", + "fields.password.label": "Contrasenya", + "fields.structure.add": "Afegir", + "fields.structure.add.first": "Afegir la primera entrada", + "fields.structure.empty": "Encara no hi ha entrades.", + "fields.structure.entry.error": "No s'ha trobat", + "fields.structure.cancel": "Cancel·lar", + "fields.structure.save": "Ok", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "Estàs segur d'eliminar aquesta entrada?", + "fields.tags.label": "Etiquetes", + "fields.tel.label": "Telèfon", + "fields.textarea.buttons.bold.label": "Texte negreta", + "fields.textarea.buttons.bold.text": "Texte negreta", + "fields.textarea.buttons.italic.label": "Texte cursiva", + "fields.textarea.buttons.italic.text": "Texte cursiva", + "fields.textarea.buttons.link.label": "Enllaç", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imatge", + "fields.textarea.buttons.file.label": "Arxiu", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "Encès", + "fields.toggle.off": "Apagat", + "fields.error.missing.controller": "El \"Controller\" del camp no s'ha trobat", + "fields.error.missing.class": "La classe del \"Controller\" del camp no s'ha trobat", + "fields.error.route.invalid": "Ruta del camp incorrecte", + "fields.error.extended": "El camp no es pot extendre", + "editor.link.url.label": "Inserta URL", + "editor.link.text.label": "Texte enllaçat", + "editor.link.text.help": "L'enllaç és opcional", + "editor.email.address.label": "Inserta adressa d'email", + "editor.email.address.placeholder": "mail@exemple.com", + "editor.email.text.label": "Texte enllaçat", + "editor.email.text.help": "L'enllaç és opcional", + "editor.file.empty": "Aquesta pàgina no té fitxers", + "editor.image.empty": "Aquesta pàgina no té imatges", + "autocomplete.method.error": "Mètode d'autocompletar incorrecte", + "blueprints.error.default.missing": "El \"blueprint\" per defecte no s'ha trobat", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/ca/package.json b/panel/app/translations/ca/package.json new file mode 100644 index 0000000..c1b8934 --- /dev/null +++ b/panel/app/translations/ca/package.json @@ -0,0 +1,4 @@ +{ + "title": "Catalan", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/cs/core.json b/panel/app/translations/cs/core.json new file mode 100644 index 0000000..437f35d --- /dev/null +++ b/panel/app/translations/cs/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Zrušit", + "add": "Přidat", + "addit": "Přidat a upravit", + "save": "Uložit", + "saved": "Uloženo!", + "change": "Změnit", + "delete": "Smazat", + "insert": "Vložit", + "ok": "Ok", + "routes.error.invalid": "Neplatná URL panelu", + "controller.error.invalid": "Neplatný controller", + "controller.error.action": "Neplatná akce", + "view.error.invalid": "Neplatný view:", + "options.show": "Zobrazit možnosti", + "options.hide": "Skrýt možnosti", + "installation": "Instalace", + "installation.check.headline": "Instalace Kirby Panelu", + "installation.check.text": "Kirby během instalace narazilo na následující problémy…", + "installation.check.retry": "Zkusit znovu", + "installation.check.error": "Nastaly nějaké problémy!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts není zapisovatelné", + "installation.check.error.avatars": "/assets/avatars není zapisovatelné", + "installation.check.error.blueprints": "Prosím přidejte složku /site/blueprints", + "installation.check.error.content": "Složka content a všechny soubory a složky v ní musí být zapisovatelné.", + "installation.check.error.thumbs": "Složka thumbs musí být zapisovatelná.", + "installation.signup.username.label": "Vytvořte svůj první účet", + "installation.signup.username.placeholder": "Uživatelské jméno", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Heslo", + "installation.signup.language.label": "Jazky", + "installation.signup.button": "Vytvořit účet", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Uživatelské jméno", + "login.password.label": "Heslo", + "login.error": "Chybné jméno nebo heslo", + "login.button": "Log in", + "login.log.error.permissions": "Log soubor pro přihlášení není zapisovatelný.", + "logout": "Log out", + "topbar.error.class.definition": "Chybějící topbar definice pro třídu:", + "dashboard": "Přehled", + "dashboard.index.pages.title": "Stránky", + "dashboard.index.pages.edit": "Upravit", + "dashboard.index.pages.add": "Přidat", + "dashboard.index.site.title": "URL vašeho webu", + "dashboard.index.account.title": "Váš účet", + "dashboard.index.account.edit": "Upravit", + "dashboard.index.metatags.title": "Parametry webu", + "dashboard.index.metatags.edit": "Upravit", + "dashboard.index.history.title": "Vaše poslední změny", + "dashboard.index.history.text": "Zde budou zobrazeny stránky, které jste naposledy měnili, abyste k nim měli snadný přístup.", + "dashboard.index.license.title": "Kirby licence", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Parametry webu", + "metatags.info": "Kirby informace", + "metatags.license": "Kirby licence", + "metatags.version.toolkit": "Verze Toolkitu", + "metatags.version.kirby": "Verze Kirby", + "metatags.version.panel": "Verze panelu", + "metatags.back": "Zpět na přehled", + "metatags.files": "Soubory webu", + "site.delete.error": "Tento web nemůže být smazán", + "pages.show.settings": "Nastavení stránky", + "pages.show.preview": "Otevřít náhled", + "pages.show.template": "Šablona", + "pages.show.changeurl": "Změnit URL", + "pages.show.invisible": "Status: neviditelný", + "pages.show.visible": "Status: viditelný", + "pages.show.changes.text": "Máte neuložené změny!", + "pages.show.changes.button": "Zahodit", + "pages.show.delete": "Smazat tuto stránku", + "pages.show.subpages.title": "Stránky", + "pages.show.subpages.edit": "Upravit", + "pages.show.subpages.add": "Přidat", + "pages.show.subpages.empty": "Tato strana nemá podstrany", + "pages.show.files.title": "Soubory", + "pages.show.files.edit": "Upravit", + "pages.show.files.add": "Přidat", + "pages.show.files.empty": "Tato strana nemá soubory", + "pages.show.error.permissions.title": "Tato strana není zapisovatelná", + "pages.show.error.permissions.text": "Prosím zkontrolujte nastavení složky content a všech jejích podsložek a souborů", + "pages.show.error.permissions.retry": "Zkusit znovu", + "pages.show.error.notitle.title": "Blueprint nemá položku title", + "pages.show.error.notitle.text": "Prosím přidejte title a zkuste to znovu", + "pages.show.error.notitle.retry": "Zkusit znovu", + "pages.show.error.form": "Prosím vyplňte správně všechny položky", + "pages.add.title.label": "Přidat novou stránku", + "pages.add.title.placeholder": "Název", + "pages.add.url.label": "Přípona URL", + "pages.add.url.enter": "(vložte svůj název)", + "pages.add.url.close": "Zavřit", + "pages.add.url.help": "Formát: malé a-z, 0-9 a pomlčky", + "pages.add.template.label": "Šablona", + "pages.add.error.create": "Stránka nemohla být vytvořena", + "pages.add.error.title": "Chybí název", + "pages.add.error.template": "Šablona chybí", + "pages.add.error.max.headline": "Nové stránky nejsou povoleny", + "pages.add.error.max.text": "Bylo dosaženo maximálního počtu podstran pro tuto stránku", + "pages.url.uid.label": "Přípona URL", + "pages.url.uid.label.option": "Vytvořit z názvu", + "pages.url.error.exists": "Stránka se stejnou příponou již existuje", + "pages.url.error.move": "Nepodařilo se změnit příponu", + "pages.url.error.rights": "Nemůžete změnit URL této stránky", + "pages.template.select.label": "Šablona", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Pozice", + "pages.toggle.invisible": "neviditelný", + "pages.toggle.publish": "Opravdu chcete změnit status této stránky na **viditelný?**", + "pages.toggle.hide": "Opravdu chcete změnit status této stránky na **neviditelný?**", + "pages.toggle.error.error": "Status této error stránky nemůže být změněn", + "pages.delete.headline": "Opravdu chcete smazat tuto stránku?", + "pages.delete.error.home.headline": "Úvodní stránka nemůže být smazána", + "pages.delete.error.home.text": "Snažíte se smazat úvodní stránka webu. To není možné a způsobilo by to nechtěné problémy.", + "pages.delete.error.error.headline": "Chybová stránka nemůže být smazána", + "pages.delete.error.error.text": "Snažíte se smazat chybovou stránku. To není možné a způsobilo by to nechtěné problémy.", + "pages.delete.error.children.headline": "Stránka nemůže být smazána", + "pages.delete.error.children.text": "Tato stránka má podstránky a proto nemůže být smazána. Nejdříve prosím smažte všechny podstránky.", + "pages.delete.error.blocked.headline": "TStránka nemůže být smazána", + "pages.delete.error.blocked.text": "Tato stránka je zamčená a proto nemůže být smazána.", + "pages.search.help": "Vyhledat stránku pomocí URL. Pro přechod mezi výsledky použijte šipky nahodu a dolů. Pomocí klávesy Enter pak stránku vyberte.", + "pages.search.noresults": "Pro váš dotaz nejsou žádné výsledky. Zkuste to znovu s jinou URL.", + "pages.error.missing": "Stránku se nepodařilo nalézt.", + "subpages": "Stránky", + "subpages.index.headline": "Stránky v", + "subpages.index.back": "Zpět", + "subpages.index.add": "Přidat novou stránku", + "subpages.index.add.first.text": "Tato stránka ještě nemá podstránky", + "subpages.index.add.first.button": "Přidat první stránku", + "subpages.index.visible": "Viditelné stránky", + "subpages.index.visible.help": "Přetáhněte sem neviditelné stránky, pokud je chcete řadit a udělat viditelné.", + "subpages.index.invisible": "Neviditelné stránky", + "subpages.index.invisible.help": "Přetáhněte sem viditelné stránky, pokud je chcete udělat neviditelné a přestat je řadit.", + "subpages.add.error": "Tato stránka nemůže mít podstrany", + "subpages.add.error.more": "Tato stránka nemůže mít žádné další podstrany", + "subpages.error.missing": "Stránku se nepodařilo nalézt", + "files": "Soubory", + "files.index.headline": "Soubory pro", + "files.index.back": "Zpět", + "files.index.upload": "Nahrát nový soubor", + "files.index.upload.first.text": "Tato stránka ješte nemá žádné soubory", + "files.index.upload.first.button": "Nahrajte první soubor", + "files.index.edit": "Upravit", + "files.index.delete": "Smazat", + "files.index.error.disabled": "Tato stránka nemůže mít soubory", + "files.add.error.max": "Bylo dosaženo maximálního počtu souborů pro tuto stránku", + "files.add.error.extension.missing": "Nemůžete nahrát soubor bez přípony", + "files.add.error.extension.forbidden": "Zakázaná přípona souboru", + "files.add.error.mime.forbidden": "Zakázaný mime typ", + "files.add.error.htaccess": "Soubory htaccess nemohou být nahrány", + "files.add.error.invisible": "Neviditelné soubory nemohou být nahrány", + "files.add.blueprint.type.error": "Stránka povoluje pouze:", + "files.add.blueprint.size.error": "Stránka povoluje pouze soubory velikosti", + "files.show.name.label": "Jméno souboru", + "files.show.info.label": "Typ / Velikost / Rozměry", + "files.show.link.label": "Veřejný odkaz", + "files.show.open": "Ukázat/stáhnout soubot", + "files.show.back": "Zpět", + "files.show.replace": "Nahradit", + "files.show.delete": "Smazat", + "files.show.error.rename": "Soubor se nepodařilo odstranit", + "files.show.error.form": "Prosím vyplňte správně všechny položky", + "files.upload.drop": "Sem přetáhněte soubory…", + "files.upload.click": "…nebo klikněte pro nahrání", + "files.replace.drop": "Sem přetáhněte soubor…", + "files.replace.click": "…nebo klikněte pro nahrazení", + "files.replace.error.type": "Nahraný soubor musí být stejného typu", + "files.delete.headline": "Opravdu chcete smazat tento soubor?", + "files.error.missing.page": "Stránku se nepodařilo nalézt", + "files.error.missing.file": "Soubor se nepodařilo nalézt", + "users": "Uživatelé", + "users.index.headline": "Všichni uživatelé", + "users.index.add": "Přidat nového uživatele", + "users.index.edit": "Upravit", + "users.index.delete": "Smazat", + "users.form.username.label": "Uživatelské jméno", + "users.form.username.placeholder": "Vaše uživatelské jméno", + "users.form.username.help": "Povolené znaky: malé a-z, 0-9 a pomlčky", + "users.form.username.readonly": "Uživatelské jméno nelze změnit", + "users.form.firstname.label": "Křestní jméno", + "users.form.lastname.label": "Příjmení", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Heslo", + "users.form.password.confirm.label": "Potvrdit heslo", + "users.form.password.new.label": "Nové heslo", + "users.form.password.new.confirm.label": "Potvrďte nové heslo", + "users.form.password.new.help": "Nechte prázdné pro zachování současného hesla", + "users.form.language.label": "Jazyk", + "users.form.role.label": "Role", + "users.form.options.headline": "Možnosti účtu", + "users.form.options.message": "Poslat email", + "users.form.options.delete": "Smazat účet", + "users.form.avatar.headline": "Profilový obrázek", + "users.form.avatar.upload": "Nahrát profilový obrázek", + "users.form.avatar.replace": "Nahradit profilový obrázek", + "users.form.avatar.delete": "Smazat profilový obrázek", + "users.form.back": "Zpět na uživatele", + "users.form.error.password.confirm": "Prosím potvrďte heslo", + "users.form.error.update": "Uživatel nemohl být aktualizován", + "users.form.error.update.rights": "Nemáte dovoleno měnit tohoto uživatele", + "users.form.error.create": "Uživatel nemohl být vytvořen", + "users.form.error.permissions.title": "Složka account není zapisovatelná", + "users.form.error.permissions.text": "Prosím ověřte že složka /site/accounts existuje a je zapisovatelná.", + "users.delete.headline": "Opravdu chcete smazat tohoto uživatele?", + "users.delete.error": "Uživatel nemohl být smazán", + "users.delete.error.permission": "Nemáte dovoleno mazat uživatele", + "users.delete.error.permission.single": "Nemáte dovoleno smazat tohoto uživatele", + "users.delete.error.lastadmin": "Nemůžete smazat posledního administrátora", + "users.avatar.drop": "Sem přetáhněte uživatelský obrázek…", + "users.avatar.click": "…nebo klikněte pro nahrání", + "users.avatar.error.type": "Můžete nahrát pouze soubory typu JPG, PNG a GIF", + "users.avatar.error.folder.headline": "Složka avatar není zapisovatelná", + "users.avatar.error.folder.text": "Prosím vytvořte složku /assets/avatars a udělejte jí zapisovatelnou, aby bylo možné nahrát obrázek.", + "users.avatar.error.permission": "Nemáte dovoleno měnit avatara", + "users.avatar.delete.error": "Nebylo možné smazat profilový obrázek", + "users.avatar.delete.error.permission": "Nemáte dovoleno smazat avatara tomuto uživateli", + "users.avatar.delete.success": "Profilový obrázek byl smazán", + "users.avatar.missing": "Tento uživatel žádného avatara nemá", + "users.error.missing": "Uživatele se nepodařilo nalézt", + "user.error.lastadmin": "Jste jediným administrátorem. Toto nelze změnit.", + "form.error.missing": "Formulář se nepovedlo nalézt", + "form.construct.error.invalid": "Neplatná konstrukční metoda formuláře", + "fields.required": "Vyžadované", + "fields.date.label": "Datum", + "fields.date.months": [ + "Leden", + "Únor", + "Březen", + "Duben", + "Květen", + "Červen", + "Červenec", + "Srpen", + "Září", + "Říjen", + "Listopad", + "Prosinec" + ], + "fields.date.weekdays": [ + "neděle", + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota" + ], + "fields.date.weekdays.short": [ + "ne", + "po", + "út", + "st", + "čt", + "pá", + "so" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Číslo", + "fields.number.placeholder": "#", + "fields.page.label": "Stránka", + "fields.page.placeholder": "cesta/ke/strance", + "fields.password.label": "Heslo", + "fields.structure.add": "Přidat", + "fields.structure.add.first": "Přidat první záznam", + "fields.structure.empty": "Zatím nejsou žádné záznamy.", + "fields.structure.entry.error": "Položku se nepodařilo najít", + "fields.structure.cancel": "Zrušit", + "fields.structure.save": "Uložit", + "fields.structure.edit": "Upravit", + "fields.structure.delete": "Smazat", + "fields.structure.delete.label": "Opravdu chcete smazat tento záznam?", + "fields.tags.label": "Štítky", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Tučný text", + "fields.textarea.buttons.bold.text": "Tučný text", + "fields.textarea.buttons.italic.label": "Kurzíva", + "fields.textarea.buttons.italic.text": "Kurzíva", + "fields.textarea.buttons.link.label": "Odkaz", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Obrázek", + "fields.textarea.buttons.file.label": "Soubor", + "fields.toggle.yes": "Ano", + "fields.toggle.no": "Ne", + "fields.toggle.on": "Zap", + "fields.toggle.off": "Vyp", + "fields.error.missing.controller": "Soubor controller pro toto políčko chybí", + "fields.error.missing.class": "Třída controller pro toto políčko chybí", + "fields.error.route.invalid": "Neplatná cesta políčka", + "fields.error.extended": "Toto políčko nemůže být rozšířeno", + "editor.link.url.label": "Cílové URL", + "editor.link.text.label": "Text odkazu", + "editor.link.text.help": "Text odkazu je nepovinný", + "editor.email.address.label": "Vložit emailovou adresu", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Text odkazu", + "editor.email.text.help": "Text odkazu je nepovinný", + "editor.file.empty": "Tato stránka nemá žádné soubory", + "editor.image.empty": "Tato stránka nemá žádné obrázky", + "autocomplete.method.error": "Neplatná našeptávací metoda", + "blueprints.error.default.missing": "Chybí výchozí blueprint", + "error": "Chyba", + "error.headline": "Chyba" +} \ No newline at end of file diff --git a/panel/app/translations/cs/package.json b/panel/app/translations/cs/package.json new file mode 100644 index 0000000..ce3af3d --- /dev/null +++ b/panel/app/translations/cs/package.json @@ -0,0 +1,4 @@ +{ + "title": "Česky", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/da/core.json b/panel/app/translations/da/core.json new file mode 100644 index 0000000..0f78264 --- /dev/null +++ b/panel/app/translations/da/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuller", + "add": "Ny", + "addit": "Tilføj & Rediger", + "save": "Gem", + "saved": "Gemt!", + "change": "Ændre", + "delete": "Slet", + "insert": "Indsæt", + "ok": "Ok", + "routes.error.invalid": "Ugyldig Panel URL", + "controller.error.invalid": "Ugyldig controller", + "controller.error.action": "Ugyldig handling", + "view.error.invalid": "Ugyldig visning:", + "options.show": "Vis indstillinger", + "options.hide": "Skjul indstillinger", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby stødte på følgende problemer under installationen…", + "installation.check.retry": "Prøv igen", + "installation.check.error": "Der er nogle problemer!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts er ikke skrivbar", + "installation.check.error.avatars": "/assets/avatars er ikke skrivbar", + "installation.check.error.blueprints": "Tilføj venligst en /site/blueprints mappe", + "installation.check.error.content": "Content mappen samt alle underliggende filer og mapper skal være skrivbare.", + "installation.check.error.thumbs": "Thumbs mappen skal være skrivbar.", + "installation.signup.username.label": "Opret din første konto", + "installation.signup.username.placeholder": "Brugernavn", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@eksempel.dk", + "installation.signup.password.label": "Adgangskode", + "installation.signup.language.label": "Sprog", + "installation.signup.button": "Opret din konto", + "login": "Log ind", + "login.welcome": "Log ind med din nye konto", + "login.username.label": "Brugernavn", + "login.password.label": "Adgangskode", + "login.error": "Ugyldigt brugernavn eller adgangskode", + "login.button": "Log ind", + "login.log.error.permissions": "Login log fil er ikke skrivbar", + "logout": "Log ud", + "topbar.error.class.definition": "Mangler topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Sider", + "dashboard.index.pages.edit": "Rediger", + "dashboard.index.pages.add": "Tilføj", + "dashboard.index.site.title": "Dit websites URL", + "dashboard.index.account.title": "Din konto", + "dashboard.index.account.edit": "Rediger", + "dashboard.index.metatags.title": "Website indstillinger", + "dashboard.index.metatags.edit": "Rediger", + "dashboard.index.history.title": "Dine seneste opdateringer", + "dashboard.index.history.text": "Dine seneste opdaterede sider vil blive vist her, for at gøre det nemt at finde dem igen senere.", + "dashboard.index.license.title": "Kirby licens", + "dashboard.index.license.text": "Det ser ud til at du kører Kirby på en offentlig server uden en gyldig licens!\n\nStøt venligst Kirby og (link: {buy} text: køb en licens nu)\n\nHar du allerede en licens nøgle, kan du blot tilføje den i din config fil: (link: {docs} text: site/config/config.php)", + "metatags": "Website indstillinger", + "metatags.info": "Kirby info", + "metatags.license": "Kirby licens", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Tilbage til dashboard", + "metatags.files": "Website filer", + "site.delete.error": "Sitet kan ikke slettes", + "pages.show.settings": "Side indstillinger", + "pages.show.preview": "Se eksempel", + "pages.show.template": "Skabelon", + "pages.show.changeurl": "Ændre URL", + "pages.show.invisible": "Status: usynlig", + "pages.show.visible": "Status: synlig", + "pages.show.changes.text": "Du har ugemte ændringer!", + "pages.show.changes.button": "Kassér", + "pages.show.delete": "Slet denne side", + "pages.show.subpages.title": "Sider", + "pages.show.subpages.edit": "Rediger", + "pages.show.subpages.add": "Tilføj", + "pages.show.subpages.empty": "Denne side har ingen undersider", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Rediger", + "pages.show.files.add": "Tilføj", + "pages.show.files.empty": "Denne side har ingen filer", + "pages.show.error.permissions.title": "Siden er ikke skrivbar", + "pages.show.error.permissions.text": "Kontroller venligst skriverettigheder for content mappen samt alle filer.", + "pages.show.error.permissions.retry": "Prøv igen", + "pages.show.error.notitle.title": "Dette blueprint har intet titel-felt", + "pages.show.error.notitle.text": "Tilføj venligst et titel-felt og prøv igen", + "pages.show.error.notitle.retry": "Prøv igen", + "pages.show.error.form": "Udfyld venligst alle felter korrekt", + "pages.add.title.label": "Tilføj en ny side", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-appendiks", + "pages.add.url.enter": "(indtast din titel)", + "pages.add.url.close": "Luk", + "pages.add.url.help": "Format: små bogstaver a-z, 0-9 samt almindelige bindestreger", + "pages.add.template.label": "Skabelon", + "pages.add.error.create": "Siden kunne ikke oprettes", + "pages.add.error.title": "Titlen mangler", + "pages.add.error.template": "Skabelonen manger", + "pages.add.error.max.headline": "Der tillades ikke nye sider", + "pages.add.error.max.text": "Det maksimale antal undersider for denne side er nået.", + "pages.url.uid.label": "URL-appendiks", + "pages.url.uid.label.option": "Generer udfra titel", + "pages.url.error.exists": "En side med samme appendiks eksisterer allerede", + "pages.url.error.move": "URL-appendikset kunne ikke ændres", + "pages.url.error.rights": "Du kan ikke ændre denne sides URL", + "pages.template.select.label": "Skabelon", + "pages.template.warning.text": "Følgende felter ændres, hvis du skifter skabelon", + "pages.template.warning.removed": "Fjernede felter", + "pages.template.warning.replaced": "Erstattede felter", + "pages.template.warning.added": "Tilføjede felter", + "pages.template.error": "Skabelonen for denne side kan ikke ændres", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "usynlig", + "pages.toggle.publish": "Ønsker du virkelig at ændre denne sides status til **synlig?**", + "pages.toggle.hide": "Ønsker du virkelig at ændre denne sides status til **usynlig?**", + "pages.toggle.error.error": "Status for fejl-siden kan ikke ændres", + "pages.delete.headline": "Ønsker du virkelig at slette denne side?", + "pages.delete.error.home.headline": "Forsiden kunne ikke slettes", + "pages.delete.error.home.text": "Du forsøger at slette forsiden. Dette er ikke muligt da det ville lede til en uønsket oplevelse.", + "pages.delete.error.error.headline": "Fejlsiden kan ikke slettes", + "pages.delete.error.error.text": "Du forsøger at slette fejlsiden. Dette er ikke muligt da det ville lede til en uønsket oplevelse.", + "pages.delete.error.children.headline": "Siden kan ikke slettes", + "pages.delete.error.children.text": "Denne side har undersider og kan derfor ikke slettes. Slet venligst alle undersider først.", + "pages.delete.error.blocked.headline": "Siden kan ikke slettes", + "pages.delete.error.blocked.text": "Denne side er låst og kan derfor ikke slettes.", + "pages.search.help": "Søg efter sider udfra URL. Naviger igennem søgeresultater med dine op- og ned-piletaster og tryk enter for at hoppe til den valgte side.", + "pages.search.noresults": "Søgningen gav intet resultat. Prøv venligst igen med en anden URL.", + "pages.error.missing": "Siden kunne ikke findes", + "subpages": "Sider", + "subpages.index.headline": "Sider i", + "subpages.index.back": "Tilbage", + "subpages.index.add": "Tilføj en ny side", + "subpages.index.add.first.text": "Denne side har ingen undersider endnu", + "subpages.index.add.first.button": "Tilføj den første side", + "subpages.index.visible": "Synlige sider", + "subpages.index.visible.help": "Træk usynlige sider hertil for at sortere dem og gøre dem synlige.", + "subpages.index.invisible": "Usynlige sider", + "subpages.index.invisible.help": "Træk synlige sider hertil for at gøre dem usynlige.", + "subpages.add.error": "Denne side må ikke have undersider", + "subpages.add.error.more": "Denne side kan ikke have flere undersider", + "subpages.error.missing": "Siden kunne ikke findes", + "files": "Filer", + "files.index.headline": "Filer for", + "files.index.back": "Tilbage", + "files.index.upload": "Upload en ny fil", + "files.index.upload.first.text": "Denne side har ingen filer endnu", + "files.index.upload.first.button": "Upload den første fil", + "files.index.edit": "Rediger", + "files.index.delete": "Slet", + "files.index.error.disabled": "Denne side må ikke have filer", + "files.add.error.max": "Det maksimale antal filer for denne side er nået.", + "files.add.error.extension.missing": "Du kan ikke uploade filer uden fil-endelse", + "files.add.error.extension.forbidden": "Uacceptabel fil-endelse", + "files.add.error.mime.forbidden": "Uacceptabel fil-type", + "files.add.error.htaccess": "htaccess filer kan ikke uploades", + "files.add.error.invisible": "Usynlige filer kan ikke uploades", + "files.add.blueprint.type.error": "Siden tillader kun:", + "files.add.blueprint.size.error": "Siden tillader kun en fil-størrelse på:", + "files.show.name.label": "Filnavn", + "files.show.info.label": "Type / Vægt / Dimensioner", + "files.show.link.label": "Offentligt link", + "files.show.open": "Se og hent fil", + "files.show.back": "Tilbage", + "files.show.replace": "Erstat", + "files.show.delete": "Slet", + "files.show.error.rename": "Filen kunne ikke omdøbes", + "files.show.error.form": "Udfyld venligst alle felter korrekt", + "files.upload.drop": "Træk filer hertil…", + "files.upload.click": "…eller klik for at uploade", + "files.replace.drop": "Træk en fil hertil…", + "files.replace.click": "…eller klik for at erstatte", + "files.replace.error.type": "Den valgte fil skal være af samme filtype", + "files.delete.headline": "Ønsker du virkelig at slette denne fil?", + "files.error.missing.page": "Siden kunne ikke findes", + "files.error.missing.file": "Filen kunne ikke findes", + "users": "Brugere", + "users.index.headline": "Alle brugere", + "users.index.add": "Tilføj en ny bruger", + "users.index.edit": "Rediger", + "users.index.delete": "Slet", + "users.form.username.label": "Brugernavn", + "users.form.username.placeholder": "Dit brugernavn", + "users.form.username.help": "Tilladte tegn: små bogstaver a-z, 0-9 og bindestreger", + "users.form.username.readonly": "Brugernavnet kan ikke ændres", + "users.form.firstname.label": "Fornavn", + "users.form.lastname.label": "Efternavn", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@eksempel.dk", + "users.form.password.label": "Adgangskode", + "users.form.password.confirm.label": "Bekræft adgangskode", + "users.form.password.new.label": "Ny adgangskode", + "users.form.password.new.confirm.label": "Bekræft den nye adgangskode", + "users.form.password.new.help": "Lad stå tomt for at beholde den nuværende adgangskode", + "users.form.language.label": "Sprog", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Konto instillinger", + "users.form.options.message": "Send email", + "users.form.options.delete": "Slet konto", + "users.form.avatar.headline": "Profilbillede", + "users.form.avatar.upload": "Upload profilbillede", + "users.form.avatar.replace": "Erstat profilbillede", + "users.form.avatar.delete": "Slet profilbillede", + "users.form.back": "Tilbage til brugere", + "users.form.error.password.confirm": "Bekræft venligst adgangskoden", + "users.form.error.update": "Brugeren kunne ikke redigeres", + "users.form.error.update.rights": "Du har ikke tilladelse til at opdatere denne bruger", + "users.form.error.create": "Brugeren kunne ikke oprettes", + "users.form.error.permissions.title": "Account mappen er ikke skrivbar", + "users.form.error.permissions.text": "Sørg venligst for at /site/accounts eksisterer og er skrivbar.", + "users.delete.headline": "Ønsker du virkelig at slette denne bruger?", + "users.delete.error": "Brugeren kunne ikke slettes", + "users.delete.error.permission": "Du har ikke tilladelse til at slette brugere", + "users.delete.error.permission.single": "Du har ikke tilladelse til at slette denne bruger", + "users.delete.error.lastadmin": "Du kan ikke slette den sidste admin", + "users.avatar.drop": "Træk et profilbillede hertil…", + "users.avatar.click": "…eller klik for at vælge", + "users.avatar.error.type": "Du kan kun uploade JPG, PNG og GIF filer", + "users.avatar.error.folder.headline": "Avatar mappen er ikke skrivbar", + "users.avatar.error.folder.text": "Opret venligst mappen /assets/avatars og sørg for at den er skrivbar for at kunne uploade profilbilleder.", + "users.avatar.error.permission": "Du har ikke tilladelse til at ændre avatar", + "users.avatar.delete.error": "Profilbilledet kunne ikke slettes", + "users.avatar.delete.error.permission": "Du har ikke tilladelse til at slette denne brugers avatar", + "users.avatar.delete.success": "Profilbilledet er nu slettet", + "users.avatar.missing": "Denne bruger har ikke nogen avatar", + "users.error.missing": "Brugeren kunne ikke findes", + "user.error.lastadmin": "Du er den eneste admin. Dette kan ikke ændres.", + "form.error.missing": "Formularen kunne ikke findes", + "form.construct.error.invalid": "Ugyldig formular construction method", + "fields.required": "Påkrævet", + "fields.date.label": "Dato", + "fields.date.months": [ + "Januar", + "Februar", + "Marts", + "April", + "Maj", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "December" + ], + "fields.date.weekdays": [ + "Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag" + ], + "fields.date.weekdays.short": [ + "Søn", + "Man", + "Tir", + "Ons", + "Tor", + "Fre", + "Lør" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@eksempel.dk", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Side", + "fields.page.placeholder": "sti/til/side", + "fields.password.label": "Adgangskode", + "fields.structure.add": "Tilføj", + "fields.structure.add.first": "Tilføj den første indtastning", + "fields.structure.empty": "Ingen indtastninger endnu.", + "fields.structure.entry.error": "Emnet blev ikke fundet", + "fields.structure.cancel": "Annuller", + "fields.structure.save": "Gem", + "fields.structure.edit": "Rediger", + "fields.structure.delete": "Slet", + "fields.structure.delete.label": "Ønsker du virkelig at slette denne indtastning?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fed tekst", + "fields.textarea.buttons.bold.text": "Fed tekst", + "fields.textarea.buttons.italic.label": "Kursiv tekst", + "fields.textarea.buttons.italic.text": "Kursiv tekst", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Billede", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nej", + "fields.toggle.on": "Til", + "fields.toggle.off": "Fra", + "fields.error.missing.controller": "En field controller fil mangler", + "fields.error.missing.class": "En field controller class mangler", + "fields.error.route.invalid": "Ugyldig field route", + "fields.error.extended": "Field kan ikke blive extended", + "editor.link.url.label": "Indsæt URL", + "editor.link.text.label": "Link tekst", + "editor.link.text.help": "Link tekst er valgfri", + "editor.email.address.label": "Indsæt email adresse", + "editor.email.address.placeholder": "mail@eksempel.dk", + "editor.email.text.label": "Link tekst", + "editor.email.text.help": "Link tekst er valgfri", + "editor.file.empty": "Denne side har ingen filer", + "editor.image.empty": "Denne side har ingen billeder", + "autocomplete.method.error": "Ugyldig autocomplete method", + "blueprints.error.default.missing": "Mangler standard blueprint", + "error": "Fejl", + "error.headline": "Fejl" +} \ No newline at end of file diff --git a/panel/app/translations/da/package.json b/panel/app/translations/da/package.json new file mode 100644 index 0000000..24abc8f --- /dev/null +++ b/panel/app/translations/da/package.json @@ -0,0 +1,4 @@ +{ + "title": "Dansk", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/de/core.json b/panel/app/translations/de/core.json new file mode 100644 index 0000000..d442019 --- /dev/null +++ b/panel/app/translations/de/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Abbrechen", + "add": "Hinzufügen", + "addit": "Hinzufügen & Bearbeiten", + "save": "Speichern", + "saved": "Gespeichert!", + "change": "Ändern", + "delete": "Löschen", + "insert": "Einfügen", + "ok": "Ok", + "routes.error.invalid": "Ungültige Panel-URL", + "controller.error.invalid": "Ungültiger Controller", + "controller.error.action": "Ungültige Aktion", + "view.error.invalid": "Ungültiger View:", + "options.show": "Optionen einblenden", + "options.hide": "Optionen ausblenden", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby hat die folgenden Probleme festgestellt…", + "installation.check.retry": "Wiederholen", + "installation.check.error": "Es gibt einige Probleme!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts ist nicht beschreibbar", + "installation.check.error.avatars": "/assets/avatars ist nicht beschreibbar", + "installation.check.error.blueprints": "Bitte lege den Ordner /site/blueprints an", + "installation.check.error.content": "/content und alle Inhalte müssen beschreibbar sein.", + "installation.check.error.thumbs": "/thumbs muss vorhanden und beschreibbar sein.", + "installation.signup.username.label": "Erstelle den ersten Benutzer", + "installation.signup.username.placeholder": "Benutzername", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@beispiel.de", + "installation.signup.password.label": "Passwort", + "installation.signup.language.label": "Sprache", + "installation.signup.button": "Erstellen", + "login": "Anmelden", + "login.welcome": "Bitte melde dich mit deinem neuen Account an", + "login.username.label": "Benutzername", + "login.password.label": "Passwort", + "login.error": "Ungültiger Benutzername oder Passwort", + "login.button": "Anmelden", + "login.log.error.permissions": "Die Anmeldelog-Datei ist nicht beschreibbar.", + "logout": "Abmelden", + "topbar.error.class.definition": "Fehlende Topbar-Definition für Klasse:", + "dashboard": "Übersicht", + "dashboard.index.pages.title": "Seiten", + "dashboard.index.pages.edit": "Bearbeiten", + "dashboard.index.pages.add": "Hinzufügen", + "dashboard.index.site.title": "Seite", + "dashboard.index.account.title": "Dein Account", + "dashboard.index.account.edit": "Bearbeiten", + "dashboard.index.metatags.title": "Einstellungen", + "dashboard.index.metatags.edit": "Bearbeiten", + "dashboard.index.history.title": "Deine letzten Änderungen", + "dashboard.index.history.text": "Sobald du die ersten Änderungen an Seiten vornimmst, werden sie hier aufgelistet, um jeder Zeit schnell darauf zugreifen zu können.", + "dashboard.index.license.title": "Kirby Lizenz", + "dashboard.index.license.text": "Scheinbar nutzt Du Kirby auf einem öffentlichen Server ohne gültige Lizenz!\n\nBitte unterstütze Kirby und (link: {buy} text: kaufe jetzt eine Lizenz)\n\nWenn Du bereits einen Lizenzschlüssel hast, füge ihn zur Config-Datei hinzu: (link: {docs} text: site/config/config.php)", + "metatags": "Einstellungen", + "metatags.info": "Kirby Information", + "metatags.license": "Kirby Lizenz", + "metatags.version.toolkit": "Toolkit Version", + "metatags.version.kirby": "Kirby Version", + "metatags.version.panel": "Panel Version", + "metatags.back": "Zurück zur Übersicht", + "metatags.files": "Globale Dateien", + "site.delete.error": "Die Seite kann nicht gelöscht werden", + "pages.show.settings": "Seiteneinstellungen", + "pages.show.preview": "Seite öffnen", + "pages.show.template": "Vorlage", + "pages.show.changeurl": "URL ändern", + "pages.show.invisible": "Status: unsichtbar", + "pages.show.visible": "Status: sichtbar", + "pages.show.changes.text": "Du hast ungespeicherte Änderungen!", + "pages.show.changes.button": "Verwerfen", + "pages.show.delete": "Seite löschen", + "pages.show.subpages.title": "Seiten", + "pages.show.subpages.edit": "Bearbeiten", + "pages.show.subpages.add": "Hinzufügen", + "pages.show.subpages.empty": "Diese Seite hat keine Unterseiten", + "pages.show.files.title": "Dateien", + "pages.show.files.edit": "Bearbeiten", + "pages.show.files.add": "Hinzufügen", + "pages.show.files.empty": "Diese Seite hat keine Dateien", + "pages.show.error.permissions.title": "Die Seite ist nicht beschreibbar", + "pages.show.error.permissions.text": "Bitte überprüfe die Schreibrechte für /content und alle Inhalte", + "pages.show.error.permissions.retry": "Wiederholen", + "pages.show.error.notitle.title": "Das Blueprint hat kein Titelfeld", + "pages.show.error.notitle.text": "Bitte füge ein Titelfeld ein und versuche es erneut", + "pages.show.error.notitle.retry": "Wiederholen", + "pages.show.error.form": "Bitte fülle alle Felder vollständig und korrekt aus", + "pages.add.title.label": "Eine neue Seite hinzufügen", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-Anhang", + "pages.add.url.enter": "(URL eingeben)", + "pages.add.url.close": "Schließen", + "pages.add.url.help": "Format: Kleinbuchstaben a-z, 0-9 und Bindestriche", + "pages.add.template.label": "Vorlage", + "pages.add.error.create": "Die Seite konnte nicht hinzugefügt werden", + "pages.add.error.title": "Der Titel fehlt", + "pages.add.error.template": "Die Vorlage fehlt", + "pages.add.error.max.headline": "Keine weiteren Unterseiten zugelassen", + "pages.add.error.max.text": "Die maximale Anzahl an Unterseiten für die aktuelle Seite ist erreicht.", + "pages.url.uid.label": "URL-Anhang", + "pages.url.uid.label.option": "Aus Titel erzeugen", + "pages.url.error.exists": "Eine Seite mit dem selben Anhang besteht bereits.", + "pages.url.error.move": "Die URL konnte nicht geändert werden", + "pages.url.error.rights": "Du kannst die URL dieser Seite nicht ändern", + "pages.template.select.label": "Vorlage", + "pages.template.warning.text": "Folgende Felder werden sich ändern, wenn du die Vorlage wechselst. ", + "pages.template.warning.removed": "Gelöschte Felder", + "pages.template.warning.replaced": "Ersetzte Felder", + "pages.template.warning.added": "Hinzugefügte Felder", + "pages.template.error": "Die Vorlage für diese Seite kann nicht geändert werden", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "unsichtbar", + "pages.toggle.publish": "Willst du den Status der Seite wirklich in **sichtbar** umändern?", + "pages.toggle.hide": "Willst du den Status der Seite wirklich in **unsichtbar** umändern?", + "pages.toggle.error.error": "Der Status der Fehlerseite kann nicht geändert werden", + "pages.delete.headline": "Willst du diese Seite wirklich löschen?", + "pages.delete.error.home.headline": "Die Startseite kann nicht gelöscht werden", + "pages.delete.error.home.text": "Du versuchst die Startseite zu löschen. Das ist nicht möglich und würde zu ungewollten Fehlern führen.", + "pages.delete.error.error.headline": "Die Fehlerseite kann nicht gelöscht werden", + "pages.delete.error.error.text": "Du versuchst die Fehlerseite zu löschen. Das ist nicht möglich und würde zu ungewollten Fehlern führen.", + "pages.delete.error.children.headline": "Die Seite kann nicht gelöscht werden", + "pages.delete.error.children.text": "Die Seite hat Unterseiten und kann daher nicht gelöscht werden. Bitte entferne zuerst alle Unterseiten.", + "pages.delete.error.blocked.headline": "Die Seite kann nicht gelöscht werden", + "pages.delete.error.blocked.text": "Die Seite ist blockiert und kann daher nicht gelöscht werden.", + "pages.search.help": "Durchsuche alle Seiten nach URL-Pfad. Du kannst dich durch Ergebnisse mit den Pfeiltasten bewegen und per Enter zur ausgewählten Seite springen.", + "pages.search.noresults": "Es gibt leider keine Seiten zu deiner Suche. Bitte versuche es mit einem anderen Pfad.", + "pages.error.missing": "Die Seite konnte nicht gefunden werden", + "subpages": "Seiten", + "subpages.index.headline": "Seiten in", + "subpages.index.back": "Zurück", + "subpages.index.add": "Neue Seite anlegen", + "subpages.index.add.first.text": "Diese Seite hat noch keine Unterseiten", + "subpages.index.add.first.button": "Lege die erste Seite an", + "subpages.index.visible": "Sichtbare Seiten", + "subpages.index.visible.help": "Ziehe unsichtbare Seiten hierher, um sie zu sortieren/sichtbar zu machen", + "subpages.index.invisible": "Unsichtbare Seiten", + "subpages.index.invisible.help": "Ziehe sichtbare Seiten hierher, um sie unsichtbar zu machen", + "subpages.add.error": "Diese Seite darf keine Unterseiten haben", + "subpages.add.error.more": "Diese Seite kann keine weiteren Unterseiten haben", + "subpages.error.missing": "Die Seite konnte nicht gefunden werden.", + "files": "Dateien", + "files.index.headline": "Dateien für", + "files.index.back": "Zurück", + "files.index.upload": "Neue Datei hochladen", + "files.index.upload.first.text": "Diese Seite hat noch keine Dateien", + "files.index.upload.first.button": "Lade die erste Datei hoch", + "files.index.edit": "Bearbeiten", + "files.index.delete": "Löschen", + "files.index.error.disabled": "Diese Seite darf keine Dateien haben", + "files.add.error.max": "Die maximale Anzahl an Dateien für die aktuelle Seite ist erreicht.", + "files.add.error.extension.missing": "Du kannst keine Dateien ohne Dateiendung hochladen", + "files.add.error.extension.forbidden": "Verbotene Dateiendung", + "files.add.error.mime.forbidden": "Verbotener MIME-Typ", + "files.add.error.htaccess": "htaccess-Dateien können nicht hochgeladen werden", + "files.add.error.invisible": "Versteckte Dateien können nicht hochgeladen werden", + "files.add.blueprint.type.error": "Seite erlaubt nur:", + "files.add.blueprint.size.error": "Seite erlaubt nur eine Dateigröße von", + "files.show.name.label": "Dateiname", + "files.show.info.label": "Typ / Größe / Abmessungen", + "files.show.link.label": "Öffentlicher Link", + "files.show.open": "Anzeigen/Download", + "files.show.back": "Zurück", + "files.show.replace": "Ersetzen", + "files.show.delete": "Löschen", + "files.show.error.rename": "Die Datei konnte nicht umbenannt werden", + "files.show.error.form": "Bitte fülle alle Felder vollständig aus", + "files.upload.drop": "Ziehe Dateien hierher…", + "files.upload.click": "…oder klicke, um Dateien hochzuladen", + "files.replace.drop": "Ziehe eine Datei hierher…", + "files.replace.click": "…oder klicke, um die Datei zu ersetzen", + "files.replace.error.type": "Die hochgeladene Datei muss den selben Dateityp haben", + "files.delete.headline": "Willst du diese Datei wirklich löschen?", + "files.error.missing.page": "Die Seite konnte nicht gefunden werden", + "files.error.missing.file": "Die Datei konnte nicht gefunden werden", + "users": "Benutzer", + "users.index.headline": "Alle Benutzer", + "users.index.add": "Neuen Benutzer anlegen", + "users.index.edit": "Bearbeiten", + "users.index.delete": "Löschen", + "users.form.username.label": "Benutzername", + "users.form.username.placeholder": "Dein Benutzername", + "users.form.username.help": "Format: Kleinbuchstaben a-z, 0-9 und Bindestriche", + "users.form.username.readonly": "Der Benutzername kann nicht geändert werden", + "users.form.firstname.label": "Vorname", + "users.form.lastname.label": "Nachname", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@beispiel.de", + "users.form.password.label": "Passwort", + "users.form.password.confirm.label": "Passwort bestätigen", + "users.form.password.new.label": "Neues Passwort", + "users.form.password.new.confirm.label": "Neues Passwort bestätigen", + "users.form.password.new.help": "Leer lassen, um das aktuelle Passwort zu behalten", + "users.form.language.label": "Sprache", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Accounteinstellungen", + "users.form.options.message": "Email schicken", + "users.form.options.delete": "Account löschen", + "users.form.avatar.headline": "Profilbild", + "users.form.avatar.upload": "Profilbild hochladen", + "users.form.avatar.replace": "Profilbild ersetzen", + "users.form.avatar.delete": "Profilbild löschen", + "users.form.back": "Zurück zur Benutzerübersicht", + "users.form.error.password.confirm": "Bitte bestätige das Passwort", + "users.form.error.update": "Der Benutzer konnte nicht gespeichert werden", + "users.form.error.update.rights": "Du darfst diesen Benutzer nicht aktualisieren", + "users.form.error.create": "Der Benutzer konnte nicht erstellt werden", + "users.form.error.permissions.title": "Der accounts Ordner ist nicht beschreibbar", + "users.form.error.permissions.text": "Bitte stelle sicher, dass /site/accounts besteht und beschreibbar ist.", + "users.delete.headline": "Willst du diesen Benutzer wirklich löschen?", + "users.delete.error": "Der Benutzer konnte nicht gelöscht werden", + "users.delete.error.permission": "Du darfst keine Benutzer löschen", + "users.delete.error.permission.single": "Du darfst diesen Benutzer nicht löschen", + "users.delete.error.lastadmin": "Du kannst den letzten Admin nicht löschen", + "users.avatar.drop": "Ziehe ein Profilbild hierher…", + "users.avatar.click": "…oder klicke, um ein Profilbild hochzuladen", + "users.avatar.error.type": "Es sind nur JPG, PNG und GIF Dateien erlaubt.", + "users.avatar.error.folder.headline": "Der Profilbild Ordner ist nicht beschreibbar", + "users.avatar.error.folder.text": "Bitte erstelle den Ordner /assets/avatars und stelle sicher, dass er beschreibbar ist.", + "users.avatar.error.permission": "Du darfst den Avatar nicht verändern", + "users.avatar.delete.error": "Das Profilbild konnte nicht gelöscht werden", + "users.avatar.delete.error.permission": "Du darfst den Avater dieses Users nicht löschen", + "users.avatar.delete.success": "Das Profilbild wurde gelöscht", + "users.avatar.missing": "Der Benutzer hat kein Profilbild", + "users.error.missing": "Der Benutzer wurde nicht gefunden", + "user.error.lastadmin": "Du bist der letzte Admin. Das kann nicht verändert werden.", + "form.error.missing": "Das Formular kann nicht gefunden werden", + "form.construct.error.invalid": "Ungültiger Formularkonstruktor", + "fields.required": "Pflichtfeld", + "fields.date.label": "Datum", + "fields.date.months": [ + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember" + ], + "fields.date.weekdays": [ + "Sonntag", + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag" + ], + "fields.date.weekdays.short": [ + "So", + "Mo", + "Di", + "Mi", + "Do", + "Fr", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@beispiel.de", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Seite", + "fields.page.placeholder": "pfad/zur/seite", + "fields.password.label": "Passwort", + "fields.structure.add": "Hinzufügen", + "fields.structure.add.first": "Füge den ersten Eintrag hinzu", + "fields.structure.empty": "Es bestehen keine Einträge.", + "fields.structure.entry.error": "Der Eintrag konnte nicht gefunden werden", + "fields.structure.cancel": "Abbrechen", + "fields.structure.save": "Ok", + "fields.structure.edit": "Bearbeiten", + "fields.structure.delete": "Löschen", + "fields.structure.delete.label": "Willst du diesen Eintrag wirklich löschen?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fetter Text", + "fields.textarea.buttons.bold.text": "Fetter Text", + "fields.textarea.buttons.italic.label": "Kursiver Text", + "fields.textarea.buttons.italic.text": "Kursiver Text", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Bild", + "fields.textarea.buttons.file.label": "Datei", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nein", + "fields.toggle.on": "An", + "fields.toggle.off": "Aus", + "fields.error.missing.controller": "Die Feldcontroller-Datei fehlt", + "fields.error.missing.class": "Die Feldcontroller-Klasse fehlt", + "fields.error.route.invalid": "Ungültige Feld-Route", + "fields.error.extended": "Das Feld kann nicht erweitert werden", + "editor.link.url.label": "URL einfügen", + "editor.link.text.label": "Linktext", + "editor.link.text.help": "Der Linktext ist optional", + "editor.email.address.label": "Email Adresse einfügen", + "editor.email.address.placeholder": "mail@beispiel.de", + "editor.email.text.label": "Linktext", + "editor.email.text.help": "Der Linktext ist optional", + "editor.file.empty": "Diese Seite hat keine Dateien", + "editor.image.empty": "Diese Seite hat keine Bilder", + "autocomplete.method.error": "Ungültige Autocomplete-Methode", + "blueprints.error.default.missing": "Standard-Blueprint fehlt", + "error": "Fehler", + "error.headline": "Fehler" +} \ No newline at end of file diff --git a/panel/app/translations/de/package.json b/panel/app/translations/de/package.json new file mode 100644 index 0000000..dd2707c --- /dev/null +++ b/panel/app/translations/de/package.json @@ -0,0 +1,4 @@ +{ + "title": "Deutsch", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/en/core.json b/panel/app/translations/en/core.json new file mode 100644 index 0000000..23e5414 --- /dev/null +++ b/panel/app/translations/en/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancel", + "add": "Add", + "addit": "Add & Edit", + "save": "Save", + "saved": "Saved!", + "change": "Change", + "delete": "Delete", + "insert": "Insert", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Show options", + "options.hide": "Hide options", + "installation": "Installation", + "installation.check.headline": "Kirby Panel Installation", + "installation.check.text": "Kirby encountered the following issues during installation…", + "installation.check.retry": "Retry", + "installation.check.error": "There are some issues!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts is not writable", + "installation.check.error.avatars": "/assets/avatars is not writable", + "installation.check.error.blueprints": "Please add a /site/blueprints folder", + "installation.check.error.content": "The content folder and all contained files and folders must be writable.", + "installation.check.error.thumbs": "The thumbs folder must be writable.", + "installation.signup.username.label": "Create your first account", + "installation.signup.username.placeholder": "Username", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Password", + "installation.signup.language.label": "Language", + "installation.signup.button": "Create your account", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Username", + "login.password.label": "Password", + "login.error": "Invalid username or password", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pages", + "dashboard.index.pages.edit": "Edit", + "dashboard.index.pages.add": "Add", + "dashboard.index.site.title": "Your site's URL", + "dashboard.index.account.title": "Your account", + "dashboard.index.account.edit": "Edit", + "dashboard.index.metatags.title": "Site options", + "dashboard.index.metatags.edit": "Edit", + "dashboard.index.history.title": "Your last updates", + "dashboard.index.history.text": "Your last modified pages will be displayed here to make it easy to find them again later.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Site options", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Back to the dashboard", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Page settings", + "pages.show.preview": "Open preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Change URL", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Delete this page", + "pages.show.subpages.title": "Pages", + "pages.show.subpages.edit": "Edit", + "pages.show.subpages.add": "Add", + "pages.show.subpages.empty": "This page has no subpages", + "pages.show.files.title": "Files", + "pages.show.files.edit": "Edit", + "pages.show.files.add": "Add", + "pages.show.files.empty": "This page has no files", + "pages.show.error.permissions.title": "The page is not writable", + "pages.show.error.permissions.text": "Please check the permissions for the content folder and all files.", + "pages.show.error.permissions.retry": "Retry", + "pages.show.error.notitle.title": "The blueprint does not have a title field", + "pages.show.error.notitle.text": "Please add a title field and try again", + "pages.show.error.notitle.retry": "Retry", + "pages.show.error.form": "Please fill in all fields correctly", + "pages.add.title.label": "Add a new page", + "pages.add.title.placeholder": "Title", + "pages.add.url.label": "URL-appendix", + "pages.add.url.enter": "(enter your title)", + "pages.add.url.close": "Close", + "pages.add.url.help": "Format: lowercase a-z, 0-9 and regular dashes", + "pages.add.template.label": "Template", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "The title is missing", + "pages.add.error.template": "The template is missing", + "pages.add.error.max.headline": "No new pages allowed", + "pages.add.error.max.text": "The maximum number of subpages for the current page has been reached.", + "pages.url.uid.label": "URL-appendix", + "pages.url.uid.label.option": "Create from title", + "pages.url.error.exists": "A page with the same appendix already exists", + "pages.url.error.move": "The appendix could not be changed", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label" : "Template", + "pages.template.warning.text" : "The following fields will change, when you switch the template", + "pages.template.warning.removed" : "Removed fields", + "pages.template.warning.replaced" : "Replaced fields", + "pages.template.warning.added" : "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Do you really want to delete this page?", + "pages.delete.error.home.headline": "The home page cannot be deleted", + "pages.delete.error.home.text": "You are trying to delete the home page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.error.headline": "The error page cannot be deleted", + "pages.delete.error.error.text": "You are trying to delete the error page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.children.headline": "The page cannot be deleted", + "pages.delete.error.children.text": "This page has subpages and cannot be deleted. Please delete all subpages first.", + "pages.delete.error.blocked.headline": "The page cannot be deleted", + "pages.delete.error.blocked.text": "This page is locked and cannot be deleted.", + "pages.search.help": "Search pages by URL. Navigate through search results with your up and down arrow keys and hit enter to jump to the selected page.", + "pages.search.noresults": "There are no search results for your query. Please try again with a different URL.", + "pages.error.missing": "The page could not be found", + "subpages": "Pages", + "subpages.index.headline": "Pages in", + "subpages.index.back": "Back", + "subpages.index.add": "Add a new page", + "subpages.index.add.first.text": "This page has no subpages yet", + "subpages.index.add.first.button": "Add the first page", + "subpages.index.visible": "Visible pages", + "subpages.index.visible.help": "Drag invisible pages here to sort them/make them visible.", + "subpages.index.invisible": "Invisible pages", + "subpages.index.invisible.help": "Drag visible pages here to unsort them/make them invisible.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "The page could not be found", + "files": "Files", + "files.index.headline": "Files for", + "files.index.back": "Back", + "files.index.upload": "Upload a new file", + "files.index.upload.first.text": "This page has no files yet", + "files.index.upload.first.button": "Upload the first file", + "files.index.edit": "Edit", + "files.index.delete": "Delete", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Filename", + "files.show.info.label": "Type / Size / Dimensions", + "files.show.link.label": "Public link", + "files.show.open": "Show/download file", + "files.show.back": "Back", + "files.show.replace": "Replace", + "files.show.delete": "Delete", + "files.show.error.rename": "The file could not be renamed", + "files.show.error.form": "Please fill in all fields correctly", + "files.upload.drop": "Drop files here…", + "files.upload.click": "…or click to upload", + "files.replace.drop": "Drop a file here…", + "files.replace.click": "…or click to replace", + "files.replace.error.type": "The uploaded file must have the same file type", + "files.delete.headline": "Do you really want to delete this file?", + "files.error.missing.page": "The page could not be found", + "files.error.missing.file": "The file could not be found", + "users": "Users", + "users.index.headline": "All users", + "users.index.add": "Add a new user", + "users.index.edit": "Edit", + "users.index.delete": "Delete", + "users.form.username.label": "Username", + "users.form.username.placeholder": "Your username", + "users.form.username.help": "Allowed characters: lowercase a-z, 0-9 and dashes", + "users.form.username.readonly": "The username cannot be changed", + "users.form.firstname.label": "First name", + "users.form.lastname.label": "Last name", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Password", + "users.form.password.confirm.label": "Confirm password", + "users.form.password.new.label": "New password", + "users.form.password.new.confirm.label": "Confirm the new password", + "users.form.password.new.help": "Leave blank to keep the current password", + "users.form.language.label": "Language", + "users.form.role.label": "Role", + "users.form.options.headline": "Account options", + "users.form.options.message": "Send email", + "users.form.options.delete": "Delete account", + "users.form.avatar.headline": "Profile picture", + "users.form.avatar.upload": "Upload profile picture", + "users.form.avatar.replace": "Replace profile picture", + "users.form.avatar.delete": "Delete profile picture", + "users.form.back": "Back to users", + "users.form.error.password.confirm": "Please confirm the password", + "users.form.error.update": "The user could not be updated", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "The user could not be created", + "users.form.error.permissions.title": "The account folder is not writable", + "users.form.error.permissions.text": "Please make sure that /site/accounts exists and is writable.", + "users.delete.headline": "Do you really want to delete this user?", + "users.delete.error": "The user could not be deleted", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Drop a profile picture here…", + "users.avatar.click": "…or click to upload", + "users.avatar.error.type": "You can only upload JPG, PNG and GIF files", + "users.avatar.error.folder.headline": "The avatar folder is not writable", + "users.avatar.error.folder.text": "Please create the folder /assets/avatars and make it writable to upload profile pictures.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "The profile picture could not be deleted", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "The profile picture has been deleted", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "The user could not be found", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Required", + "fields.date.label": "Date", + "fields.date.months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "fields.date.weekdays": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "fields.date.weekdays.short": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Number", + "fields.number.placeholder": "#", + "fields.page.label": "Page", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "Password", + "fields.structure.add": "Add", + "fields.structure.add.first": "Add the first entry", + "fields.structure.empty": "No entries yet.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Cancel", + "fields.structure.save": "Ok", + "fields.structure.edit": "Edit", + "fields.structure.delete": "Delete", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Tags", + "fields.tel.label": "Phone", + "fields.textarea.buttons.bold.label": "Bold text", + "fields.textarea.buttons.bold.text": "Bold text", + "fields.textarea.buttons.italic.label": "Italic text", + "fields.textarea.buttons.italic.text": "Italic text", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Image", + "fields.textarea.buttons.file.label": "File", + "fields.toggle.yes": "Yes", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Insert URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "The link text is optional", + "editor.email.address.label": "Insert email address", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "The link text is optional", + "editor.file.empty": "This page has no files", + "editor.image.empty": "This page has no images", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Error", + "error.headline": "Error" +} diff --git a/panel/app/translations/en/package.json b/panel/app/translations/en/package.json new file mode 100644 index 0000000..fd2592d --- /dev/null +++ b/panel/app/translations/en/package.json @@ -0,0 +1,4 @@ +{ + "title": "English", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/es_419/core.json b/panel/app/translations/es_419/core.json new file mode 100644 index 0000000..e0d11ff --- /dev/null +++ b/panel/app/translations/es_419/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Agregar", + "addit": "Agregar y Editar", + "save": "Guardar", + "saved": "¡Guardado!", + "change": "Cambiar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "OK", + "routes.error.invalid": "URL del Panel no válida", + "controller.error.invalid": "Controlador no válido", + "controller.error.action": "Acción no válida", + "view.error.invalid": "Vista no válida:", + "options.show": "Mostrar opciones", + "options.hide": "Ocultar opciones", + "installation": "Instalación", + "installation.check.headline": "Instalación del Panel de Kirby", + "installation.check.text": "Kirby encontró los siguientes errores durante la instalación…", + "installation.check.retry": "Reintentar", + "installation.check.error": "¡Tenemos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no tine permisos de escritura", + "installation.check.error.avatars": "/assets/avatars no tine permisos de escritura", + "installation.check.error.blueprints": "Por favor agrega una carpeta /site/blueprints", + "installation.check.error.content": "La carpeta \"contenido\" y todos sus archivos y subcarpetas deben de tener permiso de escritura.", + "installation.check.error.thumbs": "La carpeta \"thumbs\" debe tener permisos de escritura.", + "installation.signup.username.label": "Crea tu primera cuenta", + "installation.signup.username.placeholder": "Usuario", + "installation.signup.email.label": "Correo electrónico", + "installation.signup.email.placeholder": "correo@ejemplo.com", + "installation.signup.password.label": "Contraseña", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crear tu cuenta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Usuario", + "login.password.label": "Contraseña", + "login.error": "Usuario o Contraseña equivocada", + "login.button": "Log in", + "login.log.error.permissions": "El archivo de registro del inicio de sesión no es modificable", + "logout": "Log out", + "topbar.error.class.definition": "Falta la definición topbar para la clase:", + "dashboard": "Inicio", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Agregar", + "dashboard.index.site.title": "URL de tu sitio", + "dashboard.index.account.title": "Tu cuenta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variables del sitio", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Actualizaciones recientes", + "dashboard.index.history.text": "Las últimas páginas modificadas serán desplegadas aquí para facilitar su acceso en el futuro.", + "dashboard.index.license.title": "Licencia Kirby", + "dashboard.index.license.text": "¡Parece que estás ejecutando Kirby en un servidor público sin una licencia válida!\n\nPor favor, apoya a Kirby y (link: {buy} text: compra una licencia)\n\nSi ya has adquirido una licencia, sólo agrégala a tu archivo de configuración: (link: {docs} text: site/config/config.php)", + "metatags": "Variables del sitio", + "metatags.info": "Información de Kirby", + "metatags.license": "Licencia Kirby", + "metatags.version.toolkit": "Versión del Toolkit", + "metatags.version.kirby": "Versión de Kirby", + "metatags.version.panel": "Versión del Panel", + "metatags.back": "Regresar al Inicio", + "metatags.files": "Archivos del sitio", + "site.delete.error": "El sitio no puede ser borrado", + "pages.show.settings": "Opciones de Página", + "pages.show.preview": "Abrir previsualización", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Cambiar URL", + "pages.show.invisible": "Estatus: invisible", + "pages.show.visible": "Estatus: visible", + "pages.show.changes.text": "¡Tienes cambios sin guardar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Agregar", + "pages.show.subpages.empty": "Esta página no posee subpáginas", + "pages.show.files.title": "Archivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Agregar", + "pages.show.files.empty": "Esta página no contiene archivos", + "pages.show.error.permissions.title": "Esta página no es modificable", + "pages.show.error.permissions.text": "Por favor revisa los permisos para la carpeta de contenido y todos los archivos.", + "pages.show.error.permissions.retry": "Reintentar", + "pages.show.error.notitle.title": "El blueprint no posee un campo de título", + "pages.show.error.notitle.text": "Por favor agrega un campo de título e intenta de nuevo", + "pages.show.error.notitle.retry": "Reintentar", + "pages.show.error.form": "Por favor llena todos los campos correctamente", + "pages.add.title.label": "Agregar nueva página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "Apéndice-URL", + "pages.add.url.enter": "(ingresa un título)", + "pages.add.url.close": "Cerrar", + "pages.add.url.help": "Formato: minúsculas a-z, 0-9 y guiones regulares", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "La página no pudo ser eliminada", + "pages.add.error.title": "Falta un título", + "pages.add.error.template": "Falta una plantilla", + "pages.add.error.max.headline": "No se permiten páginas nuevas", + "pages.add.error.max.text": "El número máximo de subpáginas para la página actual se ha alcanzado.", + "pages.url.uid.label": "Apéndice-URL", + "pages.url.uid.label.option": "Crear a partir del título", + "pages.url.error.exists": "Una página con el mismo apéndice-url ya existe", + "pages.url.error.move": "El apéndice-url no pudo ser cambiado", + "pages.url.error.rights": "Usted no puede cambiar la URL de esta página", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posición", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "¿En realidad deseas cambiar el estatus de esta página a **visible?**", + "pages.toggle.hide": "¿En realidad deseas cambiar el estatus de esta página a **invisible?**", + "pages.toggle.error.error": "El estado de la pagina de error no puede ser cambiado", + "pages.delete.headline": "¿Estás seguro que deseas eliminar esta página?", + "pages.delete.error.home.headline": "La página \"Home\" no puede ser eliminada", + "pages.delete.error.home.text": "Estás intentando eliminar la página \"Home\". Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.error.headline": "La página \"Error\" no puede ser eliminada", + "pages.delete.error.error.text": "Estás intentando eliminar la página \"Error\". Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.children.headline": "Esta página no puede ser eliminada", + "pages.delete.error.children.text": "La página contiene subpáginas y no puede ser eliminada. Por favor elimina todas las subpáginas primero.", + "pages.delete.error.blocked.headline": "Esta página no puede ser eliminada", + "pages.delete.error.blocked.text": "La página esta bloqueada y no puede ser eliminada.", + "pages.search.help": "Buscar páginas por URL. Navega por los resultados de búsqueda con las flechas arriba y abajo y presiona enter para ir a la página seleccionada.", + "pages.search.noresults": "No hay resultados de búsqueda. Por favor intenta con un URL diferente.", + "pages.error.missing": "La página no fue encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas dentro de", + "subpages.index.back": "Regresar", + "subpages.index.add": "Agregar nueva página", + "subpages.index.add.first.text": "Esta página aún no tiene subpáginas", + "subpages.index.add.first.button": "Agrega la primera página", + "subpages.index.visible": "Páginas visibles", + "subpages.index.visible.help": "Arrastra las páginas invisibles aquí para ordenar y hacerlas visibles.", + "subpages.index.invisible": "Páginas invisibles", + "subpages.index.invisible.help": "Arrastra las páginas visibles aquí para eliminar el numero de orden y hacerlas invisibles.", + "subpages.add.error": "Esta página no permite tener subpáginas", + "subpages.add.error.more": "Esta página ya no puede tener más subpáginas", + "subpages.error.missing": "La página no pudo ser encontrada", + "files": "Archivos", + "files.index.headline": "Archivos de", + "files.index.back": "Regresar", + "files.index.upload": "Subir nuevo archivo", + "files.index.upload.first.text": "Esta página aún no contiene archivos", + "files.index.upload.first.button": "Subir el primer archivo", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "Esta página no permite tener archivos", + "files.add.error.max": "El máximo número de archivos para la página actual ya han sido alcanzados", + "files.add.error.extension.missing": "Usted no puede subir archivos sin extensión", + "files.add.error.extension.forbidden": "Extensión de archivo prohibida", + "files.add.error.mime.forbidden": "Tipo mime prohibido", + "files.add.error.htaccess": "Los archivos htaccess no pueden ser subidos", + "files.add.error.invisible": "Los archivos invisibles no pueden ser subidos", + "files.add.blueprint.type.error": "La página únicamente permite:", + "files.add.blueprint.size.error": "La página únicamente permite archivos de tamaño", + "files.show.name.label": "Nombre", + "files.show.info.label": "Tipo / Tamaño / Dimensiones", + "files.show.link.label": "Enlace público", + "files.show.open": "Mostrar/descargar archivo", + "files.show.back": "Regresar", + "files.show.replace": "Reemplazar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "El archivo no pudo ser renombrado", + "files.show.error.form": "Por favor llena todos los campos correctamente", + "files.upload.drop": "Arrastra y suelta los archivos aquí…", + "files.upload.click": "…o haz click para subir", + "files.replace.drop": "Arrastra y suelta un archivo aquí…", + "files.replace.click": "…o haz click para reemplazar", + "files.replace.error.type": "El archivo subido debe ser del mismo tipo", + "files.delete.headline": "¿Estás seguro que deseas eliminar este archivo?", + "files.error.missing.page": "La página no pudo ser encontrada", + "files.error.missing.file": "El archivo no pudo ser encontrado", + "users": "Usuarios", + "users.index.headline": "Todos los usuarios", + "users.index.add": "Agregar un nuevo usuario", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Usuario", + "users.form.username.placeholder": "Tu nombre de usuario", + "users.form.username.help": "Caracteres permitidos: minúsculas a-z, 0-9 y guiones regulares", + "users.form.username.readonly": "El nombre de usuario no puede ser cambiado", + "users.form.firstname.label": "Nombre", + "users.form.lastname.label": "Apellido", + "users.form.email.label": "Email", + "users.form.email.placeholder": "correo@ejemplo.com", + "users.form.password.label": "Contraseña", + "users.form.password.confirm.label": "Confirmar contraseña", + "users.form.password.new.label": "Nueva contraseña", + "users.form.password.new.confirm.label": "Confirmar la nueva contraseña", + "users.form.password.new.help": "Dejar en blanco para mantener la misma contraseña", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opciones de cuenta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Eliminar cuenta", + "users.form.avatar.headline": "Foto de perfil", + "users.form.avatar.upload": "Subir foto de perfil", + "users.form.avatar.replace": "Reemplazar foto de perfil", + "users.form.avatar.delete": "Eliminar foto de perfil", + "users.form.back": "Regresar a usuarios", + "users.form.error.password.confirm": "Por favor confirma la contraseña", + "users.form.error.update": "El usuario no pudo ser actualizado", + "users.form.error.update.rights": "Usted no tiene permitido actualizar este usuario", + "users.form.error.create": "El usuario no pudo ser creado", + "users.form.error.permissions.title": "La carpeta de la cuenta no es modificable", + "users.form.error.permissions.text": "Por favor asegúrate de que la carpeta \"/site/accounts\" exista y sea modificable.", + "users.delete.headline": "¿Estás seguro que deseas eliminar este usuario?", + "users.delete.error": "El ususario no pudo ser eliminado", + "users.delete.error.permission": "Usted no tiene permitido borrar usuarios", + "users.delete.error.permission.single": "Usted no tiene permitido borrar este usuario", + "users.delete.error.lastadmin": "Usted no puede borrar el último administrador", + "users.avatar.drop": "Arrastra y suelta una imagen aquí…", + "users.avatar.click": "…o haz click para subir", + "users.avatar.error.type": "Sólo se pueden subir archivos con extensión JPG, PNG y GIF", + "users.avatar.error.folder.headline": "La carpeta \"avatar\" no es modificable", + "users.avatar.error.folder.text": "Por favor crea la carpeta /assets/avatars y hazla modificable para subir fotos de perfil.", + "users.avatar.error.permission": "Usted no tiene permitido cambiar el avatar", + "users.avatar.delete.error": "La foto de perfil no pudo ser eliminada", + "users.avatar.delete.error.permission": "Usted no tiene permitido borrar el avatar de este usuario", + "users.avatar.delete.success": "La foto de perfil ha sido eliminada", + "users.avatar.missing": "Este usuario no tiene avatar", + "users.error.missing": "El usuario no pudo ser encontrado", + "user.error.lastadmin": "Usted es el único administrador, Esto no puede ser cambiado", + "form.error.missing": "No se pudo encontrar el formulario", + "form.construct.error.invalid": "El método de construcción del formulario es inválido", + "fields.required": "Requerido", + "fields.date.label": "Fecha", + "fields.date.months": [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre" + ], + "fields.date.weekdays": [ + "Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "correo@ejemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "ruta/a/página", + "fields.password.label": "Contraseña", + "fields.structure.add": "Agregar", + "fields.structure.add.first": "Agregar la primera entrada", + "fields.structure.empty": "Aún no existen entradas.", + "fields.structure.entry.error": "No se pudo encontrar el item", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Guardar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "¿En realidad desea borrar esta entrada?", + "fields.tags.label": "Etiquetas", + "fields.tel.label": "Teléfono", + "fields.textarea.buttons.bold.label": "Texto en Negrita", + "fields.textarea.buttons.bold.text": "Texto en Negrita", + "fields.textarea.buttons.italic.label": "Texto en Itálicas", + "fields.textarea.buttons.italic.text": "Texto en Itálicas", + "fields.textarea.buttons.link.label": "Enlace", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imágen", + "fields.textarea.buttons.file.label": "Archivo", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "Encendido", + "fields.toggle.off": "Apagado", + "fields.error.missing.controller": "Falta el archivo del controlador del campo ", + "fields.error.missing.class": "Falta la clase del controlador del campo", + "fields.error.route.invalid": "La ruta del campo es inválida", + "fields.error.extended": "El campo no puede ser extendido", + "editor.link.url.label": "Insertar URL", + "editor.link.text.label": "Texto de Enlace", + "editor.link.text.help": "El texto de enlace es opcional", + "editor.email.address.label": "Insertar dirección email", + "editor.email.address.placeholder": "correo@ejemplo.com", + "editor.email.text.label": "Texto de Enlace", + "editor.email.text.help": "El texto de enlace es opcional", + "editor.file.empty": "Esta página no contiene archivos", + "editor.image.empty": "Esta página no contiene imágenes", + "autocomplete.method.error": "Método de autocompletar no válido", + "blueprints.error.default.missing": "Falta el blueprint predeterminado", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/es_419/package.json b/panel/app/translations/es_419/package.json new file mode 100644 index 0000000..5a9f078 --- /dev/null +++ b/panel/app/translations/es_419/package.json @@ -0,0 +1,4 @@ +{ + "title": "Español (América Latina)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/es_ES/core.json b/panel/app/translations/es_ES/core.json new file mode 100644 index 0000000..1019834 --- /dev/null +++ b/panel/app/translations/es_ES/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Añadir", + "addit": "Añadir y Editar", + "save": "Guardar", + "saved": "¡Guardado!", + "change": "Cambiar", + "delete": "Eliminar", + "insert": "Insertar", + "ok": "OK", + "routes.error.invalid": "La URL del Panel es inválida", + "controller.error.invalid": "Controlador inválido", + "controller.error.action": "Acción inválida", + "view.error.invalid": "Vista inválida", + "options.show": "Mostrar opciones", + "options.hide": "Ocultar opciones", + "installation": "Instalación", + "installation.check.headline": "Instalación de Kirby Panel", + "installation.check.text": "Kirby encontró los siguientes errores durante la instalación…", + "installation.check.retry": "Reintentar", + "installation.check.error": "¡Hay un problema!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts no tiene permisos de escritura", + "installation.check.error.avatars": "/assets/avatars no tiene permisos de escritura", + "installation.check.error.blueprints": "Por favor, crea la carpeta /site/blueprints", + "installation.check.error.content": "La carpeta de contenido y todos sus archivos y carpetas deben tener permisos de escritura.", + "installation.check.error.thumbs": "La carpeta /thumbs debe tener permisos de escritura.", + "installation.signup.username.label": "Crea tu primera cuenta", + "installation.signup.username.placeholder": "Nombre de usuario", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@ejemplo.com", + "installation.signup.password.label": "Contraseña", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crear cuenta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nombre de usuario", + "login.password.label": "Contraseña", + "login.error": "Nombre de usuario o contraseña incorrecto", + "login.button": "Log in", + "login.log.error.permissions": "El archivo de logs de inicios de sesión no tiene permisos de escritura", + "logout": "Log out", + "topbar.error.class.definition": "Falta la definición de topbar para la clase:", + "dashboard": "Tablero", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Añadir", + "dashboard.index.site.title": "URL de tu sitio", + "dashboard.index.account.title": "Tu cuenta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variables del sitio", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Tus últimas actualizaciones", + "dashboard.index.history.text": "Las páginas modificadas recientemente se mostrarán aquí para que puedas encontrarlas fácilmente.", + "dashboard.index.license.title": "Licencia de Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variables del sitio", + "metatags.info": "Información de Kirby", + "metatags.license": "Licencia de Kirby", + "metatags.version.toolkit": "Versión del Toolkit", + "metatags.version.kirby": "Versión de Kirby", + "metatags.version.panel": "Versión del Panel", + "metatags.back": "Volver al tablero", + "metatags.files": "Archivos del sitio", + "site.delete.error": "No se puede eliminar el sitio", + "pages.show.settings": "Ajustes de página", + "pages.show.preview": "Abrir previsualización", + "pages.show.template": "Plantilla", + "pages.show.changeurl": "Cambiar URL", + "pages.show.invisible": "Estado: Invisible", + "pages.show.visible": "Estado: Visible", + "pages.show.changes.text": "¡Tienes cambios sin guardar!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Eliminar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Añadir", + "pages.show.subpages.empty": "Esta página no tiene subpáginas", + "pages.show.files.title": "Archivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Añadir", + "pages.show.files.empty": "Esta página no tiene archivos", + "pages.show.error.permissions.title": "Esta página no tiene permisos de escritura", + "pages.show.error.permissions.text": "Por favor, revisa los permisos de la carpeta de contenido y todos sus archivos.", + "pages.show.error.permissions.retry": "Volver a intentar", + "pages.show.error.notitle.title": "El blueprint no tiene un campo título", + "pages.show.error.notitle.text": "Por favor, añade un título e inténtalo de nuevo", + "pages.show.error.notitle.retry": "Volver a intentar", + "pages.show.error.form": "Por favor, rellena todos los campos correctamente", + "pages.add.title.label": "Añadir una nueva página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL de la página", + "pages.add.url.enter": "(añadir título)", + "pages.add.url.close": "Cerrar", + "pages.add.url.help": "Formato: minúsculas a-z, 0-9 y guiones", + "pages.add.template.label": "Plantilla", + "pages.add.error.create": "No se ha podido crear la página", + "pages.add.error.title": "Falta el título", + "pages.add.error.template": "Falta la plantilla", + "pages.add.error.max.headline": "No se permite añadir nuevas páginas", + "pages.add.error.max.text": "Se ha alcanzado el máximo número de subpáginas para la página actual.", + "pages.url.uid.label": "URL de la página", + "pages.url.uid.label.option": "Crear a partir del título", + "pages.url.error.exists": "Ya existe una página con la misma URL", + "pages.url.error.move": "No se ha podido cambiar la URL", + "pages.url.error.rights": "No se puede cambiar la URL de esta página", + "pages.template.select.label": "Plantilla", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posición", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "¿Realmente quieres eliminar esta cambiar el estado de esta página a **visible?**", + "pages.toggle.hide": "¿Realmente quieres cambiar el estado de esta página a **invisible?**", + "pages.toggle.error.error": "No se puede cambiar el estado de la página de error", + "pages.delete.headline": "¿Realmente quieres eliminar esta página?", + "pages.delete.error.home.headline": "No se puede eliminar la página de inicio", + "pages.delete.error.home.text": "Estás intentando eliminar la página de inicio. Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.error.headline": "No se puede eliminar la página de error", + "pages.delete.error.error.text": "Estás intentando eliminar la página de error. Esto no es posible y podría causar efectos indeseados.", + "pages.delete.error.children.headline": "No se puede eliminar la página", + "pages.delete.error.children.text": "Esta página tiene subpáginas y no puede ser eliminada. Por favor, elimina primero las subpáginas.", + "pages.delete.error.blocked.headline": "No se puede eliminar la página", + "pages.delete.error.blocked.text": "Esta página está bloqueada y no puede ser eliminada.", + "pages.search.help": "Buscar páginas por URL. Navega a través de los resultados de la búsqueda mediante las flechas del teclado y pulsa Enter para ir a la página seleccionada.", + "pages.search.noresults": "No se han encontrado resultados para tu búsqueda. Por favor, inténtalo de nuevo con una URL distinta.", + "pages.error.missing": "No se ha encontrado la página", + "subpages": "Páginas", + "subpages.index.headline": "Páginas en", + "subpages.index.back": "Atrás", + "subpages.index.add": "Añadir una página nueva", + "subpages.index.add.first.text": "Esta página aún no tiene subpáginas", + "subpages.index.add.first.button": "Añade la primera página", + "subpages.index.visible": "Páginas visibles", + "subpages.index.visible.help": "Arrastra páginas invisibles aquí para ordenarlas/hacerlas visibles.", + "subpages.index.invisible": "Páginas invisibles", + "subpages.index.invisible.help": "Arrastra páginas visibles aquí para desordenarlas/hacerlas invisibles.", + "subpages.add.error": "Esta página no admite subpáginas", + "subpages.add.error.more": "Esta página no admite más sumpáginas", + "subpages.error.missing": "No se ha podido encontrar la página", + "files": "Archivos", + "files.index.headline": "Archivos para", + "files.index.back": "Atrás", + "files.index.upload": "Subir un archivo", + "files.index.upload.first.text": "Esta página no tiene archivos", + "files.index.upload.first.button": "Sube un primer archivo", + "files.index.edit": "Editar", + "files.index.delete": "Eliminar", + "files.index.error.disabled": "La página no admite archivos", + "files.add.error.max": "Se ha alcanzado el número máximo de archivos para esta página", + "files.add.error.extension.missing": "No se pueden subir archivos sin extensión", + "files.add.error.extension.forbidden": "Extensión de archivo prohibida", + "files.add.error.mime.forbidden": "mime type prohibido", + "files.add.error.htaccess": "No se pueden subir archivos htacces", + "files.add.error.invisible": "No se pueden subir archivos invisibles", + "files.add.blueprint.type.error": "La página únicamente admite:", + "files.add.blueprint.size.error": "La página únicamente admite tamaño de archivo de", + "files.show.name.label": "Nombre del archivo", + "files.show.info.label": "Tipo / Tamaño / Dimensiones", + "files.show.link.label": "Enlace público", + "files.show.open": "Mostrar/descargar archivo", + "files.show.back": "Atrás", + "files.show.replace": "Reemplazar", + "files.show.delete": "Eliminar", + "files.show.error.rename": "No se ha podido renombrar el archivo", + "files.show.error.form": "Por favor, rellena todos los campos correctamente", + "files.upload.drop": "Suelta archivos aquí…", + "files.upload.click": "…o haz clic para subir", + "files.replace.drop": "Suelta un archivo aquí……", + "files.replace.click": "…o haz clic para reemplazar", + "files.replace.error.type": "El archivo subido debe ser del mismo tipo", + "files.delete.headline": "¿Realmente quieres eliminar este archivo?", + "files.error.missing.page": "No se ha podido encontrar la página", + "files.error.missing.file": "No se ha podido encontrar el archivo", + "users": "Usuarios", + "users.index.headline": "Todos los usuarios", + "users.index.add": "Añadir usuario", + "users.index.edit": "Editar", + "users.index.delete": "Eliminar", + "users.form.username.label": "Nombre de usuario", + "users.form.username.placeholder": "Tu nombre de usuario", + "users.form.username.help": "Caracteres permitidos: minúsculas a-z, 0-9 y guiones", + "users.form.username.readonly": "No se puede cambiar el nombre de usuario", + "users.form.firstname.label": "Nombre", + "users.form.lastname.label": "Apellido", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@ejemplo.com", + "users.form.password.label": "Contraseña", + "users.form.password.confirm.label": "Confirmar contraseña", + "users.form.password.new.label": "Nueva contraseña", + "users.form.password.new.confirm.label": "Confirmar la nueva contraseña", + "users.form.password.new.help": "Dejar vacío para mantener la contraseña actual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opciones de cuenta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Eliminar cuenta", + "users.form.avatar.headline": "Foto de perfil", + "users.form.avatar.upload": "Subir una foto de perfil", + "users.form.avatar.replace": "Reemplazar la foto de perfil", + "users.form.avatar.delete": "Eliminar la foto de perfil", + "users.form.back": "Volver a Usuarios", + "users.form.error.password.confirm": "Por favor, confirma tu contraseña", + "users.form.error.update": "No se ha podido actualizar el usuario", + "users.form.error.update.rights": "No dispones de autorización para actualizar este usuario", + "users.form.error.create": "No se ha podido crear el usuario", + "users.form.error.permissions.title": "La carpeta de cuentas no tiene permisos de escritura", + "users.form.error.permissions.text": "Por favor, asegúrate de que /site/accounts existe y tiene permisos de escritura.", + "users.delete.headline": "¿Realmente quieres eliminar este usuario?", + "users.delete.error": "No se ha podido eliminar el usuario", + "users.delete.error.permission": "No dispones de autorización para eliminar usuarios", + "users.delete.error.permission.single": "No dispones de autorización para eliminar este usuario", + "users.delete.error.lastadmin": "No puedes eliminar al último admin", + "users.avatar.drop": "Suelta una foto de perfil aquí…", + "users.avatar.click": "…o haz clic para subir", + "users.avatar.error.type": "Sólo se pueden subir archivos JPG, PNG y GIF", + "users.avatar.error.folder.headline": "La carpeta de fotos de perfil no tiene permisos de escritura", + "users.avatar.error.folder.text": "Por favor, crea la carpeta /assets/avatars y asegúrate de que tiene permisos de escritura para poder subir fotos de perfil.", + "users.avatar.error.permission": "No dispones de autorización para cambiar el avatar", + "users.avatar.delete.error": "No se ha podido eliminar la foto de perfil", + "users.avatar.delete.error.permission": "No dispones de autorización para eliminar el avatar de este usuario", + "users.avatar.delete.success": "Se ha eliminado la foto de perfil", + "users.avatar.missing": "Este usuario no tiene avatar", + "users.error.missing": "Usuario no encontrado", + "user.error.lastadmin": "Eres el único administrador. Esto no se puede cambiar.", + "form.error.missing": "No se ha encontrado el formulario", + "form.construct.error.invalid": "Método de construcción de formulario inválido", + "fields.required": "Obligatorio", + "fields.date.label": "Fecha", + "fields.date.months": [ + "Enero", + "Febrero", + "Marzo", + "Abril", + "Mayo", + "Junio", + "Julio", + "Agosto", + "Septiembre", + "Octubre", + "Noviembre", + "Diciembre" + ], + "fields.date.weekdays": [ + "Domingo", + "Lunes", + "Martes", + "Miércoles", + "Jueves", + "Viernes", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Do", + "Lu", + "Ma", + "Mi", + "Ju", + "Vi", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@ejemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "ruta/a/pagina", + "fields.password.label": "Contraseña", + "fields.structure.add": "Añadir", + "fields.structure.add.first": "Añadir la primera entrada", + "fields.structure.empty": "Aún no hay entradas.", + "fields.structure.entry.error": "No se ha podido encontrar el item", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Guardar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Eliminar", + "fields.structure.delete.label": "¿Realmente quieres eliminar esta entrada?", + "fields.tags.label": "Etiquetas", + "fields.tel.label": "Teléfono", + "fields.textarea.buttons.bold.label": "Negrita", + "fields.textarea.buttons.bold.text": "Negrita", + "fields.textarea.buttons.italic.label": "Cursiva", + "fields.textarea.buttons.italic.text": "Cursiva", + "fields.textarea.buttons.link.label": "Enlace", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imágen", + "fields.textarea.buttons.file.label": "Archivo", + "fields.toggle.yes": "Sí", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "Falta el archivo del controlador de campos", + "fields.error.missing.class": "Falta la clase del controlador de campos", + "fields.error.route.invalid": "Ruta del campo inválida", + "fields.error.extended": "El campo no puede ser extendido", + "editor.link.url.label": "Insertar URL", + "editor.link.text.label": "Texto del enlace", + "editor.link.text.help": "El texto del enlace es opcional", + "editor.email.address.label": "Añadir dirección de email", + "editor.email.address.placeholder": "mail@ejemplo.com", + "editor.email.text.label": "Texto del enlace", + "editor.email.text.help": "El texto del enlace es opcional", + "editor.file.empty": "Esta página no tiene archivos", + "editor.image.empty": "Esta página no tiene imágenes", + "autocomplete.method.error": "Método de autocompletado inválido", + "blueprints.error.default.missing": "Falta el blueprint por defecto", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/es_ES/package.json b/panel/app/translations/es_ES/package.json new file mode 100644 index 0000000..16056dd --- /dev/null +++ b/panel/app/translations/es_ES/package.json @@ -0,0 +1,4 @@ +{ + "title": "Español (España)", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/fa/core.json b/panel/app/translations/fa/core.json new file mode 100644 index 0000000..18d24f9 --- /dev/null +++ b/panel/app/translations/fa/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "انصراف", + "add": "افزودن", + "addit": "اضافه کردن و ویرایش", + "save": "ذخیره", + "saved": "ذخیره شد!", + "change": "اصلاح", + "delete": "حذف", + "insert": "درج", + "ok": "تایید", + "routes.error.invalid": "آدرس پنل نامعتبر است", + "controller.error.invalid": "کنترلر نامعتبر است", + "controller.error.action": "اقدام نامعتبر", + "view.error.invalid": "ویو نامعتبر است", + "options.show": "نمایش گزینه ها", + "options.hide": "پنهان کردن گزینه ها", + "installation": "نصب و راه اندازی", + "installation.check.headline": "نصب و راه اندازی کربی پنل", + "installation.check.text": "کربی در هنگام نصب با موارد زیر روبرو شده است…", + "installation.check.retry": "تلاش مجدد", + "installation.check.error": "مشکلی رخ داده است!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts قابل نوشتن نیست", + "installation.check.error.avatars": "/assets/avatars قابل نوشتن نیست", + "installation.check.error.blueprints": "لطفا پوشه /site/blueprints را ایجاد کنید", + "installation.check.error.content": "پوشه content و همه فایل ها و پوشه های موجود باید قابل نوشتن باشد.", + "installation.check.error.thumbs": "پوشه thumbs باید قابل نوشتن باشد.", + "installation.signup.username.label": "ایجاد اولین حساب کاربری", + "installation.signup.username.placeholder": "نام کاربری", + "installation.signup.email.label": "پست الکترونیک", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "گذرواژه", + "installation.signup.language.label": "زبان", + "installation.signup.button": "ایجاد حساب کاربری", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "نام کاربری", + "login.password.label": "گذرواژه", + "login.error": "نام کاربری یا گذرواژه صحیح نیست", + "login.button": "Log in", + "login.log.error.permissions": "فایل تاریخچه ورود کاربران غیرقابل نوشتن است", + "logout": "Log out", + "topbar.error.class.definition": "topbar برای کلاس تعریف نشده است:", + "dashboard": "پیشخوان", + "dashboard.index.pages.title": "صفحات", + "dashboard.index.pages.edit": "ویرایش", + "dashboard.index.pages.add": "افزودن", + "dashboard.index.site.title": "آدرس وبسایت شما", + "dashboard.index.account.title": "مشخصات کاربر جاری", + "dashboard.index.account.edit": "ویرایش", + "dashboard.index.metatags.title": "تنطیمات سایت", + "dashboard.index.metatags.edit": "ویرایش", + "dashboard.index.history.title": "آخرین بروزرسانی ها", + "dashboard.index.history.text": "آخرین صفحاتی که تغییر داده اید در این مکان جهت سهولت دسترسی نمایش داده خواهد شد.", + "dashboard.index.license.title": "مجوز نرم افزار", + "dashboard.index.license.text": "به نظر می‌رسد در حال اجرای کربی در یک سرور عمومی بدون یک مجوز معتبر هستید!\n\nلطفا پشتیبان کربی باشید و (link: {buy} text: همین حالا یک مجوز خریداری کنید)\n\nاگر در حال حاضر دارای یک کلید مجوز هستید، کافی است آن را به فایل پیکربندی خود اضافه کنید: (link: {docs} text: site/config/config.php)", + "metatags": "تنظیمات سایت", + "metatags.info": "اطلاعات نرم افزار", + "metatags.license": "مجوز", + "metatags.version.toolkit": "نسخه هسته", + "metatags.version.kirby": "نسخه نرم افزار", + "metatags.version.panel": "نسخه پنل مدیریت", + "metatags.back": "بازگشت به پیشخوان", + "metatags.files": "فایل های عمومی", + "site.delete.error": "حذف سایت ممکن نیست", + "pages.show.settings": "تنظیمات صفحه", + "pages.show.preview": "پیش نمایش", + "pages.show.template": "قالب صفحه", + "pages.show.changeurl": "تغییر نشانی اینترنتی صفحه", + "pages.show.invisible": "وضعیت: مخفی", + "pages.show.visible": "وضعیت: قابل مشاهده", + "pages.show.changes.text": "برخی تغییرات ذخیره نشده است!", + "pages.show.changes.button": "انصراف", + "pages.show.delete": "حذف صفحه جاری", + "pages.show.subpages.title": "صفحات", + "pages.show.subpages.edit": "ویرایش", + "pages.show.subpages.add": "افزودن", + "pages.show.subpages.empty": "فاقد صفحه فرعی", + "pages.show.files.title": "فایل ها", + "pages.show.files.edit": "ویرایش", + "pages.show.files.add": "افزودن", + "pages.show.files.empty": "فاقد فایل", + "pages.show.error.permissions.title": "صفحه قابل نوشتن نیست", + "pages.show.error.permissions.text": "لطفا دسترسی پوشه محتوا و تمام فایل ها را بررسی نمایید.", + "pages.show.error.permissions.retry": "تلاش مجدد", + "pages.show.error.notitle.title": "طرح الگو فیلد عنوان ندارد.", + "pages.show.error.notitle.text": "لطفا یک فیلد عنوان اضافه کنید و دوباره سعی کنید", + "pages.show.error.notitle.retry": "تلاش مجدد", + "pages.show.error.form": "لطفا همه فیلدها را به درستی پر نمایید", + "pages.add.title.label": "افزودن صفحه جدید", + "pages.add.title.placeholder": "عنوان", + "pages.add.url.label": "پیوست نشانی اینترنتی", + "pages.add.url.enter": "(عنوان خود را وارد کنید)", + "pages.add.url.close": "بستن", + "pages.add.url.help": "Format: lowercase a-z, 0-9 and regular dashes", + "pages.add.template.label": "قالب صفحه", + "pages.add.error.create": "صفحه ایجاد نشد", + "pages.add.error.title": "عنوان وارد نشده است", + "pages.add.error.template": "فالب وارد نشده است.", + "pages.add.error.max.headline": "ساخت صفحه جدید ممکن نیست", + "pages.add.error.max.text": "حداکثر تعداد زیرصفحه برای صفحه فعلی پر شده است.", + "pages.url.uid.label": "پیوست نشانی اینترنتی", + "pages.url.uid.label.option": "ایجاد از روی عنوان", + "pages.url.error.exists": "یک صفحه با پیوست مشابه در حال حاضر وجود دارد", + "pages.url.error.move": "پیوست تغییر نکرد", + "pages.url.error.rights": "شما امکان تغییر آدرس این صفحه را ندارید", + "pages.template.select.label": "قالب صفحه", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "موقعیت", + "pages.toggle.invisible": "مخفی", + "pages.toggle.publish": "آیااز تغییر وضعیت صفحه به **قابل مشاهده** مطمئن هستید؟", + "pages.toggle.hide": "آیااز تغییر وضعیت صفحه به **مخفی** مطمئن هستید؟", + "pages.toggle.error.error": "وضعیت صفحه خطا قابل تغییر نیست", + "pages.delete.headline": "صفحه جاری حذف شود؟", + "pages.delete.error.home.headline": "صفحه اصلی وب سایت نمی تواند حذف شود", + "pages.delete.error.home.text": "شما در حال تلاش برای حذف صفحه اصلی هستید. این امکان پذیر نمی باشد و به اثرات ناخواسته منجر می شود.", + "pages.delete.error.error.headline": "صفحه خطا نمی تواند حذف شود", + "pages.delete.error.error.text": "شما در حال تلاش برای حذف صفحه خطا هستید. این امکان پذیر نمی باشد و به اثرات ناخواسته منجر می شود.", + "pages.delete.error.children.headline": "حذف صفحه ممکن نیست", + "pages.delete.error.children.text": "این صفحه دارای زیرصفحه است و نمی تواند حذف شود. لطفا ابتدا تمام زیرصفحه های آنرا حذف کنید.", + "pages.delete.error.blocked.headline": "حذف صفحه ممکن نیست", + "pages.delete.error.blocked.text": "این صفحه قفل شده است و نمی تواند حذف شود.", + "pages.search.help": "صفحات را بر اساس نشانی اینترنتی جستجو کنید. با استفاده از کلیدهای جهت دار بالا و پایین ردیف مورد نظر خود را انتخاب و جهت انتقال به صفحه انتخابی کلید Enter را فشار دهید.", + "pages.search.noresults": "هیچ نتیجه ای منطبق بر درخواست شما وجود دارد. لطفا دوباره با یک نشانی اینترنتی متفاوت امتحان کنید.", + "pages.error.missing": "صفحه مورد نظر پیدا نشد.", + "subpages": "صفحات", + "subpages.index.headline": "صفحات فرعی", + "subpages.index.back": "بازگشت", + "subpages.index.add": "افزودن صفحه جدید", + "subpages.index.add.first.text": "این صفحه در حال حاضر هیچ زیرصفحه ای ندارد", + "subpages.index.add.first.button": "افزودن نخستین صفحه", + "subpages.index.visible": "صفحات قابل مشاهده", + "subpages.index.visible.help": "صفحات مخفی را جهت مرتب سازی و آشکارسازی به اینجا بکشید.", + "subpages.index.invisible": "صفحات مخفی", + "subpages.index.invisible.help": "صفحات آشکار را جهت مخفی کردن به اینجا بکشید.", + "subpages.add.error": "این صفحه نمی‌تواند دارای صفحات فرعی باشد", + "subpages.add.error.more": "ایجاد صفحات فرعی بیشتر ممکن نیست", + "subpages.error.missing": "صفحه مورد نظر پیدا نشد.", + "files": "فایل ها", + "files.index.headline": "فایل های مرتبط با", + "files.index.back": "بازگشت", + "files.index.upload": "آپلود فایل جدید", + "files.index.upload.first.text": "این صفحه در حال حاضر فاقد فایل است", + "files.index.upload.first.button": "آپلود نخستین فایل", + "files.index.edit": "ویرایش", + "files.index.delete": "حذف", + "files.index.error.disabled": "این صفحه نمی‌تواند دارای فایل باشد", + "files.add.error.max": "محدودیت حداکثر تعداد فایل برای صفحه فعلی سر رسیده است.", + "files.add.error.extension.missing": "شما نمی‌توانید فایل‌های بدون پسوند را آپلود کنید", + "files.add.error.extension.forbidden": "پسوند فایل غیرمجاز است", + "files.add.error.mime.forbidden": "فرمت فایل غیرمجاز است", + "files.add.error.htaccess": "امکان آپلود فایل htaccess وجود ندارد", + "files.add.error.invisible": "امکان آپلود فایلهای مخفی وجود ندارد", + "files.add.blueprint.type.error": "صفحه تنها اجازه می‌دهد که:", + "files.add.blueprint.size.error": "سایز فایل مجاز برای صفحه", + "files.show.name.label": "نام فایل", + "files.show.info.label": "نوع / حجم / ابعاد", + "files.show.link.label": "لینک عمومی", + "files.show.open": "نمایش/دانلود فایل", + "files.show.back": "بازگشت", + "files.show.replace": "جایگزینی", + "files.show.delete": "حذف", + "files.show.error.rename": "فایل را نمی توان تغییر نام داد", + "files.show.error.form": "لطفا همه فیلدها را به درستی پر نمایید", + "files.upload.drop": "فایل ها را اینجا رها کنید…", + "files.upload.click": "…یا جهت انتخاب فایل کلیک کنید", + "files.replace.drop": "فایل ها را اینجا رها کنید…", + "files.replace.click": "…یا جهت انتخاب فایل کلیک کنید", + "files.replace.error.type": "فایل آپلود شده باید نوع فایل یکسان داشته باشد", + "files.delete.headline": "آیا شما واقعا می خواهید این فایل را حذف کنید؟", + "files.error.missing.page": "صفحه مورد نظر پیدا نشد.", + "files.error.missing.file": "فایل مورد نظر پیدا نشد.", + "users": "کاربران", + "users.index.headline": "همه کاربران", + "users.index.add": "افزودن کاربر جدید", + "users.index.edit": "ویرایش", + "users.index.delete": "حذف", + "users.form.username.label": "نام کاربری", + "users.form.username.placeholder": "نام کاربری شما", + "users.form.username.help": "حروف مجاز: حروف کوچک a-z، اعداد 0-9 و خط تیره -", + "users.form.username.readonly": "نام کاربری را نمیتوان تغییر داد", + "users.form.firstname.label": "نام", + "users.form.lastname.label": "نام خانوادگی", + "users.form.email.label": "پست الکترونیک", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "گذرواژه", + "users.form.password.confirm.label": "تکرار گذرواژه", + "users.form.password.new.label": "گذرواژه جدید", + "users.form.password.new.confirm.label": "تکرار گذرواژه", + "users.form.password.new.help": "برای حفظ گذرواژه فعلی فیلد را خالی رها کنید", + "users.form.language.label": "زبان", + "users.form.role.label": "نقش", + "users.form.options.headline": "گزینه های حساب کاربری", + "users.form.options.message": "ارسال پست الکترونیک", + "users.form.options.delete": "حذف حساب کاربری", + "users.form.avatar.headline": "تصویر پروفایل", + "users.form.avatar.upload": "آپلود تصویر پروفایل", + "users.form.avatar.replace": "جایگزینی تصویر پروفایل", + "users.form.avatar.delete": "حذف نصویر پروفایل", + "users.form.back": "بازگشت به لیست کاربران", + "users.form.error.password.confirm": "لطفا تکرار گذرواژه را وارد نمایید", + "users.form.error.update": "کاربر نمی تواند به روز شود", + "users.form.error.update.rights": "شما اجازه بروزرسانی این کاربر را ندارید", + "users.form.error.create": "کاربر نمی تواند ایجاد شود", + "users.form.error.permissions.title": "پوشه account قابل نوشتن نیست", + "users.form.error.permissions.text": "لطفا مطمئن شوید که /site/accounts موجود و قابل نوشتن است.", + "users.delete.headline": "کاربر جاری حذف شود؟", + "users.delete.error": "کاربر نمی تواند حذف شود", + "users.delete.error.permission": "شما اجازه حذف کاربران را ندارید", + "users.delete.error.permission.single": "شما اجازه حذف این کاربر را ندارید", + "users.delete.error.lastadmin": "حذف آخرین مدیر سیستم ممکن نیست", + "users.avatar.drop": "تصویر پروفایل را اینجا رها کنید…", + "users.avatar.click": "یا برای انتخاب و آپلود کلیک کنید", + "users.avatar.error.type": "شما فقط می توانید فایل های JPG، PNG و GIF آپلود کنید", + "users.avatar.error.folder.headline": "پوشه avatar قایل نوشتن نیست", + "users.avatar.error.folder.text": "جهت آپلود تصویر پروفایل پوشه /assets/avatars را ایجاد و آن را قابل نوشتن کنید.", + "users.avatar.error.permission": "شما اجازه تغییر تصویر پروفایل را ندارید", + "users.avatar.delete.error": "تصویر پروفایل را نمیتوان حذف کرد", + "users.avatar.delete.error.permission": "شما اجازه حذف تصویر پروفایل این کاربر را ندارید", + "users.avatar.delete.success": "تصویر پروفایل حذف شد", + "users.avatar.missing": "فاقد تصویر پروفایل", + "users.error.missing": "کاربر مورد نظر پیدا نشد", + "user.error.lastadmin": "شما تنها کاربر مدیر این سیستم هستید و تغییر آن ممکن نیست", + "form.error.missing": "فرم یافت نشد", + "form.construct.error.invalid": "روال ساخت فرم نامعتبر است", + "fields.required": "اجباری", + "fields.date.label": "تاریخ", + "fields.date.months": [ + "ژانویه", + "فوریه", + "مارس", + "آوریل", + "می", + "ژوئن", + "ژوئیه", + "اوت", + "سپتامبر", + "اکتبر", + "نوامبر", + "دسامبر" + ], + "fields.date.weekdays": [ + "یکشنبه", + "دوشنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه" + ], + "fields.date.weekdays.short": [ + "یکشنبه", + "دوشنبه", + "سه شنبه", + "چهارشنبه", + "پنجشنبه", + "جمعه", + "شنبه" + ], + "fields.email.label": "پست الکترونیک", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "عدد", + "fields.number.placeholder": "#", + "fields.page.label": "صفحه", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "گذرواژه", + "fields.structure.add": "افزودن", + "fields.structure.add.first": "افزودن نخستین مورد", + "fields.structure.empty": "موردی وجود ندارد.", + "fields.structure.entry.error": "آیتم یافت نشد", + "fields.structure.cancel": "انصراف", + "fields.structure.save": "تایید", + "fields.structure.edit": "ویرایش", + "fields.structure.delete": "حذف", + "fields.structure.delete.label": "مدخل جاری حذف شود؟", + "fields.tags.label": "برچسب ها", + "fields.tel.label": "تلفن", + "fields.textarea.buttons.bold.label": "متن با حروف درشت", + "fields.textarea.buttons.bold.text": "متن با حروف درشت", + "fields.textarea.buttons.italic.label": "متن اریب", + "fields.textarea.buttons.italic.text": "متن اریب", + "fields.textarea.buttons.link.label": "پیوند", + "fields.textarea.buttons.email.label": "پست الکترونیک", + "fields.textarea.buttons.image.label": "تصویر", + "fields.textarea.buttons.file.label": "فایل", + "fields.toggle.yes": "بله", + "fields.toggle.no": "خیر", + "fields.toggle.on": "روشن", + "fields.toggle.off": "خاموش", + "fields.error.missing.controller": "فایل کنترلر فیلد یافت نشد", + "fields.error.missing.class": "کلاس کنترلر فیلد سافت نشد", + "fields.error.route.invalid": "مسید فیلد نامعتبر است", + "fields.error.extended": "توسعه فیلد ممکن نیست", + "editor.link.url.label": "آدرس پیوند", + "editor.link.text.label": "متن پیوند", + "editor.link.text.help": "متن پیوند اختیاری است", + "editor.email.address.label": "درج نشانی پست الکترونیک", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "متن پیوند", + "editor.email.text.help": "متن پیوند اختیاری است", + "editor.file.empty": "این صفحه فاقد فایل است", + "editor.image.empty": "این صفحه فاقد تصویر است", + "autocomplete.method.error": "روال تکمیل خودکار معتبر نیست", + "blueprints.error.default.missing": "طرح الگوی پیش‌فرض یافت نشد", + "error": "خطا", + "error.headline": "خطا" +} \ No newline at end of file diff --git a/panel/app/translations/fa/package.json b/panel/app/translations/fa/package.json new file mode 100644 index 0000000..a80ab8f --- /dev/null +++ b/panel/app/translations/fa/package.json @@ -0,0 +1,4 @@ +{ + "title": "پارسی", + "direction": "rtl" +} \ No newline at end of file diff --git a/panel/app/translations/fi/core.json b/panel/app/translations/fi/core.json new file mode 100644 index 0000000..ddfa940 --- /dev/null +++ b/panel/app/translations/fi/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Peruuta", + "add": "Lisää", + "addit": "Add & Edit", + "save": "Tallenna", + "saved": "Tallennettu!", + "change": "Change", + "delete": "Poista", + "insert": "Lisää", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Näytä asetukset", + "options.hide": "Piilota asetukset", + "installation": "Asennus", + "installation.check.headline": "Kirby Panel asennus", + "installation.check.text": "Kirby havaitsi seuraavat virheet asennuksen yhteydessä…", + "installation.check.retry": "Yritä uudelleen", + "installation.check.error": "Muutama ongelma löytyi!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts edellyttää kirjoitusoikeudet", + "installation.check.error.avatars": "/assets/avatars edellyttää kirjoitusoikeudet", + "installation.check.error.blueprints": "Ole hyvä ja lisää /site/blueprints kansio", + "installation.check.error.content": "/content ja kaikki sen alla olevat tiedostot ja kansiot edellyttävät kirjoitusoikeudet.", + "installation.check.error.thumbs": "/thumbs edellyttää kirjoitusikeudet.", + "installation.signup.username.label": "Luo ensimmäinen käyttäjä", + "installation.signup.username.placeholder": "Tunnus", + "installation.signup.email.label": "Sähköposti", + "installation.signup.email.placeholder": "nimi@osoite.fi", + "installation.signup.password.label": "Salasana", + "installation.signup.language.label": "Kieli", + "installation.signup.button": "Luo käyttäjä", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Käyttäjätunnus", + "login.password.label": "Salasana", + "login.error": "Tunnus tai salasana on virheellinen", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Hallintapaneeli", + "dashboard.index.pages.title": "Sivut", + "dashboard.index.pages.edit": "Muokkaa", + "dashboard.index.pages.add": "Lisää", + "dashboard.index.site.title": "Sivuston URL-osoite", + "dashboard.index.account.title": "Käyttäjätilisi", + "dashboard.index.account.edit": "Muokkaa", + "dashboard.index.metatags.title": "Sivuston asetukset", + "dashboard.index.metatags.edit": "Muokkaa", + "dashboard.index.history.title": "Viimeisimmät päivityksesi", + "dashboard.index.history.text": "Viimeksi muokkaamasi sivut listataan tässä jotta löydät ne jatkossa helpommin.", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Sivuston asetukset", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "Takaisin hallintapaneeliin", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Sivun asetukset", + "pages.show.preview": "Esikatselu", + "pages.show.template": "Sivupohja", + "pages.show.changeurl": "Vaihda URL-osoite", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Poista tämä sivu", + "pages.show.subpages.title": "Sivut", + "pages.show.subpages.edit": "Muokkaa", + "pages.show.subpages.add": "Lisää", + "pages.show.subpages.empty": "Tälle sivulle ei ole lisätty alasivuja", + "pages.show.files.title": "Tiedostot", + "pages.show.files.edit": "Muokkaa", + "pages.show.files.add": "Lisää", + "pages.show.files.empty": "Tälle sivulle ei ole lisätty tiedostoja", + "pages.show.error.permissions.title": "Sivun muokkaamista ei ole sallittu", + "pages.show.error.permissions.text": "Ole hyvä ja tarkista kansion /content ja sen alla olevien tiedostojen kirjoitusoikeudet.", + "pages.show.error.permissions.retry": "Yritä uudelleen", + "pages.show.error.notitle.title": "Tässä pohjassa ei ole nimikenttää", + "pages.show.error.notitle.text": "Ole hyvä ja lisää ensin nimikenttä", + "pages.show.error.notitle.retry": "Yritä uudelleen", + "pages.show.error.form": "Ole hyvä ja täytä kaikki kentät oikein.", + "pages.add.title.label": "Lisää uusi sivu", + "pages.add.title.placeholder": "Nimi", + "pages.add.url.label": "URL-osoite", + "pages.add.url.enter": "(kirjoita osoite)", + "pages.add.url.close": "Sulje", + "pages.add.url.help": "Sallitut merkit: pienet kirjaimet a-z, 0-9 sekä väliviivat", + "pages.add.template.label": "Sivupohja", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "Otsikko puuttuu", + "pages.add.error.template": "Sivupohjaa ei ole asetettu", + "pages.add.error.max.headline": "Uusia sivuja ei voi enää luoda", + "pages.add.error.max.text": "Nykyiselle sivulle ei voi luoda enempää alasivuja", + "pages.url.uid.label": "URL-pääte", + "pages.url.uid.label.option": "Luo nimen perusteella", + "pages.url.error.exists": "Olemassaolevalla sivulla on jo sama URL-pääte", + "pages.url.error.move": "Päätettä ei voitu muuttaa", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Sivupohja", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Haluatko varmasti poistaa sivun?", + "pages.delete.error.home.headline": "Aloitussivua ei voi poistaa", + "pages.delete.error.home.text": "Sivuston aloitussivun poistaminen ei ole mahdollista.", + "pages.delete.error.error.headline": "Virhesivua ei voi poistaa", + "pages.delete.error.error.text": "Virhesivun poistaminen ei ole mahdollista.", + "pages.delete.error.children.headline": "Sivua ei voida poistaa", + "pages.delete.error.children.text": "Tällä sivulla on alasivuja, jotka pitää poistaa ennen kuin itse sivu voidaan poistaa.", + "pages.delete.error.blocked.headline": "Sivua ei voida poistaa", + "pages.delete.error.blocked.text": "Tämä sivu on suojattu eikä sitä voida poistaa.", + "pages.search.help": "Hae sivuja osoitteen perusteella. Voit selata hakutuloksia nuolinäppäimillä ylös tai alas, ja painamalla ENTER siirryt valitulle sivulle.", + "pages.search.noresults": "Hakutermeillä ei löytynyt tuloksia.", + "pages.error.missing": "Sivua ei löytynyt", + "subpages": "Sivut", + "subpages.index.headline": "Alasivut sivulla", + "subpages.index.back": "Takaisin", + "subpages.index.add": "Lisää uusi sivu", + "subpages.index.add.first.text": "Tälle sivulle ei ole vielä lisätty alasivuja", + "subpages.index.add.first.button": "Lisää alasivu", + "subpages.index.visible": "Näkyvät sivut", + "subpages.index.visible.help": "Raahaa piilotetut sivut tähän jotta voit tehdä niistä näkyviä tai muuttaa niiden järjestystä.", + "subpages.index.invisible": "Piilotetut sivut", + "subpages.index.invisible.help": "Raahaa näkyvät sivut tähän niin voit tehdä niistä piilotettuja.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Sivua ei löytynyt", + "files": "Tiedostot", + "files.index.headline": "Tiedostot sivulla", + "files.index.back": "Takaisin", + "files.index.upload": "Lisää uusi tiedosto", + "files.index.upload.first.text": "Tälle sivulle ei ole lisätty tiedostoja", + "files.index.upload.first.button": "Lisää tiedosto", + "files.index.edit": "Muokkaa", + "files.index.delete": "Poista", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Tiedostonimi", + "files.show.info.label": "Tyyppi / Koko / Mitat", + "files.show.link.label": "Julkinen linkki", + "files.show.open": "Näytä/lisää tiedosto", + "files.show.back": "Takaisin", + "files.show.replace": "Korvaa", + "files.show.delete": "Poista", + "files.show.error.rename": "Tiedostoa ei voitu nimetä uudelleen", + "files.show.error.form": "Ole hyvä ja täytä kaikki kentät oikein", + "files.upload.drop": "Raahaa tiedostot tähän…", + "files.upload.click": "…tai valitse tiedosto", + "files.replace.drop": "Raahaa tiedosto tähän…", + "files.replace.click": "…tai valitse uusi tiedosto", + "files.replace.error.type": "Uuden ja korvattavan tiedoston tulee olla samaa tyyppiä", + "files.delete.headline": "Haluatko varmasti poistaa tiedoston?", + "files.error.missing.page": "Sivua ei löytynyt", + "files.error.missing.file": "Tiedostoa ei löytynyt", + "users": "Käyttäjät", + "users.index.headline": "Kaikki käyttäjät", + "users.index.add": "Lisää uusi käyttäjä", + "users.index.edit": "Muokkaa", + "users.index.delete": "Poista", + "users.form.username.label": "Käyttäjätunnus", + "users.form.username.placeholder": "Tunnuksesi", + "users.form.username.help": "Sallitut merkit: pienet kirjaimet a-z, 0-9 sekä väliviivat", + "users.form.username.readonly": "Tunnuksen muuttamista ei ole sallittu", + "users.form.firstname.label": "Etunimi", + "users.form.lastname.label": "Sukunimi", + "users.form.email.label": "Sähköposti", + "users.form.email.placeholder": "nimi@osoite.fi", + "users.form.password.label": "Salasana", + "users.form.password.confirm.label": "Salasana uudelleen", + "users.form.password.new.label": "Uusi salasana", + "users.form.password.new.confirm.label": "Uusi salasana uudelleen", + "users.form.password.new.help": "Jätä tyhjäksi jos et halua vaihtaa salasanaasi", + "users.form.language.label": "Kieli", + "users.form.role.label": "Käyttäjätaso", + "users.form.options.headline": "Tilinhallinta", + "users.form.options.message": "Lähetä sähköposti", + "users.form.options.delete": "Poista tili", + "users.form.avatar.headline": "Profiilikuva", + "users.form.avatar.upload": "Lisää profiilikuva", + "users.form.avatar.replace": "Vaihda profiilikuva", + "users.form.avatar.delete": "Poista profiilikuva", + "users.form.back": "Takaisin käyttäjähallintaan", + "users.form.error.password.confirm": "Varmista salasana", + "users.form.error.update": "Käyttäjätilin päivitys epäonnistui", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Käyttäjätilin luominen epäonnistui", + "users.form.error.permissions.title": "Käyttäjähakemistoon ei voitu kirjoittaa", + "users.form.error.permissions.text": "Ole hyvä ja varmista että /site/accounts on olemassa ja tarkista sen kirjoitusoikeudet.", + "users.delete.headline": "Haluatko varmsti poistaa käyttäjän?", + "users.delete.error": "Käyttäjää ei voitu poistaa", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Raahaa profiilikuva tähän…", + "users.avatar.click": "…tai valitse tiedosto", + "users.avatar.error.type": "Kuvien tulee olla JPG, PNG tai GIF muodossa", + "users.avatar.error.folder.headline": "Kansio avatar vaatii kirjoitusoikeudet", + "users.avatar.error.folder.text": "Profiilikuvan lisääminen edellyttää /assets/avatars kansion luomista ja kirjoitusoikeuksia.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Profiilikuvaa ei voitu poistaa", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Profiilikuva poistettiin", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Käyttäjää ei löytynyt", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Pakollinen", + "fields.date.label": "Päivämäärä", + "fields.date.months": [ + "Tammikuu", + "Helmikuu", + "Maaliskuu", + "Huhtikuu", + "Toukokuu", + "Kesäkuu", + "Heinäkuu", + "Elokuu", + "Syyskuu", + "Lokakuu", + "Marraskuu", + "Joulukuu" + ], + "fields.date.weekdays": [ + "Sunnuntai", + "Maanantai", + "Tiistai", + "Keskiviikko", + "Torstai", + "Perjantai", + "Lauantai" + ], + "fields.date.weekdays.short": [ + "Su", + "Ma", + "Ti", + "Ke", + "To", + "Pe", + "La" + ], + "fields.email.label": "Sähköposti", + "fields.email.placeholder": "nimi@osoite.fi", + "fields.number.label": "Numero", + "fields.number.placeholder": "#", + "fields.page.label": "Sivu", + "fields.page.placeholder": "polku/sivulle", + "fields.password.label": "Salasana", + "fields.structure.add": "Lisää", + "fields.structure.add.first": "Lisää ensimmäinen kirjoitus", + "fields.structure.empty": "Ei kirjoituksia toistaiseksi.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Peruuta", + "fields.structure.save": "Tallenna", + "fields.structure.edit": "Muokkaa", + "fields.structure.delete": "Poista", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Avainsanat", + "fields.tel.label": "Puhelin", + "fields.textarea.buttons.bold.label": "Lihavointi", + "fields.textarea.buttons.bold.text": "Lihavointi", + "fields.textarea.buttons.italic.label": "Kursivointi", + "fields.textarea.buttons.italic.text": "Kursivointi", + "fields.textarea.buttons.link.label": "Linkki", + "fields.textarea.buttons.email.label": "Sähköposti", + "fields.textarea.buttons.image.label": "Kuva", + "fields.textarea.buttons.file.label": "Tiedosto", + "fields.toggle.yes": "Kyllä", + "fields.toggle.no": "Ei", + "fields.toggle.on": "Päällä", + "fields.toggle.off": "Pois päältä", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Lisää URL-osoite", + "editor.link.text.label": "Linkin teksti", + "editor.link.text.help": "Linkin teksti on valinnainen", + "editor.email.address.label": "Lisää sähköpostiosoite", + "editor.email.address.placeholder": "nimi@osoite.fi", + "editor.email.text.label": "Linkin testi", + "editor.email.text.help": "Linkin teksti on valinnainen", + "editor.file.empty": "Sivulle ei ole lisätty tiedostoja", + "editor.image.empty": "Sivulle ei ole lisätty kuvia", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Virhe", + "error.headline": "Virhe" +} \ No newline at end of file diff --git a/panel/app/translations/fi/package.json b/panel/app/translations/fi/package.json new file mode 100644 index 0000000..609698d --- /dev/null +++ b/panel/app/translations/fi/package.json @@ -0,0 +1,4 @@ +{ + "title": "Suomi", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/fr/core.json b/panel/app/translations/fr/core.json new file mode 100644 index 0000000..75930d1 --- /dev/null +++ b/panel/app/translations/fr/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuler", + "add": "Ajouter", + "addit": "Ajouter et éditer", + "save": "Enregistrer", + "saved": "Enregistré !", + "change": "Modifier", + "delete": "Supprimer", + "insert": "Insérer", + "ok": "Ok", + "routes.error.invalid": "URL du Panel incorrecte", + "controller.error.invalid": "Contrôleur incorrect", + "controller.error.action": "Action incorrecte", + "view.error.invalid": "Vue incorrecte : ", + "options.show": "Afficher les options", + "options.hide": "Masquer les options", + "installation": "Installation", + "installation.check.headline": "Installation de Kirby Panel", + "installation.check.text": "Durant l’installation, Kirby a rencontré les problèmes suivants…", + "installation.check.retry": "Essayer de nouveau", + "installation.check.error": "Des problèmes on été rencontrés !", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Le répertoire “/site/accounts” n’est pas accessible en écriture", + "installation.check.error.avatars": "Le répertoire “/assets/avatars” n’est pas accessible en écriture", + "installation.check.error.blueprints": "Veuillez ajouter un répertoire “/site/blueprints”", + "installation.check.error.content": "Le répertoire “/content”, les sous-répertoires et les fichiers qu’il contient doivent être accessibles en écriture.", + "installation.check.error.thumbs": "Le répertoire “thumbs/” doit être accessible en écriture.", + "installation.signup.username.label": "Créer votre premier compte utilisateur", + "installation.signup.username.placeholder": "Nom d’utilisateur", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemple.com", + "installation.signup.password.label": "Mot de passe", + "installation.signup.language.label": "Langue", + "installation.signup.button": "Créer votre compte", + "login": "Connexion", + "login.welcome": "Veuillez vous identifier avec votre nouveau compte", + "login.username.label": "Identifiant", + "login.password.label": "Mot de passe", + "login.error": "Identifiant ou mot de passe incorrect", + "login.button": "Connexion", + "login.log.error.permissions": "Le journal de connexion n’est pas accessible en écriture.", + "logout": "Déconnexion", + "topbar.error.class.definition": "Définition de topbar manquante pour la classe : ", + "dashboard": "Tableau de bord", + "dashboard.index.pages.title": "Pages", + "dashboard.index.pages.edit": "Modifier", + "dashboard.index.pages.add": "Ajouter", + "dashboard.index.site.title": "URL de votre site", + "dashboard.index.account.title": "Votre profil", + "dashboard.index.account.edit": "Modifier", + "dashboard.index.metatags.title": "Paramètres du site", + "dashboard.index.metatags.edit": "Modifier", + "dashboard.index.history.title": "Modifications récentes", + "dashboard.index.history.text": "Vos modifications les plus récentes seront affichées ici afin de les retrouver plus aisément.", + "dashboard.index.license.title": "Licence de Kirby", + "dashboard.index.license.text": "Il semblerait que vous utilisiez Kirby sans licence valide sur un serveur public !\n\nMerci de soutenir Kirby en (link: {buy} text: achetant une licence maintenant)\n\nSi vous avez déjà une licence, ajoutez-là simplement à votre fichier de configuration : (link: {docs} text: site/config/config.php)", + "metatags": "Paramètres du site", + "metatags.info": "Informations de Kirby", + "metatags.license": "License de Kirby", + "metatags.version.toolkit": "Version du Toolkit", + "metatags.version.kirby": "Version de Kirby", + "metatags.version.panel": "Version du Panel", + "metatags.back": "Retour au tableau de bord", + "metatags.files": "Fichiers du site", + "site.delete.error": "Le site ne peut être supprimé", + "pages.show.settings": "Options de la page", + "pages.show.preview": "Prévisualiser", + "pages.show.template": "Modèle de page", + "pages.show.changeurl": "Modifier l’URL", + "pages.show.invisible": "Statut : invisible", + "pages.show.visible": "Statut : visible", + "pages.show.changes.text": "Vous avez des modifications non enregistrées !", + "pages.show.changes.button": "Les supprimer", + "pages.show.delete": "Supprimer cette page", + "pages.show.subpages.title": "Pages", + "pages.show.subpages.edit": "Modifier", + "pages.show.subpages.add": "Ajouter", + "pages.show.subpages.empty": "Cette page n’a aucune page secondaire", + "pages.show.files.title": "Fichiers", + "pages.show.files.edit": "Modifier", + "pages.show.files.add": "Ajouter", + "pages.show.files.empty": "Cette page n’a aucun fichier attaché", + "pages.show.error.permissions.title": "La page n’est pas accessible en écriture", + "pages.show.error.permissions.text": "Merci de vérifier les permissions pour le répertoire “/content” et ses fichiers.", + "pages.show.error.permissions.retry": "Essayer de nouveau", + "pages.show.error.notitle.title": "Ce blueprint n’a pas de champs “title”", + "pages.show.error.notitle.text": "Veuillez ajouter un champs “title” et essayer de nouveau", + "pages.show.error.notitle.retry": "Essayer de nouveau", + "pages.show.error.form": "Merci de remplir correctement l’ensemble des champs", + "pages.add.title.label": "Ajouter une nouvelle page", + "pages.add.title.placeholder": "Titre", + "pages.add.url.label": "Identifiant pour l’URL de la page", + "pages.add.url.enter": "(saisir votre titre)", + "pages.add.url.close": "Fermer", + "pages.add.url.help": "Format : minuscules a-z, chiffres 0-9 et tiret simple", + "pages.add.template.label": "Modèle de page", + "pages.add.error.create": "La page n'a pu être créée", + "pages.add.error.title": "Le titre est manquant", + "pages.add.error.template": "Le template est manquant", + "pages.add.error.max.headline": "La création de nouvelles pages secondaires n’est pas autorisé pour cette page", + "pages.add.error.max.text": "Le nombre maximum de sous-pages a été atteint pour cette page.", + "pages.url.uid.label": "Identifiant de l’URL", + "pages.url.uid.label.option": "Créer à partir du titre", + "pages.url.error.exists": "Une page du même nom existe déjà", + "pages.url.error.move": "L’identifiant de l’URL n’a pu être changé", + "pages.url.error.rights": "Vous ne pouvez pas modifier l’URL de cette page", + "pages.template.select.label": "Modèle de page", + "pages.template.warning.text": "Les champs suivants seront modifiés si vous changez de modèle de page", + "pages.template.warning.removed": "Champs supprimés", + "pages.template.warning.replaced": "Champs remplacés", + "pages.template.warning.added": "Champs ajoutés", + "pages.template.error": "Le modèle de cette page ne peut être changé", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Voulez-vous vraiment modifier le statut de cette page en **visible**?", + "pages.toggle.hide": "Voulez-vous vraiment modifier le statut de cette page en **invisible**?", + "pages.toggle.error.error": "Le statut de la page d'erreur ne peut pas être modifié", + "pages.delete.headline": "Voulez-vous vraiment supprimer définitivement cette page ?", + "pages.delete.error.home.headline": "La page d’accueil ne peut être supprimée", + "pages.delete.error.home.text": "Vous essayez de supprimer la page d’accueil. Ce n’est pas possible car cela entraînerait des effets indésirables.", + "pages.delete.error.error.headline": "La page d’erreur ne peut être supprimée", + "pages.delete.error.error.text": "Vous essayez de supprimer la page d’erreur. Ce n’est pas possible car cela aurait des effets indésirables.", + "pages.delete.error.children.headline": "La page ne peut être supprimée", + "pages.delete.error.children.text": "Cette page contient des pages secondaires. Veuillez supprimer les pages associées au préalable.", + "pages.delete.error.blocked.headline": "La page ne peut être supprimée", + "pages.delete.error.blocked.text": "Cette page est verrouillée et ne peut être supprimée.", + "pages.search.help": "Rechercher des pages par URL. Naviguer entre les résultats avec les flèches « haut » et « bas » du clavier, puis appuyer sur la touche « Entrée » pour aller à la page sélectionnée.", + "pages.search.noresults": "Il n’y a pas de résultat à votre recherche. Veuillez essayer de nouveau avec une URL différente.", + "pages.error.missing": "La page n’a pu être trouvée", + "subpages": "Pages", + "subpages.index.headline": "Pages dans", + "subpages.index.back": "Retour", + "subpages.index.add": "Ajouter une nouvelle page", + "subpages.index.add.first.text": "Cette page n’a pas encore de page secondaire", + "subpages.index.add.first.button": "Ajouter une première page", + "subpages.index.visible": "Pages visibles", + "subpages.index.visible.help": "Glisser une page invisible ici pour la classer/la rendre visible.", + "subpages.index.invisible": "Pages invisibles", + "subpages.index.invisible.help": "Glisser une page ici pour la rendre invisible.", + "subpages.add.error": "Cette page ne peut contenir de sous-page", + "subpages.add.error.more": "Cette page ne peut plus avoir de sous-page supplémentaire", + "subpages.error.missing": "La page n’a pu être trouvée", + "files": "Fichiers", + "files.index.headline": "Fichiers pour la page :", + "files.index.back": "Retour", + "files.index.upload": "Ajouter un fichier", + "files.index.upload.first.text": "Cette page n’a pas encore de fichier attaché", + "files.index.upload.first.button": "Ajouter un premier fichier", + "files.index.edit": "Modifier", + "files.index.delete": "Supprimer", + "files.index.error.disabled": "La page ne peut contenir de page secondaire", + "files.add.error.max": "Le nombre maximum de fichiers de cette page a été atteint.", + "files.add.error.extension.missing": "Vous ne pouvez transférer de fichier sans extension", + "files.add.error.extension.forbidden": "Extension de fichier interdite", + "files.add.error.mime.forbidden": "Ce type MIME est interdit", + "files.add.error.htaccess": "Les fichiers htaccess ne peuvent être transférés", + "files.add.error.invisible": "Les fichiers invisibles ne peuvent être transférés", + "files.add.blueprint.type.error": "La page n’autorise que :", + "files.add.blueprint.size.error": "La page n’autorise qu’un poids de fichier de", + "files.show.name.label": "Nom du fichier", + "files.show.info.label": "Type / Poids / Dimensions", + "files.show.link.label": "Lien public", + "files.show.open": "Afficher/télécharger le fichier", + "files.show.back": "Retour", + "files.show.replace": "Remplacer", + "files.show.delete": "Supprimer", + "files.show.error.rename": "Le fichier n’a pu être renommé", + "files.show.error.form": "Merci de remplir correctement chaque champ du formulaire", + "files.upload.drop": "Glisser un fichier ici…", + "files.upload.click": "…ou cliquer pour le transférer depuis votre ordinateur", + "files.replace.drop": "Glisser un fichier ici…", + "files.replace.click": "…ou cliquer pour le remplacer", + "files.replace.error.type": "Le fichier transféré doit être du même type", + "files.delete.headline": "Voulez-vous vraiment supprimer ce fichier ?", + "files.error.missing.page": "La page n’a pu être trouvée", + "files.error.missing.file": "Le fichier n’a pu être trouvé", + "users": "Utilisateurs", + "users.index.headline": "Tous les utilisateurs", + "users.index.add": "Ajouter un utilisateur", + "users.index.edit": "Modifier", + "users.index.delete": "Supprimer", + "users.form.username.label": "Nom d’utilisateur", + "users.form.username.placeholder": "Votre nom d’utilisateur", + "users.form.username.help": "Caractères autorisés : minuscules a-z, chiffres 0-9 et tirets simple", + "users.form.username.readonly": "Le nom d’utilisateur ne peut être modifié", + "users.form.firstname.label": "Prénom", + "users.form.lastname.label": "Nom", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemple.com", + "users.form.password.label": "Mot de passe", + "users.form.password.confirm.label": "Confirmer votre mot de passe", + "users.form.password.new.label": "Nouveau mot de passe", + "users.form.password.new.confirm.label": "Confirmer le nouveau mot de passe", + "users.form.password.new.help": "Laisser vide pour conserver votre mot de passe actuel", + "users.form.language.label": "Langue", + "users.form.role.label": "Rôle", + "users.form.options.headline": "Options du compte", + "users.form.options.message": "Envoyer un email", + "users.form.options.delete": "Supprimer le compte", + "users.form.avatar.headline": "Image du profil", + "users.form.avatar.upload": "Ajouter une image de profil", + "users.form.avatar.replace": "Remplacer l’image du profil", + "users.form.avatar.delete": "Supprimer l’image du profil", + "users.form.back": "Retour au compte", + "users.form.error.password.confirm": "Veuillez confirmer le mot de passe", + "users.form.error.update": "Le compte utilisateur ne peut être mis à jour", + "users.form.error.update.rights": "Vous n’êtes pas autorisé à mettre à jour cet utilisateur", + "users.form.error.create": "Le compte utilisateur n’a pu être créé", + "users.form.error.permissions.title": "Le répertoire des comptes n’est pas accessible en écriture", + "users.form.error.permissions.text": "Vérifiez que le répertoire “/site/accounts” existe et est accessible en écriture.", + "users.delete.headline": "Voulez-vous vraiment supprimer ce compte ?", + "users.delete.error": "Le compte utilisateur n’a pu être supprimé", + "users.delete.error.permission": "Vous n’êtes pas autorisé à supprimer des utilisateurs", + "users.delete.error.permission.single": "Vous n’êtes pas autorisé à supprimer cet utilisateur", + "users.delete.error.lastadmin": "Vous ne pouvez supprimer le dernier “admin”", + "users.avatar.drop": "Glisser ici un fichier image…", + "users.avatar.click": "…ou cliquer pour le transférer depuis votre ordinateur", + "users.avatar.error.type": "Vous pouvez uniquement transférer des fichiers JPG, PNG et GIF", + "users.avatar.error.folder.headline": "Le répertoire des avatars n’est pas accessible en écriture", + "users.avatar.error.folder.text": "Veuillez créer le répertoire /assets/avatars/ et en autoriser l'écriture pour pouvoir charger une image de profil.", + "users.avatar.error.permission": "Vous n’êtes pas autorisé à modifier l’avatar", + "users.avatar.delete.error": "L’image du profil n’a pu être supprimée", + "users.avatar.delete.error.permission": "Vous n’êtes pas autorisé à modifier l’avatar de cet utilisateur", + "users.avatar.delete.success": "L’image du profil a été supprimée", + "users.avatar.missing": "Cet utilisateur n’a pas d'avatar", + "users.error.missing": "Aucun compte utilisateur à ce nom n’a été trouvé", + "user.error.lastadmin": "Vous êtes le seul admin. Ceci ne peut être modifié.", + "form.error.missing": "Le formulaire n'a pu être trouvé", + "form.construct.error.invalid": "Méthode de construction de formulaire incorrecte", + "fields.required": "Requis", + "fields.date.label": "Date", + "fields.date.months": [ + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Août", + "Septembre", + "Octobre", + "Novembre", + "Décembre" + ], + "fields.date.weekdays": [ + "Dimanche", + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi" + ], + "fields.date.weekdays.short": [ + "Dim", + "Lun", + "Mar", + "Mer", + "Jeu", + "Ven", + "Sam" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemple.com", + "fields.number.label": "Numéro", + "fields.number.placeholder": "N°", + "fields.page.label": "Page", + "fields.page.placeholder": "chemin/vers/la/page", + "fields.password.label": "Mot de passe", + "fields.structure.add": "Ajouter", + "fields.structure.add.first": "Créer la première entrée", + "fields.structure.empty": "Aucune entrée pour le moment.", + "fields.structure.entry.error": "L’élément n’a pu être trouvé", + "fields.structure.cancel": "Annuler", + "fields.structure.save": "Valider", + "fields.structure.edit": "Modifier", + "fields.structure.delete": "Supprimer", + "fields.structure.delete.label": "Voulez-vous vraiment supprimer cette entrée ?", + "fields.tags.label": "Tags", + "fields.tel.label": "Téléphone", + "fields.textarea.buttons.bold.label": "Gras", + "fields.textarea.buttons.bold.text": "Texte en gras", + "fields.textarea.buttons.italic.label": "Italique", + "fields.textarea.buttons.italic.text": "Texte en italique", + "fields.textarea.buttons.link.label": "Lien", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Image", + "fields.textarea.buttons.file.label": "Fichier", + "fields.toggle.yes": "Oui", + "fields.toggle.no": "Non", + "fields.toggle.on": "Activé", + "fields.toggle.off": "Désactivé", + "fields.error.missing.controller": "Le fichier du contrôleur de champ est manquant", + "fields.error.missing.class": "La classe du contrôleur de champ est manquante", + "fields.error.route.invalid": "Routage du champ incorrect", + "fields.error.extended": "Le champ ne peut être étendu", + "editor.link.url.label": "Insérer une URL", + "editor.link.text.label": "Texte du lien", + "editor.link.text.help": "Le texte du lien est optionnel", + "editor.email.address.label": "Insérer une adresse email", + "editor.email.address.placeholder": "mail@exemple.com", + "editor.email.text.label": "Texte du lien", + "editor.email.text.help": "Le texte du lien est optionnel", + "editor.file.empty": "Cette page n’a aucun fichier attaché", + "editor.image.empty": "Cette page n’a aucune image attachée", + "autocomplete.method.error": "Méthode d’auto-complétion incorrecte", + "blueprints.error.default.missing": "Blueprint par défaut manquant", + "error": "Erreur", + "error.headline": "Erreur" +} \ No newline at end of file diff --git a/panel/app/translations/fr/package.json b/panel/app/translations/fr/package.json new file mode 100644 index 0000000..39ea6ab --- /dev/null +++ b/panel/app/translations/fr/package.json @@ -0,0 +1,4 @@ +{ + "title": "Français", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/hu/core.json b/panel/app/translations/hu/core.json new file mode 100644 index 0000000..5924566 --- /dev/null +++ b/panel/app/translations/hu/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Mégsem", + "add": "Hozzáad", + "addit": "Hozzáad és szerkeszt", + "save": "Mentés", + "saved": "Mentve!", + "change": "Módosítás", + "delete": "Törlés", + "insert": "Beilleszt", + "ok": "Ok", + "routes.error.invalid": "Érvénytelen Panel URL", + "controller.error.invalid": "Érvénytelen vezérlő", + "controller.error.action": "Érvénytelen művelet", + "view.error.invalid": "Érvénytelen nézet:", + "options.show": "Beállítások mutatása", + "options.hide": "Beállítások elrejtése", + "installation": "Telepítés", + "installation.check.headline": "Kirby Panel telepítés", + "installation.check.text": "Kirby az alábbi problémával találkozott…", + "installation.check.retry": "Újból", + "installation.check.error": "Van néhány probléma!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts nem írható", + "installation.check.error.avatars": "/assets/avatars nem írható", + "installation.check.error.blueprints": "Kérlek hozd létre a site/blueprints mappát", + "installation.check.error.content": "A content mappa és az összes tartalmazott mappának és fájlnak írhatónak kell lennie.", + "installation.check.error.thumbs": "A thumbs mappának írhatónak kell lennie.", + "installation.signup.username.label": "Első fiók létrehozása", + "installation.signup.username.placeholder": "Felhasználónév", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@példa.hu", + "installation.signup.password.label": "Jelszó", + "installation.signup.language.label": "Nyelv", + "installation.signup.button": "Fiókod létrehozása", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Felhasználónév", + "login.password.label": "Jelszó", + "login.error": "Érvénytelen felhasználónév vagy jelszó", + "login.button": "Log in", + "login.log.error.permissions": "A bejelentkezési logfájl nem írható", + "logout": "Log out", + "topbar.error.class.definition": "Ez az osztály nincs definiálva: ", + "dashboard": "Vezérlőközpont", + "dashboard.index.pages.title": "Oldalak", + "dashboard.index.pages.edit": "Oldal szerkesztése", + "dashboard.index.pages.add": "Új oldal", + "dashboard.index.site.title": "Honlapod URL-je", + "dashboard.index.account.title": "Fiókod", + "dashboard.index.account.edit": "Szerkesztés", + "dashboard.index.metatags.title": "Metatagok", + "dashboard.index.metatags.edit": "Szerkesztés", + "dashboard.index.history.title": "Legutóbbi frissítések", + "dashboard.index.history.text": "Az általad legutóbb módosított oldalakat találod itt, hogy könnyebben megtaláld újból.", + "dashboard.index.license.title": "Kirby licenc", + "dashboard.index.license.text": "Úgy tűnik a Kirby egy publikus szerveren fut érvényes licensz nélkül.\n\nKérlek támogasd a Kirby-t és (link: {buy} text: vásárold meg a licenszet most)\n\nHa már rendelkezel licensz kulccsal, csak add hozzá a konfig fájlodhoz: (link: {docs} text: site/config/config.php)", + "metatags": "Metatagok", + "metatags.info": "Kirby infó", + "metatags.license": "Kirby licenc", + "metatags.version.toolkit": "Toolkit verzió", + "metatags.version.kirby": "Kirby verzió", + "metatags.version.panel": "Panel verzió", + "metatags.back": "Vissza a vezerlőközpontba", + "metatags.files": "A weboldalhoz tartozó fájlok", + "site.delete.error": "A weboldal nem törölhető", + "pages.show.settings": "Oldal beállítások", + "pages.show.preview": "Előnézet megnyitása", + "pages.show.template": "Sablon", + "pages.show.changeurl": "URL változtatása", + "pages.show.invisible": "Állapot: rejtett", + "pages.show.visible": "Állapot: látható", + "pages.show.changes.text": "Nem mentett változtatások vannak!", + "pages.show.changes.button": "Visszavonás", + "pages.show.delete": "Oldal törlése", + "pages.show.subpages.title": "Aloldalak", + "pages.show.subpages.edit": "Aloldal szerkesztése", + "pages.show.subpages.add": "Új aloldal", + "pages.show.subpages.empty": "Az oldalhoz nem tartoznak aloldalak", + "pages.show.files.title": "Fájlok", + "pages.show.files.edit": "Fájl szerkesztése", + "pages.show.files.add": "Új hozzáadása", + "pages.show.files.empty": "Az oldalhoz nem tartoznak fájlok", + "pages.show.error.permissions.title": "Az oldal nem írható", + "pages.show.error.permissions.text": "Kérlek ellenőrizd a content mappa és a fájlok jogosultságát.", + "pages.show.error.permissions.retry": "Újból", + "pages.show.error.notitle.title": "Ennek a blueprintnek nincs cím mezője", + "pages.show.error.notitle.text": "Töltsd ki a cím mezőt és próbálkozz újból", + "pages.show.error.notitle.retry": "Újból", + "pages.show.error.form": "Kérlek minden mezőt tölts ki", + "pages.add.title.label": "Új aloldal hozzáadása", + "pages.add.title.placeholder": "Cím", + "pages.add.url.label": "URL név", + "pages.add.url.enter": "(add meg a címet)", + "pages.add.url.close": "Bezár", + "pages.add.url.help": "Formátum: kisbetűs a-z, 0-9 és kötőjel", + "pages.add.template.label": "Sablon", + "pages.add.error.create": "Az oldal nem hozható létre", + "pages.add.error.title": "Hiányzik a cím", + "pages.add.error.template": "A sablon hiányzik", + "pages.add.error.max.headline": "Új oldal látrehozása nem engedélyezett", + "pages.add.error.max.text": "Elérted a maximálisan létrehozható aloldalak számát.", + "pages.url.uid.label": "URL név", + "pages.url.uid.label.option": "Létrehozás címből", + "pages.url.error.exists": "Van már egy másik oldal ezzel az URL-lel", + "pages.url.error.move": "Az URL-t nem sikerült megváltoztatni", + "pages.url.error.rights": "Ennek az oldalnak nem tudod megváltoztatni az URL-jét", + "pages.template.select.label": "Sablon", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Elhelyezkedés", + "pages.toggle.invisible": "rejtett", + "pages.toggle.publish": "Biztos megváltoztatod az oldal állapotát erre: **látható**?", + "pages.toggle.hide": "Biztos megváltoztatod az oldal állapotát erre: **rejtett**?", + "pages.toggle.error.error": "A hibaoldal állapota nem módosítható", + "pages.delete.headline": "Biztos vagy benne, hogy törlöd az oldalt?", + "pages.delete.error.home.headline": "A főoldal nem törölhető", + "pages.delete.error.home.text": "Te a főoldalt próbálod törölni. Ez nem lehetséges és beláthatatlan következményei lennének.", + "pages.delete.error.error.headline": "A hiba oldal nem törölhető", + "pages.delete.error.error.text": "Te a hiba oldalt próbálod törölni. Ez nem lehetséges és beláthatatlan következményei lennének.", + "pages.delete.error.children.headline": "Az oldal nem törölhető", + "pages.delete.error.children.text": "Az oldalnak vannak aloldalai és nem törölhető.Előbb töröld az összes aloldalt.", + "pages.delete.error.blocked.headline": "Az oldal nem törölhető", + "pages.delete.error.blocked.text": "Ez az oldal zárolt és nem törölhető.", + "pages.search.help": "Keresés URL szerint.Navigálhatsz a fel és le nyilakkal és az enter-rel ugorhatsz a kiválasztott oldalhoz.", + "pages.search.noresults": "Amire kerestél, nincs találat. Kérlek próbáld meg egy másik URL-lel.", + "pages.error.missing": "Az oldal nem található", + "subpages": "Aloldalak", + "subpages.index.headline": "Aloldalak kezelése ennél:", + "subpages.index.back": "Vissza", + "subpages.index.add": "Új aloldal", + "subpages.index.add.first.text": "Ennek az oldalnak még nincs aloldala", + "subpages.index.add.first.button": "Az első aloldal hozzáadása", + "subpages.index.visible": "Látható aloldalak", + "subpages.index.visible.help": "Fogd meg jobbról az oldalt és rendezd vagy tedd láthatóvá.", + "subpages.index.invisible": "Láthatatlan aloldalak", + "subpages.index.invisible.help": "Fogd meg balról az oldalt és rendezd vagy tedd láthatatlanná.", + "subpages.add.error": "Ennek az oldalnak nem lehetnek aloldalai", + "subpages.add.error.more": "Ennek az oldalnak nem lehet több aloldala", + "subpages.error.missing": "Az oldal nem található", + "files": "Fájlok", + "files.index.headline": "Fájlkezelés ennél:", + "files.index.back": "Vissza", + "files.index.upload": "Új fájl feltöltése", + "files.index.upload.first.text": "Ennél az oldalnál még nincs fájl feltöltve", + "files.index.upload.first.button": "Töltsd fel az első fájlt", + "files.index.edit": "Szerkeszt", + "files.index.delete": "Töröl", + "files.index.error.disabled": "Ehhez az oldalhoz nem tartozhatnak fájlok", + "files.add.error.max": "Ehhez az oldalhoz nem tölthető fel a jelenleginél több fájl", + "files.add.error.extension.missing": "Kiterjesztés nélküli fájl nem tölthető fel", + "files.add.error.extension.forbidden": "Tiltott kiterjesztésű fájl", + "files.add.error.mime.forbidden": "Tiltott mime-típus", + "files.add.error.htaccess": "A htaccess fájlt nem lehet feltölteni", + "files.add.error.invisible": "Láthatatlan fájlok nem tölthetők fel", + "files.add.blueprint.type.error": "Az oldal az alábbiakat engedélyezi:", + "files.add.blueprint.size.error": "Az oldal által engedélyezett legnagyobb fájlméret", + "files.show.name.label": "Fájlnév", + "files.show.info.label": "Típus / fájlméret / méretek", + "files.show.link.label": "Publikus link", + "files.show.open": "Fájl megtekintése/letöltése", + "files.show.back": "Vissza", + "files.show.replace": "Cserél", + "files.show.delete": "Töröl", + "files.show.error.rename": "A fájl nem nevezhető át", + "files.show.error.form": "Kérlek minden mezőt tölts ki", + "files.upload.drop": "Dobd a fájlokat ide…", + "files.upload.click": "…vagy kattints ide a feltöltéshez", + "files.replace.drop": "Dobd a fájlt ide…", + "files.replace.click": "…vagy kattints ide a cseréhez", + "files.replace.error.type": "A feltöltött fájlnak azonos a típusa", + "files.delete.headline": "Biztos törölni akarod ezt a fájlt?", + "files.error.missing.page": "Az oldal nem található", + "files.error.missing.file": "A fájl nem található", + "users": "Felhasználók", + "users.index.headline": "Összes felhasználó", + "users.index.add": "Új felhasználó", + "users.index.edit": "szerkeszt", + "users.index.delete": "töröl", + "users.form.username.label": "Felhasználónév", + "users.form.username.placeholder": "Felhasználóneved", + "users.form.username.help": "Engedélyezett karakterek: kisbetűs a-z, 0-9 és kötőjel", + "users.form.username.readonly": "A felhasználónév nem változtatható meg", + "users.form.firstname.label": "Keresztnév", + "users.form.lastname.label": "Vezetéknév", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@pálda.hu", + "users.form.password.label": "Jelszó", + "users.form.password.confirm.label": "Jelszó megerősítése", + "users.form.password.new.label": "Új jelszó", + "users.form.password.new.confirm.label": "Az új jelszó megerősítése", + "users.form.password.new.help": "Hagyd üresen, ha a jelenlegi jelszót meg akarod tartani", + "users.form.language.label": "Nyelv", + "users.form.role.label": "Role", + "users.form.options.headline": "Fiók beállítások", + "users.form.options.message": "Email küldése", + "users.form.options.delete": "Fiók törlése", + "users.form.avatar.headline": "Profil kép", + "users.form.avatar.upload": "Profil kép feltöltése", + "users.form.avatar.replace": "Profil kép cseréje", + "users.form.avatar.delete": "Profil kép törlése", + "users.form.back": "Vissza a felhasználókhoz", + "users.form.error.password.confirm": "Kérlek erősítsd meg a jelszót", + "users.form.error.update": "A felhasználó nem frissíthető", + "users.form.error.update.rights": "Nincs jogosultságod a felhasználó módosításához", + "users.form.error.create": "A felhasználó nem hozható létre", + "users.form.error.permissions.title": "A account mappa nem írható", + "users.form.error.permissions.text": "Kérlek ellenőrizd, hogy a /site/accounts mappa létezik és írható.", + "users.delete.headline": "Biztos törlöd ezt a felhasználót?", + "users.delete.error": "A felhasználó nem törölhető", + "users.delete.error.permission": "Nincs jogosultságod felhasználók törléséhez", + "users.delete.error.permission.single": "Nincs jogosultságod törölni ezt a felhasználót", + "users.delete.error.lastadmin": "Nem törölheted az egyetlen adminisztrátort", + "users.avatar.drop": "Dobd a profil képet ide…", + "users.avatar.click": "…vagy kattints ide a feltöltéshez", + "users.avatar.error.type": "Csak JPG, PNG és GIF fájl tölthető fel", + "users.avatar.error.folder.headline": "Az avatar mappa nem írható", + "users.avatar.error.folder.text": "Kérlek hozd létre a /assets/avatars mappát és tedd írhatóvá a profil képek feltöltéséhez.", + "users.avatar.error.permission": "Nincs jogosultságod megváltoztatni a profilképet", + "users.avatar.delete.error": "A profil kép nem törölhető", + "users.avatar.delete.error.permission": "Nincs jogosultságod megváloztatni ennek a felhasználónak a profilképét", + "users.avatar.delete.success": "A profil kép törölve", + "users.avatar.missing": "Ennek a felhasználónak nincs profilképe", + "users.error.missing": "A felhasználó nem található", + "user.error.lastadmin": "Te vagy az egyetlen adminisztrátor. Ez a beállítás nem módosítható.", + "form.error.missing": "Az űrlap nem található", + "form.construct.error.invalid": "Érvénytelen űrlapfelépítés", + "fields.required": "Kötelező", + "fields.date.label": "Dátum", + "fields.date.months": [ + "január", + "február", + "március", + "április", + "május", + "június", + "július", + "augusztus", + "szeptember", + "október", + "november", + "december" + ], + "fields.date.weekdays": [ + "vasárnap", + "hétfő", + "kedd", + "szerda", + "csütörtök", + "péntek", + "szombat" + ], + "fields.date.weekdays.short": [ + "va", + "hé", + "ke", + "sze", + "csü", + "pé", + "szo" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@példa.hu", + "fields.number.label": "Szám", + "fields.number.placeholder": "#", + "fields.page.label": "Oldal", + "fields.page.placeholder": "az/oldal/elérése", + "fields.password.label": "Jelszó", + "fields.structure.add": "Új", + "fields.structure.add.first": "Első bejegyzés hozzáadása", + "fields.structure.empty": "Nincs még bejegyzés", + "fields.structure.entry.error": "Az elem nem található", + "fields.structure.cancel": "Mégse", + "fields.structure.save": "Mentés", + "fields.structure.edit": "Szerkeszt", + "fields.structure.delete": "Törlés", + "fields.structure.delete.label": "Biztos törölni szeretnéd ezt a bejegyzést?", + "fields.tags.label": "Címkék", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Félkövér szöveg", + "fields.textarea.buttons.bold.text": "Félkövér szöveg", + "fields.textarea.buttons.italic.label": "Dölt szöveg", + "fields.textarea.buttons.italic.text": "Dölt szöveg", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Kép", + "fields.textarea.buttons.file.label": "Fájl", + "fields.toggle.yes": "Igen", + "fields.toggle.no": "Nem", + "fields.toggle.on": "Be", + "fields.toggle.off": "Ki", + "fields.error.missing.controller": "A mezőszabályozó fájl nincs meg", + "fields.error.missing.class": "A mezőszabályozó osztály hiányzik", + "fields.error.route.invalid": "A mező elérési útvonala érvénytelen", + "fields.error.extended": "Ez a mező nem terjeszthető ki", + "editor.link.url.label": "URL beillesztése", + "editor.link.text.label": "Link szöveg", + "editor.link.text.help": "A link szövege nem kötelező", + "editor.email.address.label": "Email cím beillesztése", + "editor.email.address.placeholder": "mail@példa.hu", + "editor.email.text.label": "Link szöveg", + "editor.email.text.help": "A link szövege nem kötelező", + "editor.file.empty": "Az oldalnak nincsenek fájljai", + "editor.image.empty": "Az oldalnak nincsenek képei", + "autocomplete.method.error": "Érvénytelen auto-kiegészítés metódus", + "blueprints.error.default.missing": "Hiányzó alapértelmezett blueprint", + "error": "Hiba", + "error.headline": "Hiba" +} \ No newline at end of file diff --git a/panel/app/translations/hu/package.json b/panel/app/translations/hu/package.json new file mode 100644 index 0000000..d99d05e --- /dev/null +++ b/panel/app/translations/hu/package.json @@ -0,0 +1,4 @@ +{ + "title": "Hungarian", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/id/core.json b/panel/app/translations/id/core.json new file mode 100644 index 0000000..1a87bc5 --- /dev/null +++ b/panel/app/translations/id/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Batal", + "add": "Tambah", + "addit": "Tambah & Sunting", + "save": "Simpan", + "saved": "Telah disimpan!", + "change": "Change", + "delete": "Hapus", + "insert": "Sisip", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Tunjukan pilihan", + "options.hide": "Sembunyikan pilihan", + "installation": "Instalasi", + "installation.check.headline": "Instalasi Kirby Panel", + "installation.check.text": "Kirby menemukan beberapa masalah selagi menginstalasi…", + "installation.check.retry": "Coba lagi", + "installation.check.error": "Ada beberapa masalah!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts tidak bisa diakses", + "installation.check.error.avatars": "/assets/avatars tidak bisa diakses", + "installation.check.error.blueprints": "Tolong tambahkan folder /site/blueprints", + "installation.check.error.content": "The content folder and all contained files and folders must be writable.", + "installation.check.error.thumbs": "The thumbs folder must be writable.", + "installation.signup.username.label": "Buatlah akun pertama anda", + "installation.signup.username.placeholder": "Nama pengguna", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "email@contohemail.com", + "installation.signup.password.label": "Kata sandi", + "installation.signup.language.label": "Bahasa", + "installation.signup.button": "Buatlah akun anda", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nama pengguna", + "login.password.label": "Kata sandi", + "login.error": "Kata sandi atau username anda tidak terdaftar", + "login.button": "Log in", + "login.log.error.permissions": "Berkas log masuk tidak bisa disunting", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Halaman", + "dashboard.index.pages.edit": "Sunting", + "dashboard.index.pages.add": "Tambah", + "dashboard.index.site.title": "URL situs anda", + "dashboard.index.account.title": "Akun anda", + "dashboard.index.account.edit": "Sunting", + "dashboard.index.metatags.title": "Variabel situs", + "dashboard.index.metatags.edit": "Sunting", + "dashboard.index.history.title": "Update terakhir anda", + "dashboard.index.history.text": "Halaman terakhir yang anda ubah akan ditampilkan di sini supaya mudah untuk menemukannya lagi.", + "dashboard.index.license.title": "Lisensi Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variabel situs", + "metatags.info": "Info Kirby", + "metatags.license": "Lisensi Kirby", + "metatags.version.toolkit": "Versi Toolkit", + "metatags.version.kirby": "Versi Kirby", + "metatags.version.panel": "Versi Panel", + "metatags.back": "Kembali ke dashboard", + "metatags.files": "Berkas situs", + "site.delete.error": "Situs tidak bisa dihapus", + "pages.show.settings": "Pengaturan halaman", + "pages.show.preview": "Buka pratinjau", + "pages.show.template": "Template", + "pages.show.changeurl": "Ganti URL", + "pages.show.invisible": "Status: tak terlihat", + "pages.show.visible": "Status: terlihat", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "Hapus halaman ini", + "pages.show.subpages.title": "Halaman", + "pages.show.subpages.edit": "Sunting", + "pages.show.subpages.add": "Tambah", + "pages.show.subpages.empty": "Halaman ini tidak memiliki sub-halaman", + "pages.show.files.title": "Berkas", + "pages.show.files.edit": "Sunting", + "pages.show.files.add": "Tambah", + "pages.show.files.empty": "Halaman ini tidak memiliki berkas", + "pages.show.error.permissions.title": "Halaman ini tidak bisa disunting", + "pages.show.error.permissions.text": "Tolong periksa hak akses untuk file dan folder.", + "pages.show.error.permissions.retry": "Coba lagi", + "pages.show.error.notitle.title": "Blueprint ini tidak memiliki bidang untuk 'title'", + "pages.show.error.notitle.text": "Tolong tambahkan bidang untuk 'title' dan coba lagi", + "pages.show.error.notitle.retry": "Coba lagi", + "pages.show.error.form": "Tolong isi semua bidang dengan benar", + "pages.add.title.label": "Tambah halaman baru", + "pages.add.title.placeholder": "Judul", + "pages.add.url.label": "URL-lampiran", + "pages.add.url.enter": "(masukkan judul anda)", + "pages.add.url.close": "Tutup", + "pages.add.url.help": "Format: huruf kecil a-z, 0-9 dan regular dashes", + "pages.add.template.label": "Template", + "pages.add.error.create": "Halaman tidak dapat dibuat", + "pages.add.error.title": "Judul tidak ditemukan", + "pages.add.error.template": "Template tidak ditemukan", + "pages.add.error.max.headline": "Halaman baru tidak diperbolehkan", + "pages.add.error.max.text": "Jumlah maksimum sub-halaman telah tercapai.", + "pages.url.uid.label": "URL-lampiran", + "pages.url.uid.label.option": "Buat dari judul", + "pages.url.error.exists": "Halaman dengan lampiran yang sama sudah ada", + "pages.url.error.move": "Lampiran tidak bisa diubah", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posisi", + "pages.toggle.invisible": "Status: tak terlihat", + "pages.toggle.publish": "Apakah anda benar-benar mau mengganti status halaman ini menjadi **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Apakah anda benar-benar mau menghapus halaman ini?", + "pages.delete.error.home.headline": "Beranda tidak bisa dihapus", + "pages.delete.error.home.text": "Anda sedang mencoba menghapus beranda. Hal ini tidak mungkin dan akan menyebabkan efek yang tidak diinginkan.", + "pages.delete.error.error.headline": "Halaman error tidak bisa dihapus", + "pages.delete.error.error.text": "Anda sedang mencoba menghapus halaman error. Hal ini tidak mungkin dan akan menyebabkan efek yang tidak diinginkan.", + "pages.delete.error.children.headline": "Halaman ini tidak bisa dihapus", + "pages.delete.error.children.text": "Halaman ini memiliki sub-halaman dan tidak bisa dihapus. Tolong hapus sub-halaman terdahulu.", + "pages.delete.error.blocked.headline": "Halaman ini tidak bisa dihapus", + "pages.delete.error.blocked.text": "Halaman ini dikunci dan tidak bisa dihapus.", + "pages.search.help": "Cari halaman dengan URL. Navigasi hasil pencarian dengan tombol atas dan bawah kemudian tekan tombol enter untuk melompat ke halaman yang dipilih.", + "pages.search.noresults": "Tidak ada hasil pencarian untuk permintaan anda. Silakan coba lagi dengan URL yang berbeda.", + "pages.error.missing": "Halaman yang anda cari tidak bisa ditemukan.", + "subpages": "Halaman", + "subpages.index.headline": "Halaman yang ada didalam", + "subpages.index.back": "Kembali", + "subpages.index.add": "Tambah halaman baru", + "subpages.index.add.first.text": "Halaman ini belum memiliki sub-halaman", + "subpages.index.add.first.button": "Tambah halaman pertama", + "subpages.index.visible": "Halaman terlihat", + "subpages.index.visible.help": "Seretlah halaman tersembunyi ke sini untuk menyortir atau membuat halaman terlihat", + "subpages.index.invisible": "Halaman tersembunyi", + "subpages.index.invisible.help": "Seretlah halaman terlihat ke sini untuk menyortir atau membuat halaman tersembunyi.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Halaman tidak bisa ditemukan", + "files": "Berkas", + "files.index.headline": "Berkas untuk", + "files.index.back": "Kembali", + "files.index.upload": "Unggah berkas baru", + "files.index.upload.first.text": "Halaman ini belum memiliki berkas", + "files.index.upload.first.button": "Unggah berkas pertama", + "files.index.edit": "Sunting", + "files.index.delete": "Hapus", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Nama berkas", + "files.show.info.label": "Tipe / Ukuran / Dimensi", + "files.show.link.label": "Link publik", + "files.show.open": "Tampilkan atau download file", + "files.show.back": "Kembali", + "files.show.replace": "Ganti", + "files.show.delete": "Hapus", + "files.show.error.rename": "Nama berkas tidak bisa diganti", + "files.show.error.form": "Tolong isi semua bidang dengan benar", + "files.upload.drop": "Drop file disini…", + "files.upload.click": "…atau tekan untuk meng-upload", + "files.replace.drop": "Drop file disini…", + "files.replace.click": "…atau tekan untuk mengganti", + "files.replace.error.type": "Berkas yang diunggah harus memiliki tipe yang sama.", + "files.delete.headline": "Apakah anda benar-benar mau menghapus file ini?", + "files.error.missing.page": "Halaman tidak bisa ditemukan", + "files.error.missing.file": "File tidak bisa ditemukan", + "users": "Pengguna", + "users.index.headline": "Semua pengguna", + "users.index.add": "Tambah pengguna baru", + "users.index.edit": "Sunting", + "users.index.delete": "Hapus", + "users.form.username.label": "Nama pengguna", + "users.form.username.placeholder": "Nama pengguna anda", + "users.form.username.help": "Karakter yang diperbolehkan: huruf kecil a-z, 0-9 dan strip", + "users.form.username.readonly": "Nama pengguna tidak bisa diganti", + "users.form.firstname.label": "Nama depan", + "users.form.lastname.label": "Nama keluarga", + "users.form.email.label": "Email", + "users.form.email.placeholder": "email@contohemail.com", + "users.form.password.label": "Kata sandi", + "users.form.password.confirm.label": "Konfirmasi kata sandi", + "users.form.password.new.label": "Kata sandi baru", + "users.form.password.new.confirm.label": "Konfirmasi kata sandi yang baru", + "users.form.password.new.help": "Biarkan kosong untuk menyimpan kata sandi yang sekarang", + "users.form.language.label": "Bahasa", + "users.form.role.label": "Peran", + "users.form.options.headline": "Opsi akun", + "users.form.options.message": "Kirim email", + "users.form.options.delete": "Hapus akun", + "users.form.avatar.headline": "Gambar profil", + "users.form.avatar.upload": "Unggah gambar profil", + "users.form.avatar.replace": "Ganti gambar profil", + "users.form.avatar.delete": "Hapus gambar profil", + "users.form.back": "Kembali ke pengguna", + "users.form.error.password.confirm": "Tolong konfirmasi kata sandi", + "users.form.error.update": "Pengguna tidak bisa di-update", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Pengguna tidak bisa dibuat", + "users.form.error.permissions.title": "Folder 'account' tidak bisa diakses", + "users.form.error.permissions.text": "Tolong pastikan bahwa /site/accounts ada dan bisa diakses.", + "users.delete.headline": "Apakah anda benar-benar mau menghapus pengguna ini?", + "users.delete.error": "Pengguna ini tidak bisa dihapus", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Drop profil foto disini…", + "users.avatar.click": "…or atau tekan untuk meng-upload", + "users.avatar.error.type": "Anda hanya bisa meng-upload file dengan tipe JPG, PNG dan GIF", + "users.avatar.error.folder.headline": "Folder avatar tidak bisa diakses", + "users.avatar.error.folder.text": "Tolong buat folder /assets/avatars dan berikan hak akses untuk mengupload profil foto.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Profil fotonya tidak bisa dihapus", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Profil fotonya telah dihapus", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Pengguna tidak bisa ditemukan", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Diperlukan", + "fields.date.label": "Tanggal", + "fields.date.months": [ + "Januari", + "Februari", + "Maret", + "April", + "Mei", + "Juni", + "Juli", + "Agustus", + "September", + "Oktober", + "November", + "Desember" + ], + "fields.date.weekdays": [ + "Minggu", + "Senin", + "Selasa", + "Rabu", + "Kamis", + "Jum'at", + "Sabtu" + ], + "fields.date.weekdays.short": [ + "Min", + "Sen", + "Sel", + "Rab", + "Kam", + "Jum", + "Sab" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Nomor", + "fields.number.placeholder": "#", + "fields.page.label": "Halaman", + "fields.page.placeholder": "path/ke/halaman", + "fields.password.label": "Kata sandi", + "fields.structure.add": "Tambah", + "fields.structure.add.first": "Tambahkan entri pertama", + "fields.structure.empty": "Belum ada entri", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Batal", + "fields.structure.save": "Simpan", + "fields.structure.edit": "Sunting", + "fields.structure.delete": "Hapus", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telepon", + "fields.textarea.buttons.bold.label": "Teks tebal", + "fields.textarea.buttons.bold.text": "Teks tebal", + "fields.textarea.buttons.italic.label": "Teks miring", + "fields.textarea.buttons.italic.text": "Teks miring", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Gambar", + "fields.textarea.buttons.file.label": "Berkas", + "fields.toggle.yes": "Ya", + "fields.toggle.no": "Tidak", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Masukkan URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "Link text opsional", + "editor.email.address.label": "Masukkan alamat email", + "editor.email.address.placeholder": "email@contohemail.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "Link text opsional", + "editor.file.empty": "Halaman ini tidak memiliki file", + "editor.image.empty": "Halaman ini tidak memiliki gambar", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Error", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/id/package.json b/panel/app/translations/id/package.json new file mode 100644 index 0000000..967ccfc --- /dev/null +++ b/panel/app/translations/id/package.json @@ -0,0 +1,4 @@ +{ + "title": "Bahasa Indonesia", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/it/core.json b/panel/app/translations/it/core.json new file mode 100644 index 0000000..6857e61 --- /dev/null +++ b/panel/app/translations/it/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annulla", + "add": "Aggiungi", + "addit": "Aggiungi e Modifica ", + "save": "Salva", + "saved": "Salvato!", + "change": "Cambia", + "delete": "Elimina", + "insert": "Inserisci", + "ok": "Ok", + "routes.error.invalid": "URL del Panel non valido", + "controller.error.invalid": "Controller non valido", + "controller.error.action": "Azione non valida", + "view.error.invalid": "Vista non valida", + "options.show": "Mostra opzioni", + "options.hide": "Nascondi opzioni", + "installation": "Installazione", + "installation.check.headline": "Installazione di Kirby Panel", + "installation.check.text": "Kirby ha riscontrato i seguenti problemi durante l'installazione…", + "installation.check.retry": "Riprova", + "installation.check.error": "Sono stati riscontrati dei problemi!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts non dispone dei permessi di scrittura", + "installation.check.error.avatars": "/assets/avatars non dispone dei permessi di scrittura", + "installation.check.error.blueprints": "Aggiungi la cartella /site/blueprints", + "installation.check.error.content": "La cartella /content e tutti i file e le cartelle in essa contenuti devono disporre dei permessi di scrittura.", + "installation.check.error.thumbs": "La cartella /thumbs deve disporre dei permessi di scrittura.", + "installation.signup.username.label": "Crea il tuo primo profilo", + "installation.signup.username.placeholder": "Nome utente", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@esempio.com", + "installation.signup.password.label": "Password", + "installation.signup.language.label": "Lingua", + "installation.signup.button": "Crea un nuovo profilo", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Username", + "login.password.label": "Password", + "login.error": "Username o password non validi", + "login.button": "Log in", + "login.log.error.permissions": "Il file di log degli accessi non è scrivibile.", + "logout": "Log out", + "topbar.error.class.definition": "Manca la definizione topbar per la classe:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pagine", + "dashboard.index.pages.edit": "Modifica", + "dashboard.index.pages.add": "Aggiungi", + "dashboard.index.site.title": "URL del sito", + "dashboard.index.account.title": "Il tuo account", + "dashboard.index.account.edit": "Modifica", + "dashboard.index.metatags.title": "Informazioni sul sito", + "dashboard.index.metatags.edit": "Modifica", + "dashboard.index.history.title": "Le tue ultime modifiche", + "dashboard.index.history.text": "Le ultime pagine modificate verranno mostrate qui per permetterti di ritrovarle più facilmente.", + "dashboard.index.license.title": "Licenza di Kirby", + "dashboard.index.license.text": "Sembra che stai usando Kirby su un server pubblico senza una licenza valida!\n\nPer favore, supporta Kirby (link: {buy} text: comprando subito una licenza)\n\nSe già possiedi una licenza, basta aggiungerla al tuo file di configurazione: (link: {docs} text: site/config/config.php)", + "metatags": "Informazioni sul sito", + "metatags.info": "Kirby info", + "metatags.license": "Licenza di Kirby", + "metatags.version.toolkit": "Versione del Toolkit", + "metatags.version.kirby": "Versione di Kirby", + "metatags.version.panel": "Versione del Panel", + "metatags.back": "Torna alla dashboard", + "metatags.files": "File del sito", + "site.delete.error": "Il sito non può essere rimosso", + "pages.show.settings": "Impostazioni della pagina", + "pages.show.preview": "Anteprima", + "pages.show.template": "Template", + "pages.show.changeurl": "Modifica URL", + "pages.show.invisible": "Status: invisibile", + "pages.show.visible": "Status: visibile", + "pages.show.changes.text": "Ci sono modifiche non salvate!", + "pages.show.changes.button": "Abbandona", + "pages.show.delete": "Elimina questa pagina", + "pages.show.subpages.title": "Pagine", + "pages.show.subpages.edit": "Modifica", + "pages.show.subpages.add": "Aggiungi", + "pages.show.subpages.empty": "Questa pagina non ha sottopagine", + "pages.show.files.title": "File", + "pages.show.files.edit": "Modifica", + "pages.show.files.add": "Aggiungi", + "pages.show.files.empty": "Questa pagina non ha file", + "pages.show.error.permissions.title": "Questa pagina non dispone dei permessi di scrittura", + "pages.show.error.permissions.text": "Verifica i permessi della cartella /content e di tutto il suo contenuto.", + "pages.show.error.permissions.retry": "Riprova", + "pages.show.error.notitle.title": "Nel modello non è previsto un campo per il titolo", + "pages.show.error.notitle.text": "Aggiungi un campo titolo e riprova", + "pages.show.error.notitle.retry": "Riprova", + "pages.show.error.form": "Compila tutti i campi correttamente", + "pages.add.title.label": "Aggiungi una nuova pagina", + "pages.add.title.placeholder": "Titolo", + "pages.add.url.label": "URL", + "pages.add.url.enter": "(inserisci un titolo)", + "pages.add.url.close": "Chiudi", + "pages.add.url.help": "Formato: minuscole a-z, 0-9 e trattini", + "pages.add.template.label": "Template", + "pages.add.error.create": "La pagina non può essere creata", + "pages.add.error.title": "Il titolo è vuoto", + "pages.add.error.template": "Il template non è stato trovato", + "pages.add.error.max.headline": "Non è consentita l'aggiunta di nuove pagine", + "pages.add.error.max.text": "Il numero massimo di sottopagine per questa pagina è stato raggiunto.", + "pages.url.uid.label": "URL", + "pages.url.uid.label.option": "Crea in base al titolo", + "pages.url.error.exists": "Esiste già una pagina con lo stesso URL", + "pages.url.error.move": "L'URL non può essere modificato", + "pages.url.error.rights": "Non puoi cambiare l'URL di questa pagina", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posizione", + "pages.toggle.invisible": "invisibile", + "pages.toggle.publish": "Vuoi veramente cambiare lo status della pagina a **visibile?**", + "pages.toggle.hide": "Vuoi veramente cambiare lo status della pagina a **invisibile?**", + "pages.toggle.error.error": "Lo status della pagina d'errore non può essere cambiato", + "pages.delete.headline": "Sei sicuro di voler eliminare questa pagina?", + "pages.delete.error.home.headline": "La pagina home non può essere eliminata", + "pages.delete.error.home.text": "Stai cercando di eliminare la pagina home. Quest'operazione non è possibile e potrebbe causare effetti indesiderati.", + "pages.delete.error.error.headline": "La pagina di errore non può essere eliminata", + "pages.delete.error.error.text": "Stai cercando di eliminare la pagina di errore. Quest'operazione non è possibile e potrebbe causare effetti indesiderati.", + "pages.delete.error.children.headline": "La pagina non può essere eliminata", + "pages.delete.error.children.text": "Questa pagina ha delle sottopagine e quindi non può essere eliminata. Elimina prima tutte le sottopagine.", + "pages.delete.error.blocked.headline": "La pagina non può essere eliminata", + "pages.delete.error.blocked.text": "Questa pagina è bloccata e non può essere eliminata.", + "pages.search.help": "Cerca le pagine indicando l'URL. Naviga tra i risultati della ricerca con i tasti freccia e premi invio per visualizzare la pagina selezionata.", + "pages.search.noresults": "La tua ricerca non ha dato risultati. Prova di nuovo con un URL differente.", + "pages.error.missing": "La pagina non è stata trovata", + "subpages": "Pagine", + "subpages.index.headline": "Pagine in", + "subpages.index.back": "Indietro", + "subpages.index.add": "Aggiungi nuova pagina", + "subpages.index.add.first.text": "Questa pagina non ha ancora sottopagine", + "subpages.index.add.first.button": "Aggiungi la prima pagina", + "subpages.index.visible": "Pagine visibili", + "subpages.index.visible.help": "Trascina qui una pagina nascosta per riordinarla/renderla visibile.", + "subpages.index.invisible": "Pagine nascoste", + "subpages.index.invisible.help": "Trascina qui una pagina visibile per nasconderla.", + "subpages.add.error": "A questa pagina non è consentito avere altre sottopagine", + "subpages.add.error.more": "Questa pagina non può avere altre sottopagine", + "subpages.error.missing": "La pagina non è stata trovata", + "files": "Files", + "files.index.headline": "Files associati a", + "files.index.back": "Indietro", + "files.index.upload": "Carica un nuovo file", + "files.index.upload.first.text": "Questa pagina non contiene file", + "files.index.upload.first.button": "Carica il primo file", + "files.index.edit": "Modifica", + "files.index.delete": "Elimina", + "files.index.error.disabled": "Alla pagina non è consentito avere nessun file", + "files.add.error.max": "Il numero massimo di file per l'utente corrente è stato raggiunto.", + "files.add.error.extension.missing": "Non puoi caricare file senza estensione", + "files.add.error.extension.forbidden": "Estensione non consentita", + "files.add.error.mime.forbidden": "Mime type non consentito", + "files.add.error.htaccess": "Il file htaccess non può essere caricato", + "files.add.error.invisible": "I file invisibili non possono essere caricati", + "files.add.blueprint.type.error": "La pagina consente solo:", + "files.add.blueprint.size.error": "La pagina permette una dimensione del file di", + "files.show.name.label": "Nome del file", + "files.show.info.label": "Tipo / Dimensione / Misure", + "files.show.link.label": "Link pubblico", + "files.show.open": "Mostra/scarica file", + "files.show.back": "Indietro", + "files.show.replace": "Sostituisci", + "files.show.delete": "Elimina", + "files.show.error.rename": "Non è stato possibile rinominare il file", + "files.show.error.form": "Compila tutti i campi correttamente", + "files.upload.drop": "Trascina un file qui…", + "files.upload.click": "…o clicca per caricarne uno", + "files.replace.drop": "Trascina un file qui…", + "files.replace.click": "…o clicca per caricarne uno", + "files.replace.error.type": "Il file caricato deve avere lo stesso formato", + "files.delete.headline": "Sei sicuro di voler eliminare questo file?", + "files.error.missing.page": "La pagina non è stata trovata", + "files.error.missing.file": "Il file non è stato trovato", + "users": "Utenti", + "users.index.headline": "Tutti gli utenti", + "users.index.add": "Aggiungi nuovo utente", + "users.index.edit": "Modifica", + "users.index.delete": "Elimina", + "users.form.username.label": "Nome utente", + "users.form.username.placeholder": "Il tuo nome utente", + "users.form.username.help": "Caratteri consentiti: minuscole a-z, 0-9 e trattini", + "users.form.username.readonly": "Il nome utente non può essere modificato", + "users.form.firstname.label": "Nome", + "users.form.lastname.label": "Cognome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@esempio.com", + "users.form.password.label": "Password", + "users.form.password.confirm.label": "Conferma password", + "users.form.password.new.label": "Nuova password", + "users.form.password.new.confirm.label": "Conferma la nuova password", + "users.form.password.new.help": "Lasciare vuoto per mantenere la password corrente", + "users.form.language.label": "Lingua", + "users.form.role.label": "Ruolo", + "users.form.options.headline": "Opzioni account", + "users.form.options.message": "Invia email", + "users.form.options.delete": "Elimina account", + "users.form.avatar.headline": "Immagine del profilo", + "users.form.avatar.upload": "Carica immagine del profilo", + "users.form.avatar.replace": "Sostituisci immagine del profilo", + "users.form.avatar.delete": "Elimina immagine del profilo", + "users.form.back": "Torna agli utenti", + "users.form.error.password.confirm": "Conferma la password", + "users.form.error.update": "Il profilo dell'utente non può essere aggiornato", + "users.form.error.update.rights": "Non ti è permesso aggiornare questo utente", + "users.form.error.create": "Il profilo dell'utente non può essere creato", + "users.form.error.permissions.title": "La cartella degli account non dispone dei permessi di scrittura.", + "users.form.error.permissions.text": "Verifica che la cartella /site/accounts esista e disponga dei permessi di scrittura.", + "users.delete.headline": "Sei sicuro di voler eliminare questo utente?", + "users.delete.error": "L'utente non può essere eliminato", + "users.delete.error.permission": "Non ti è permesso eliminare gli utenti", + "users.delete.error.permission.single": "Non ti è permesso eliminare questo utente ", + "users.delete.error.lastadmin": "Non puoi eliminare l'ultimo amministratore", + "users.avatar.drop": "Trascina qui un'immagine per il profilo…", + "users.avatar.click": "…o clicca per caricarne una", + "users.avatar.error.type": "Puoi solo caricare files in formato JPG, PNG o GIF.", + "users.avatar.error.folder.headline": "La cartella delle immagini di profilo non ha i permessi di scrittura", + "users.avatar.error.folder.text": "Crea la cartella /assets/avatars e assegnale i permessi di scrittura per caricare le immagini dei profili.", + "users.avatar.error.permission": "Non ti è permesso di cambiare l'avatar", + "users.avatar.delete.error": "L'immagine del profilo non è potuta essere eliminata.", + "users.avatar.delete.error.permission": "Non ti è permesso di eliminare l'avatar di questo utente", + "users.avatar.delete.success": "L'immagine del profilo è stata eliminata", + "users.avatar.missing": "Questo utente non ha un avatar", + "users.error.missing": "L'utente non è stato trovato", + "user.error.lastadmin": "Tu sei l'unico amministratore. Questa voce non può essere cambiata.", + "form.error.missing": "Il form non esiste", + "form.construct.error.invalid": "Il metodo construction del form è invalido ", + "fields.required": "Campo obbligatorio", + "fields.date.label": "Data", + "fields.date.months": [ + "Gennaio", + "Febbraio", + "Marzo", + "Aprile", + "Maggio", + "Giugno", + "Luglio", + "Agosto", + "Settembre", + "Ottobre", + "Novembre", + "Dicembre" + ], + "fields.date.weekdays": [ + "Domenica", + "Lunedì", + "Martedì", + "Mercoledì", + "Giovedì", + "Venerdì", + "Sabato" + ], + "fields.date.weekdays.short": [ + "Do", + "Lu", + "Ma", + "Me", + "Gi", + "Ve", + "Sa" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@esempio.com", + "fields.number.label": "Numero", + "fields.number.placeholder": "#", + "fields.page.label": "Pagina", + "fields.page.placeholder": "percorso/alla/pagina", + "fields.password.label": "Password", + "fields.structure.add": "Aggiungi", + "fields.structure.add.first": "Aggiungi il primo elemento", + "fields.structure.empty": "Non ci sono ancora elementi.", + "fields.structure.entry.error": "L'elemento non esiste", + "fields.structure.cancel": "Annulla", + "fields.structure.save": "Salva", + "fields.structure.edit": "Modifica", + "fields.structure.delete": "Elimina", + "fields.structure.delete.label": "Vuoi veramente eliminare questo elemento?", + "fields.tags.label": "Tag", + "fields.tel.label": "Telefono", + "fields.textarea.buttons.bold.label": "Testo in grassetto", + "fields.textarea.buttons.bold.text": "Testo in grassetto", + "fields.textarea.buttons.italic.label": "Testo in corsivo", + "fields.textarea.buttons.italic.text": "Testo in corsivo", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Immagine", + "fields.textarea.buttons.file.label": "File", + "fields.toggle.yes": "Sì", + "fields.toggle.no": "No", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "Manca il file controller del campo", + "fields.error.missing.class": "Manca la classe controller del campo", + "fields.error.route.invalid": "Route del campo invalida", + "fields.error.extended": "Il campo non può essere esteso", + "editor.link.url.label": "Link", + "editor.link.text.label": "Testo del link", + "editor.link.text.help": "Il testo del link è opzionale", + "editor.email.address.label": "Email", + "editor.email.address.placeholder": "mail@esempio.com", + "editor.email.text.label": "Testo del link", + "editor.email.text.help": "Il testo del link è opzionale", + "editor.file.empty": "In questa pagina non ci sono files", + "editor.image.empty": "In questa pagina non ci sono immagini", + "autocomplete.method.error": "Metodo di autocompletamento invalido", + "blueprints.error.default.missing": "Manca il blueprint di default", + "error": "Errore", + "error.headline": "Errore" +} \ No newline at end of file diff --git a/panel/app/translations/it/package.json b/panel/app/translations/it/package.json new file mode 100644 index 0000000..89467f8 --- /dev/null +++ b/panel/app/translations/it/package.json @@ -0,0 +1,4 @@ +{ + "title": "Italiano", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ja/core.json b/panel/app/translations/ja/core.json new file mode 100644 index 0000000..23aaa52 --- /dev/null +++ b/panel/app/translations/ja/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "キャンセル", + "add": "追加", + "addit": "Add & Edit", + "save": "保存", + "saved": "保存しました", + "change": "更新", + "delete": "削除", + "insert": "挿入", + "ok": "Ok", + "routes.error.invalid": "パネルのURLが間違っています。", + "controller.error.invalid": "不正な controlle です", + "controller.error.action": "不正な actio です", + "view.error.invalid": "不正な view です", + "options.show": "オプションを表示", + "options.hide": "オプションを隠す", + "installation": "インストール", + "installation.check.headline": "Kirby管理パネルのインストール", + "installation.check.text": "Kirbyのインストールプロセスで以下の問題が発生しました", + "installation.check.retry": "リトライ", + "installation.check.error": "次の項目を確認してください。", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts に書き込み権限がありません", + "installation.check.error.avatars": "/assets/avatars に書き込み権限がありません", + "installation.check.error.blueprints": "/site/blueprints フォルダがありません", + "installation.check.error.content": "content フォルダとその中のフォルダ/ファイルは全て書き込み権限を与えてください", + "installation.check.error.thumbs": "thumbs フォルダに書き込み権限がありません", + "installation.signup.username.label": "最初のアカウントを作成します", + "installation.signup.username.placeholder": "ユーザーID", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "パスワード", + "installation.signup.language.label": "表示言語", + "installation.signup.button": "アカウントを作成", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "ユーザーID", + "login.password.label": "パスワード", + "login.error": "ユーザーID または パスワード が正しくありません", + "login.button": "Log in", + "login.log.error.permissions": "ログインログファイルに書き込み権限がありません", + "logout": "Log out", + "topbar.error.class.definition": "トップバーのクラス定義ファイルがありません", + "dashboard": "ダッシュボード", + "dashboard.index.pages.title": "ページ", + "dashboard.index.pages.edit": "編集", + "dashboard.index.pages.add": "作成", + "dashboard.index.site.title": "WebサイトURL", + "dashboard.index.account.title": "アカウント", + "dashboard.index.account.edit": "編集", + "dashboard.index.metatags.title": "サイト変数", + "dashboard.index.metatags.edit": "編集", + "dashboard.index.history.title": "最近編集したページ", + "dashboard.index.history.text": "最近編集したページの履歴がここに表示されます。", + "dashboard.index.license.title": "Kirbyのライセンス", + "dashboard.index.license.text": "現在 Kirby はライセンス登録なしにパブリックサーバーにインストールされています。\n\nPlease, support Kirby and (link: {buy} text: ライセンスの購入)により Kirby のサポートをお願いします。\n\nライセンスキーをお持ちでしたら設定ファイル((link: {docs} text: site/config/config.php))に記入してください。", + "metatags": "サイト変数", + "metatags.info": "Kirbyの情報", + "metatags.license": "Kirbyのライセンス", + "metatags.version.toolkit": "Toolkitのバージョン", + "metatags.version.kirby": "Kirbyのバージョン", + "metatags.version.panel": "管理パネルのバージョン", + "metatags.back": "ダッシュボードに戻る", + "metatags.files": "サイトファイル", + "site.delete.error": "サイトは削除できません", + "pages.show.settings": "ページアクション", + "pages.show.preview": "プレビュー", + "pages.show.template": "テンプレート", + "pages.show.changeurl": "ページ識別子(URL)", + "pages.show.invisible": "状態: 管理対象外", + "pages.show.visible": "状態: 管理対象", + "pages.show.changes.text": "保存されていない編集箇所があります!", + "pages.show.changes.button": "編集内容を破棄", + "pages.show.delete": "このページを削除", + "pages.show.subpages.title": "サブページ", + "pages.show.subpages.edit": "編集", + "pages.show.subpages.add": "作成", + "pages.show.subpages.empty": "サブページはありません", + "pages.show.files.title": "ファイル", + "pages.show.files.edit": "編集", + "pages.show.files.add": "アップロード", + "pages.show.files.empty": "ファイルはありません", + "pages.show.error.permissions.title": "書き込み権限がありません", + "pages.show.error.permissions.text": "content フォルダとその中のフォルダ/ファイルは全て書き込み権限を与えてください", + "pages.show.error.permissions.retry": "リトライ", + "pages.show.error.notitle.title": "定義ファイルに title フィールドが定義されていません", + "pages.show.error.notitle.text": "title フィールドを定義した上でリトライしてください", + "pages.show.error.notitle.retry": "リトライ", + "pages.show.error.form": "入力が正しくありません", + "pages.add.title.label": "新規ページ", + "pages.add.title.placeholder": "タイトル", + "pages.add.url.label": "ページ識別子(URL)", + "pages.add.url.enter": "(このページのURLに使用されます)", + "pages.add.url.close": "閉じる", + "pages.add.url.help": "使用可能な文字: 英数小文字 a-z, 0-9 と ハイフン ( - )", + "pages.add.template.label": "テンプレート", + "pages.add.error.create": "ページを作成できませんでした", + "pages.add.error.title": "タイトルが空欄です", + "pages.add.error.template": "テンプレートを指定してください", + "pages.add.error.max.headline": "ページを作成できません", + "pages.add.error.max.text": "上限に達しましたので、このページにはこれ以上サブページを作成できませんでした", + "pages.url.uid.label": "ページ識別子(URL)", + "pages.url.uid.label.option": "ページ名から生成", + "pages.url.error.exists": "同じページ識別子(URL)を持つページが既に存在しています", + "pages.url.error.move": "ページ識別子(URL)を変更できませんでした", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "テンプレート", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "ページ順序", + "pages.toggle.invisible": "管理対象外", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "このページを削除してもよろしいですか?", + "pages.delete.error.home.headline": "The home page cannot be deleted", + "pages.delete.error.home.text": "You are trying to delete the home page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.error.headline": "The error page cannot be deleted", + "pages.delete.error.error.text": "You are trying to delete the error page. This is not possible and would lead to unwanted effects.", + "pages.delete.error.children.headline": "The page cannot be deleted", + "pages.delete.error.children.text": "This page has subpages and cannot be deleted. Please delete all subpages first.", + "pages.delete.error.blocked.headline": "The page cannot be deleted", + "pages.delete.error.blocked.text": "This page is locked and cannot be deleted.", + "pages.search.help": "Search pages by URL. Navigate through search results with your up and down arrow keys and hit enter to jump to the selected page.", + "pages.search.noresults": "There are no search results for your query. Please try again with a different URL.", + "pages.error.missing": "The page could not be found", + "subpages": "ページ", + "subpages.index.headline": "ページの一覧:", + "subpages.index.back": "戻る", + "subpages.index.add": "新規ページ", + "subpages.index.add.first.text": "このページにはサブページがありません", + "subpages.index.add.first.button": "サブページを作成", + "subpages.index.visible": "管理対象のページ", + "subpages.index.visible.help": "管理対象外のページをここにドラッグアンドドロップで管理対象に設定されます。ソート順は設定ファイルに準じます。", + "subpages.index.invisible": "管理対象外のページ", + "subpages.index.invisible.help": "管理対象のページをここにドラッグアンドドロップで管理対象外に設定されます。ソート順はクリアされます。", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "ページが見つかりませんでした", + "files": "ファイル", + "files.index.headline": "ファイルの一覧:", + "files.index.back": "戻る", + "files.index.upload": "ファイルのアップロード", + "files.index.upload.first.text": "このページにはファイルがありません", + "files.index.upload.first.button": "ファイルのアップロード", + "files.index.edit": "編集", + "files.index.delete": "削除", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "ファイル名", + "files.show.info.label": "Type / Size / Dimensions", + "files.show.link.label": "Public link", + "files.show.open": "Show/download file", + "files.show.back": "Back", + "files.show.replace": "Replace", + "files.show.delete": "Delete", + "files.show.error.rename": "ファイル名を変更できませんでした", + "files.show.error.form": "入力が正しくありません", + "files.upload.drop": "ファイルをここにドロップ", + "files.upload.click": "もしくは、クリックしてアップロード", + "files.replace.drop": "ファイルをここにドロップ", + "files.replace.click": "もしくは、クリックして置き換え", + "files.replace.error.type": "元と同じ種類のファイルを選択してください", + "files.delete.headline": "このファイルを削除してもよろしいですか?", + "files.error.missing.page": "ページが見つかりませんでした", + "files.error.missing.file": "ファイルが見つかりませんでした", + "users": "ユーザー", + "users.index.headline": "ユーザーの一覧", + "users.index.add": "新規ユーザー", + "users.index.edit": "編集", + "users.index.delete": "削除", + "users.form.username.label": "ユーザーID", + "users.form.username.placeholder": "Your username", + "users.form.username.help": "使用可能な文字: 英数小文字 a-z, 0-9 と ハイフン ( - )", + "users.form.username.readonly": "ユーザーIDは変更できません", + "users.form.firstname.label": "姓", + "users.form.lastname.label": "名", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "パスワード", + "users.form.password.confirm.label": "パスワードを再入力", + "users.form.password.new.label": "新しいパスワード", + "users.form.password.new.confirm.label": "新しいパスワードを再入力", + "users.form.password.new.help": "変更しない場合は入力しない", + "users.form.language.label": "表示言語", + "users.form.role.label": "権限", + "users.form.options.headline": "アカウントオプション", + "users.form.options.message": "Emailを送る", + "users.form.options.delete": "アカウントを削除", + "users.form.avatar.headline": "プロフィール用画像", + "users.form.avatar.upload": "画像のアップロード", + "users.form.avatar.replace": "画像の変更", + "users.form.avatar.delete": "画像の削除", + "users.form.back": "ユーザーの一覧に戻る", + "users.form.error.password.confirm": "パスワードを確認してください", + "users.form.error.update": "ユーザー情報は更新できませんでした", + "users.form.error.update.rights": "このユーザーの情報を編集する権限がありません", + "users.form.error.create": "新規ユーザーは作成できませんでした", + "users.form.error.permissions.title": "アカウント用のフォルダに書き込み権限がありません", + "users.form.error.permissions.text": "/site/accounts が存在し、書き込み権限があることを確認してください", + "users.delete.headline": "このユーザーを削除してもよろしいですか?", + "users.delete.error": "ユーザーを削除できませんでした", + "users.delete.error.permission": "ユーザーを削除する権限がありません", + "users.delete.error.permission.single": "このユーザーを削除する権限がありません", + "users.delete.error.lastadmin": "最後のAdminユーザーは削除できません", + "users.avatar.drop": "画像をここにドロップ", + "users.avatar.click": "もしくは、クリックしてアップロード", + "users.avatar.error.type": "JPG, PNG および GIF 形式のファイルのみアップロード可能です", + "users.avatar.error.folder.headline": "avatar フォルダに書き込み権限がありません", + "users.avatar.error.folder.text": "プロフィール用画像格納用に /assets/avatars フォルダを作成し、書き込み権限を与えてください", + "users.avatar.error.permission": "プロフィール用画像を変更する権限がありません", + "users.avatar.delete.error": "プロフィール用画像を削除できませんでした", + "users.avatar.delete.error.permission": "このユーザーのプロフィール用画像を削除する権限がありません", + "users.avatar.delete.success": "プロフィール用画像を削除しました", + "users.avatar.missing": "このユーザーはプロフィール用画像が設定されていません", + "users.error.missing": "ユーザーが見つかりませんでした", + "user.error.lastadmin": "現在あなたが唯一のAdminユーザーです。変更することはできません", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必須", + "fields.date.label": "Date", + "fields.date.months": [ + "1月", + "2月", + "3月", + "4月", + "5月", + "6月", + "7月", + "8月", + "9月", + "10月", + "11月", + "12月" + ], + "fields.date.weekdays": [ + "日曜日", + "月曜日", + "火曜日", + "水曜日", + "木曜日", + "金曜日", + "土曜日" + ], + "fields.date.weekdays.short": [ + "日", + "月", + "火", + "水", + "木", + "金", + "土" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "数値", + "fields.number.placeholder": "整数値", + "fields.page.label": "ページ", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "パスワード", + "fields.structure.add": "作成", + "fields.structure.add.first": "最初のカードを作成してください。", + "fields.structure.empty": "カードがありません。", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "キャンセル", + "fields.structure.save": "保存", + "fields.structure.edit": "編集", + "fields.structure.delete": "削除", + "fields.structure.delete.label": "このカードを削除してもよろしいですか?", + "fields.tags.label": "タグ", + "fields.tel.label": "電話番号", + "fields.textarea.buttons.bold.label": "強調", + "fields.textarea.buttons.bold.text": "強調", + "fields.textarea.buttons.italic.label": "斜体", + "fields.textarea.buttons.italic.text": "斜体", + "fields.textarea.buttons.link.label": "リンク", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "画像", + "fields.textarea.buttons.file.label": "ファイル", + "fields.toggle.yes": "はい", + "fields.toggle.no": "いいえ", + "fields.toggle.on": "オン", + "fields.toggle.off": "オフ", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "URLを入力", + "editor.link.text.label": "リンクテキスト", + "editor.link.text.help": "リンクテキストは任意で設定可能です", + "editor.email.address.label": "Emailアドレスを入力してください", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "リンクテキスト", + "editor.email.text.help": "リンクテキストは任意で設定可能です", + "editor.file.empty": "'このページにはファイルがありません',", + "editor.image.empty": "'このページには画像がありません',", + "autocomplete.method.error": "オートコンプリートがエラーを起こしました", + "blueprints.error.default.missing": "デフォルトの定義ファイル(/site/blueprints/default.php)がありません", + "error": "エラー", + "error.headline": "エラー" +} \ No newline at end of file diff --git a/panel/app/translations/ja/package.json b/panel/app/translations/ja/package.json new file mode 100644 index 0000000..1c20e5e --- /dev/null +++ b/panel/app/translations/ja/package.json @@ -0,0 +1,4 @@ +{ + "title": "Japanese", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/nb/core.json b/panel/app/translations/nb/core.json new file mode 100644 index 0000000..c323d1e --- /dev/null +++ b/panel/app/translations/nb/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Avbryt", + "add": "Legg til", + "addit": "Legg til og rediger", + "save": "Lagre", + "saved": "Lagret!", + "change": "Endre", + "delete": "Slett", + "insert": "Sett Inn", + "ok": "Ok", + "routes.error.invalid": "Ugylid Panel URL", + "controller.error.invalid": "Ugyldig kontroller", + "controller.error.action": "Ugyldig handling", + "view.error.invalid": "Ugyldig view:", + "options.show": "Vis alternativer", + "options.hide": "Skjul alternativer", + "installation": "Installasjon", + "installation.check.headline": "Kirby Panel Installasjon", + "installation.check.text": "Det oppsto problemer under installasjonen av Kirby…", + "installation.check.retry": "Prøv på nytt", + "installation.check.error": "Det er noen problemer!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts er ikke skrivbar", + "installation.check.error.avatars": "/assets/avatars er ikke skrivbar", + "installation.check.error.blueprints": "Vennligst legg til en /site/blueprints mappe", + "installation.check.error.content": "Mappen content og alt av innhold må være skrivbar.", + "installation.check.error.thumbs": "Mappen thumbs må være skrivbar.", + "installation.signup.username.label": "Lag din første konto", + "installation.signup.username.placeholder": "Brukernavn", + "installation.signup.email.label": "Epost", + "installation.signup.email.placeholder": "epost@eksempel.no", + "installation.signup.password.label": "Passord", + "installation.signup.language.label": "Språk", + "installation.signup.button": "Opprett konto", + "login": "Logg Inn", + "login.welcome": "Vennligst logg inn med din nye konto", + "login.username.label": "Brukernavn", + "login.password.label": "Passord", + "login.error": "Brukernavn eller passord er feil", + "login.button": "Logg Inn", + "login.log.error.permissions": "Logg inn loggfilen er ikke skrivbar.", + "logout": "Logg ut", + "topbar.error.class.definition": "Mangler topbar definisjon for klassen:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Sider", + "dashboard.index.pages.edit": "Rediger", + "dashboard.index.pages.add": "Legg til", + "dashboard.index.site.title": "Din side's URL", + "dashboard.index.account.title": "Din konto", + "dashboard.index.account.edit": "Rediger", + "dashboard.index.metatags.title": "Nettsted variabler", + "dashboard.index.metatags.edit": "Rediger", + "dashboard.index.history.title": "Dine siste endringer", + "dashboard.index.history.text": "Dine siste endrede sider vil vises her for å gjøre det enkelt å finne dem igjen senere.", + "dashboard.index.license.title": "Kirby lisens", + "dashboard.index.license.text": "Det ser ut som om du kjører Kirby på en offentlig server uten en gyldig lisens!\n\nVær så snill, støtt Kirby og (link: {buy} text: kjøp en lisens nå)\n\nOm du allerede har en lisens nøkkel, legg den til i din konfigurasjons fil: (link {docs} text: site/config/config.php)", + "metatags": "Nettsted variabler", + "metatags.info": "Kirby informasjon", + "metatags.license": "Kirby lisens", + "metatags.version.toolkit": "Toolkit versjon", + "metatags.version.kirby": "Kirby versjon", + "metatags.version.panel": "Panel versjon", + "metatags.back": "Tilbake til dashboardet", + "metatags.files": "Sidefiler", + "site.delete.error": "Siden kan ikke slettes", + "pages.show.settings": "Sideinnstillinger", + "pages.show.preview": "Åpne forhåndsvisning", + "pages.show.template": "Mal", + "pages.show.changeurl": "Endre URL", + "pages.show.invisible": "Status: usynlig", + "pages.show.visible": "Status: synlig", + "pages.show.changes.text": "Du har ulagrede endringer!", + "pages.show.changes.button": "Forkast", + "pages.show.delete": "Slett denne siden", + "pages.show.subpages.title": "Sider", + "pages.show.subpages.edit": "Rediger", + "pages.show.subpages.add": "Legg til", + "pages.show.subpages.empty": "Denne siden har ingen undersider", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Rediger", + "pages.show.files.add": "Legg til", + "pages.show.files.empty": "Denne siden har ingen filer", + "pages.show.error.permissions.title": "Siden er ikke skrivbar", + "pages.show.error.permissions.text": "Vennligst sjekk rettigheten for content mappen og filer.", + "pages.show.error.permissions.retry": "Prøv på nytt", + "pages.show.error.notitle.title": "Blueprint har ikke tittelfelt", + "pages.show.error.notitle.text": "Vennligst legg til ett tittelfelt og prøv igjen", + "pages.show.error.notitle.retry": "Prøv igjen", + "pages.show.error.form": "Vennligst fyll inn alle feltene korrekt", + "pages.add.title.label": "Legg til en ny side", + "pages.add.title.placeholder": "Tittel", + "pages.add.url.label": "URL-appendiks", + "pages.add.url.enter": "(skriv inn din tittel)", + "pages.add.url.close": "Lukk", + "pages.add.url.help": "Format: små bokstaver a-z, 0-9 og vanlige bindestreker", + "pages.add.template.label": "Mal", + "pages.add.error.create": "Siden kunne ikke opprettes", + "pages.add.error.title": "Tittelen mangler", + "pages.add.error.template": "Malen mangler", + "pages.add.error.max.headline": "Ingen nye sider tillat", + "pages.add.error.max.text": "Maksimalt antall undersider for den gjeldende siden er nådd.", + "pages.url.uid.label": "URL-appendiks", + "pages.url.uid.label.option": "Opprett fra tittel", + "pages.url.error.exists": "En side med samme appendiks finnes allerede", + "pages.url.error.move": "Appendiks kunne ikke bli endret", + "pages.url.error.rights": "Du kan ikke endre URLen for denne siden", + "pages.template.select.label": "Mal", + "pages.template.warning.text": "Disse feltene vil endres når du endrer mal", + "pages.template.warning.removed": "Fjernet felt", + "pages.template.warning.replaced": "Erstattet felt", + "pages.template.warning.added": "Nye felt", + "pages.template.error": "Malen for denne siden kan ikke endres", + "pages.toggle.position": "Posisjon", + "pages.toggle.invisible": "usynlig", + "pages.toggle.publish": "Vil du virkelig endre statusen for denne siden til **synlig?**", + "pages.toggle.hide": "Vil du virkelig endre statusen for denne siden til **usynlig?**", + "pages.toggle.error.error": "Statusen for error siden kan ikke bli endret", + "pages.delete.headline": "Vil du virkelig slette denne siden?", + "pages.delete.error.home.headline": "Startsiden kan ikke slettes", + "pages.delete.error.home.text": "Du prøver å slette startsiden. Dette er ikke mulig og vil lede til uønskede effekter.", + "pages.delete.error.error.headline": "Feil siden kan ikke slette", + "pages.delete.error.error.text": "Du prøver å slette feil siden. Dette er ikke mulig og vil lede til uønskede effekter..", + "pages.delete.error.children.headline": "Denne siden kan ikke slette", + "pages.delete.error.children.text": "Denne siden har undersider og kan ikke bli slette. Vennligst slett alle undersider først.", + "pages.delete.error.blocked.headline": "Denne siden kan ikke slette", + "pages.delete.error.blocked.text": "Denne siden er låst og kan ikke slettes.", + "pages.search.help": "Søk sider med URL. Naviger gjennom søkeresultatene med opp og ned piltastene og trykk enter for å gå til den valgte siden", + "pages.search.noresults": "Det finnes ingen resultater for søket ditt. Vennligst prøv igjen med en annen nettadresse", + "pages.error.missing": "Siden kunne ikke bli funnet", + "subpages": "Sider", + "subpages.index.headline": "Sider i", + "subpages.index.back": "Tilbake", + "subpages.index.add": "Legg til en ny side", + "subpages.index.add.first.text": "Denne siden har ingen undersider ennå", + "subpages.index.add.first.button": "Legg til den første siden", + "subpages.index.visible": "Synlige sider", + "subpages.index.visible.help": "Dra usynlige sider her for å sortere dem/gjøre dem synlige.", + "subpages.index.invisible": "Usynlige sider", + "subpages.index.invisible.help": "Dra synlige sider her for å sortere dem/gjøre dem usynlige.", + "subpages.add.error": "Denne siden er ikke tillatt å ha undersider", + "subpages.add.error.more": "Denne siden kan ikke å ha flere undersider", + "subpages.error.missing": "Siden kunne ikke bli funnet", + "files": "Filer", + "files.index.headline": "Filer for", + "files.index.back": "Tilbake", + "files.index.upload": "Last opp en ny fil", + "files.index.upload.first.text": "Denne siden har ingen filer ennå", + "files.index.upload.first.button": "Last opp den første filen", + "files.index.edit": "Rediger", + "files.index.delete": "Slett", + "files.index.error.disabled": "Denne siden er ikke tillatt å ha filer", + "files.add.error.max": "Maksimalt antall filer for den gjeldende siden er nådd.", + "files.add.error.extension.missing": "Du kan ikke laste opp filer uten filtype", + "files.add.error.extension.forbidden": "Ugyldig filtype", + "files.add.error.mime.forbidden": "Ugyldig MIME-type", + "files.add.error.htaccess": "htaccess filer kan ikke bli lastet opp", + "files.add.error.invisible": "Usynlige filer kan ikke bli lastet opp", + "files.add.blueprint.type.error": "Siden godtar kun:", + "files.add.blueprint.size.error": "Siden tillater bare filstørrelsen:", + "files.show.name.label": "Filnavn", + "files.show.info.label": "Type / Størrelse / Dimensjoner", + "files.show.link.label": "Offentlig link", + "files.show.open": "Vis/last ned fil", + "files.show.back": "Tilbake", + "files.show.replace": "Erstatt", + "files.show.delete": "Slett", + "files.show.error.rename": "Filen kunne ikke endre navn", + "files.show.error.form": "Vennligst fyll inn alle feltene korrekt", + "files.upload.drop": "Slipp filene her…", + "files.upload.click": "…eller klikk for å laste opp", + "files.replace.drop": "Slipp en fil her…", + "files.replace.click": "…eller klikk for å erstatte", + "files.replace.error.type": "Den opplastede filen må ha samme filtype", + "files.delete.headline": "Vil du virkelig slette denne filen?", + "files.error.missing.page": "Siden kunne ikke bli funnet", + "files.error.missing.file": "Filen kunne ikke bli funnet", + "users": "Brukere", + "users.index.headline": "Alle brukere", + "users.index.add": "Legg til en ny bruker", + "users.index.edit": "Rediger", + "users.index.delete": "Slett", + "users.form.username.label": "Brukernavn", + "users.form.username.placeholder": "Ditt brukernavn", + "users.form.username.help": "Tillatte tegn: små bokstaver a-z, 0-9 og vanlige bindestreker", + "users.form.username.readonly": "Brukernavnet kan ikke endres", + "users.form.firstname.label": "Fornavn", + "users.form.lastname.label": "Etternavn", + "users.form.email.label": "Epost", + "users.form.email.placeholder": "epost@eksempel.no", + "users.form.password.label": "Passord", + "users.form.password.confirm.label": "Bekreft passord", + "users.form.password.new.label": "Nytt passord", + "users.form.password.new.confirm.label": "Bekreft det nye passordet", + "users.form.password.new.help": "La stå tomt for å beholde det gjeldende passord", + "users.form.language.label": "Språk", + "users.form.role.label": "Rolle", + "users.form.options.headline": "Kontoalternativer", + "users.form.options.message": "Send epost", + "users.form.options.delete": "Slett konto", + "users.form.avatar.headline": "Profil bilde", + "users.form.avatar.upload": "Last opp ett profil bilde", + "users.form.avatar.replace": "Erstatt profil bildet", + "users.form.avatar.delete": "Slett profil bildet", + "users.form.back": "Tilbake til brukere", + "users.form.error.password.confirm": "Vennligst bekreft passordet", + "users.form.error.update": "Brukeren kunne ikke bli oppdatert", + "users.form.error.update.rights": "Du er ikke tillat til å oppdatere denne brukeren", + "users.form.error.create": "Brukeren kunne ikke bli opprettes", + "users.form.error.permissions.title": "Account mappen er ikke skrivbar", + "users.form.error.permissions.text": "Vennligst kontroller at /site/accounts eksiterer og er skrivbar.", + "users.delete.headline": "Vil du virkelig slette denne konten?", + "users.delete.error": "Denne brukeren kunne ikke bli slettet", + "users.delete.error.permission": "Du er ikke tillat til å slette brukere", + "users.delete.error.permission.single": "Du er ikke tillat å slette denne brukeren", + "users.delete.error.lastadmin": "Du kan ikke slette den siste admin", + "users.avatar.drop": "Slipp profil bildet her…", + "users.avatar.click": "…eller klikk for å laste opp", + "users.avatar.error.type": "Du kan kun laste opp JPG, PNG og GIF filer", + "users.avatar.error.folder.headline": "Avatar mappen er ikke skrivbar", + "users.avatar.error.folder.text": "Vennligst opprett mappen /assets/avatars og kontroller att den er skrivbar for å laste opp profil bilder.", + "users.avatar.error.permission": "Du er ikke tillat til å endre avatar", + "users.avatar.delete.error": "Profil bildet kunne ikke bli slette", + "users.avatar.delete.error.permission": "Du er ikke tillat til å slette avataren til denne brukeren", + "users.avatar.delete.success": "Profil bildet har blitt slettet", + "users.avatar.missing": "Denne brukerer har ingen avatar", + "users.error.missing": "Brukeren kunne ikke bli funnet", + "user.error.lastadmin": "Du er den eneste administrator. Dette kan ikke endres.", + "form.error.missing": "Skjemaet kan ikke funnet", + "form.construct.error.invalid": "Ugyldig skjema byggemåte", + "fields.required": "Påkrevd", + "fields.date.label": "Dato", + "fields.date.months": [ + "Januar", + "Februar", + "Mars", + "April", + "Mai", + "Juni", + "July", + "August", + "September", + "Oktober", + "November", + "Desember" + ], + "fields.date.weekdays": [ + "Søndag", + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag" + ], + "fields.date.weekdays.short": [ + "Søn", + "Man", + "Tir", + "Ons", + "Tor", + "Fre", + "Lør" + ], + "fields.email.label": "Epost", + "fields.email.placeholder": "epost@eksempel.no", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Side", + "fields.page.placeholder": "sti/til/side", + "fields.password.label": "Passord", + "fields.structure.add": "Legg til", + "fields.structure.add.first": "Legg til den første oppføringen", + "fields.structure.empty": "Ingen oppføringer enda", + "fields.structure.entry.error": "Elementet kunne ikke bli funnet", + "fields.structure.cancel": "Avbryt", + "fields.structure.save": "Lagre", + "fields.structure.edit": "Rediger", + "fields.structure.delete": "Slett", + "fields.structure.delete.label": "Ønsker du virkelig å slette denne oppføringen?", + "fields.tags.label": "Tagger", + "fields.tel.label": "Mobil", + "fields.textarea.buttons.bold.label": "Tykk tekst", + "fields.textarea.buttons.bold.text": "Tykk tekst", + "fields.textarea.buttons.italic.label": "Kursiv tekst", + "fields.textarea.buttons.italic.text": "Kursiv tekst", + "fields.textarea.buttons.link.label": "Adresse", + "fields.textarea.buttons.email.label": "Epost", + "fields.textarea.buttons.image.label": "Bilde", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nei", + "fields.toggle.on": "På", + "fields.toggle.off": "Av", + "fields.error.missing.controller": "Felt kontroller filen mangler", + "fields.error.missing.class": "Felt kontroller klassen mangler", + "fields.error.route.invalid": "Ugyldig felt rute", + "fields.error.extended": "Feltet kan ikke bli utvidet", + "editor.link.url.label": "Sett inn URL", + "editor.link.text.label": "Koblingstekst", + "editor.link.text.help": "Koblingstekst er valgfri", + "editor.email.address.label": "Sett inn epost adresse", + "editor.email.address.placeholder": "epost@eksempel.no", + "editor.email.text.label": "Koblingstekst", + "editor.email.text.help": "Koblingstekst er valgfri", + "editor.file.empty": "Denne siden har ingen filer", + "editor.image.empty": "Denne siden har ingen bilder", + "autocomplete.method.error": "Ugyldig autocomplete metode", + "blueprints.error.default.missing": "Mangler standard blåkopi", + "error": "Feil", + "error.headline": "Feil" +} \ No newline at end of file diff --git a/panel/app/translations/nb/package.json b/panel/app/translations/nb/package.json new file mode 100644 index 0000000..72ddcc6 --- /dev/null +++ b/panel/app/translations/nb/package.json @@ -0,0 +1,4 @@ +{ + "title": "Norsk Bokmål", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/nl/core.json b/panel/app/translations/nl/core.json new file mode 100644 index 0000000..b713063 --- /dev/null +++ b/panel/app/translations/nl/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Annuleren", + "add": "Toevoegen", + "addit": "Toevoegen en bewerken", + "save": "Opslaan", + "saved": "Opgeslagen!", + "change": "Wijzig", + "delete": "Verwijder", + "insert": "Toevoegen", + "ok": "Ok", + "routes.error.invalid": "Ongeldige Panel URL", + "controller.error.invalid": "Ongeldige controller", + "controller.error.action": "Ongeldige actie", + "view.error.invalid": "Ongeldige view:", + "options.show": "Toon opties", + "options.hide": "Verberg opties", + "installation": "Installatie", + "installation.check.headline": "Kirby Panel installatie", + "installation.check.text": "Kirby vond de volgende fouten tijdens de installatie...", + "installation.check.retry": "Opnieuw proberen", + "installation.check.error": "Er zijn een aantal problemen!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts heeft geen schrijfrechten", + "installation.check.error.avatars": "/assets/avatars heeft geen schrijfrechten", + "installation.check.error.blueprints": "Voeg een /site/blueprints map toe", + "installation.check.error.content": "De contentmap en alle bestanden hierin moeten schrijfrechten hebben.", + "installation.check.error.thumbs": "De thumbs-map moet schrijfrechten hebben.", + "installation.signup.username.label": "Maak je eerste account", + "installation.signup.username.placeholder": "Gebruikersnaam", + "installation.signup.email.label": "E-mailadres", + "installation.signup.email.placeholder": "mail@voorbeeld.nl", + "installation.signup.password.label": "Wachtwoord", + "installation.signup.language.label": "Taal", + "installation.signup.button": "Maak account aan", + "login": "Inloggen", + "login.welcome": "Log in met je nieuwe account", + "login.username.label": "Gebruikersnaam", + "login.password.label": "Wachtwoord", + "login.error": "Ongeldige gebruikersnaam of wachtwoord", + "login.button": "Inloggen", + "login.log.error.permissions": "Login logbestand heeft geen schrijfrecht.", + "logout": "Uitloggen", + "topbar.error.class.definition": "Ontbrekende 'topbar definition' voor class:", + "dashboard": "Dashboard", + "dashboard.index.pages.title": "Pagina's", + "dashboard.index.pages.edit": "Wijzig", + "dashboard.index.pages.add": "Toevoegen", + "dashboard.index.site.title": "URL van je site", + "dashboard.index.account.title": "Jouw account", + "dashboard.index.account.edit": "Wijzig", + "dashboard.index.metatags.title": "Site-variabelen", + "dashboard.index.metatags.edit": "Wijzig", + "dashboard.index.history.title": "Jouw laatste updates", + "dashboard.index.history.text": "Pagina's die het laatst door jou zijn gewijzigd komen hier te staan, om ze weer makkelijk terug te kunnen vinden.", + "dashboard.index.license.title": "Kirby licentie", + "dashboard.index.license.text": "Het lijkt erop dat je Kirby gebruikt op een publieke server zonder een geldige licentie!\nOndersteun Kirby en (link: {buy} text: koop een licentie)\nAls je al een licentie code hebt, voeg deze dan toe aan je configuratie bestand: (link: {docs} text: site/config/config.php)", + "metatags": "Site-variabelen", + "metatags.info": "Kirby info", + "metatags.license": "Kirby licentie", + "metatags.version.toolkit": "Toolkit versie", + "metatags.version.kirby": "Kirby versie", + "metatags.version.panel": "Panel versie", + "metatags.back": "Terug naar het dashboard", + "metatags.files": "Site bestanden", + "site.delete.error": "De site kan niet worden verwijderd", + "pages.show.settings": "Pagina opties", + "pages.show.preview": "Open voorbeeld", + "pages.show.template": "Template", + "pages.show.changeurl": "Verander URL", + "pages.show.invisible": "Status: onzichtbaar", + "pages.show.visible": "Status: zichtbaar", + "pages.show.changes.text": "Je hebt wijzigingen die nog niet zijn opgeslagen!", + "pages.show.changes.button": "Annuleren", + "pages.show.delete": "Verwijder deze pagina", + "pages.show.subpages.title": "Pagina's", + "pages.show.subpages.edit": "Wijzig", + "pages.show.subpages.add": "Toevoegen", + "pages.show.subpages.empty": "Deze pagina heeft geen subpagina's", + "pages.show.files.title": "Bestanden", + "pages.show.files.edit": "Wijzig", + "pages.show.files.add": "Toevoegen", + "pages.show.files.empty": "Deze pagina heeft geen bestanden", + "pages.show.error.permissions.title": "Deze pagina is niet te bewerken", + "pages.show.error.permissions.text": "Controleer de rechten voor de content-map en alle bestanden in de map.", + "pages.show.error.permissions.retry": "Opnieuw proberen", + "pages.show.error.notitle.title": "De blueprint heeft geen \\`title\\`-veld", + "pages.show.error.notitle.text": "Voeg een \\`title\\` veld toe en probeer het opnieuw.", + "pages.show.error.notitle.retry": "Opnieuw proberen", + "pages.show.error.form": "Vul alle velden correct in", + "pages.add.title.label": "Voeg een nieuwe pagina toe", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "URL-toevoeging", + "pages.add.url.enter": "(Voer een titel in)", + "pages.add.url.close": "Sluiten", + "pages.add.url.help": "Toegestaan: a-z (kleine letters) en standaard streepjes (-)", + "pages.add.template.label": "Template", + "pages.add.error.create": "De pagina kon niet worden aangemaakt", + "pages.add.error.title": "De titel ontbreekt", + "pages.add.error.template": "De template ontbreekt", + "pages.add.error.max.headline": "Geen nieuwe pagina's toegestaan", + "pages.add.error.max.text": "Je hebt het maximum aantal van subpagina's voor deze pagina bereikt.", + "pages.url.uid.label": "URL-toevoeging", + "pages.url.uid.label.option": "Maak op basis van titel", + "pages.url.error.exists": "Er bestaat al een pagina met deze URL-toevoeging", + "pages.url.error.move": "De URL-toevoeging kan niet worden gewijzigd", + "pages.url.error.rights": "Je kunt de URL van deze pagina niet wijzigen", + "pages.template.select.label": "Template", + "pages.template.warning.text": "De volgende velden veranderen, als je de template wijzigt.", + "pages.template.warning.removed": "Verwijderde velden", + "pages.template.warning.replaced": "Vervangen velden", + "pages.template.warning.added": "Toegevoegde velden", + "pages.template.error": "De template voor deze pagina kan niet worden gewijzigd", + "pages.toggle.position": "Positie", + "pages.toggle.invisible": "Onzichtbaar", + "pages.toggle.publish": "Wil je de status wijzigen naar **zichtbaar**?", + "pages.toggle.hide": "Wil je de status wijzigen naar **onzichtbaar**?", + "pages.toggle.error.error": "De status van de error-pagina kan niet gewijzigd worden", + "pages.delete.headline": "Weet je zeker dat je deze pagina wil verwijderen?", + "pages.delete.error.home.headline": "De homepage kan niet worden verwijderd", + "pages.delete.error.home.text": "Je probeert de homepage te verwijderen. Dit is niet mogelijk en zou kunnen leiden tot ongewenste resultaten.", + "pages.delete.error.error.headline": "De fout-pagina kan niet worden verwijderd", + "pages.delete.error.error.text": "Je probeert de fout-pagina te verwijderen. Dit is niet mogelijk en zou kunnen leiden tot ongewenste resultaten.", + "pages.delete.error.children.headline": "De pagina kan niet worden verwijderd", + "pages.delete.error.children.text": "Deze pagina heeft subpagina's en kan niet worden verwijderd. Verwijder eerst de subpagina's.", + "pages.delete.error.blocked.headline": "De pagina kan niet worden verwijderd", + "pages.delete.error.blocked.text": "Deze pagina is vergrendeld en kan niet worden verwijderd.", + "pages.search.help": "Zoek door pagina's op URL. Je kunt navigeren tussen de pagina's met je pijltjestoetsen. Met enter ga je naar de geselecteerde pagina.", + "pages.search.noresults": "Geen zoekresultaten. Probeer het opnieuw met een andere URL.", + "pages.error.missing": "De pagina kan niet worden gevonden", + "subpages": "Pagina's", + "subpages.index.headline": "Pagina's in", + "subpages.index.back": "Terug", + "subpages.index.add": "Nieuwe pagina toevoegen", + "subpages.index.add.first.text": "Deze pagina heeft nog geen subpagina's", + "subpages.index.add.first.button": "Voeg de eerste pagina toe", + "subpages.index.visible": "Zichtbare pagina's", + "subpages.index.visible.help": "Sleep onzichtbare pagina's hiernaartoe om ze zichtbaar te maken.", + "subpages.index.invisible": "Onzichtbare pagina's", + "subpages.index.invisible.help": "Sleep zichtbare pagina's hiernaartoe om ze onzichtbaar te maken.", + "subpages.add.error": "Deze pagina mag geen subpagina's bevatten", + "subpages.add.error.more": "Deze pagina kan niet meer subpagina's bevatten", + "subpages.error.missing": "De pagina kan niet worden gevonden", + "files": "Bestanden", + "files.index.headline": "Bestanden voor", + "files.index.back": "Terug", + "files.index.upload": "Upload een nieuw bestand", + "files.index.upload.first.text": "Deze pagina heeft nog geen bestanden", + "files.index.upload.first.button": "Upload het eerste bestand", + "files.index.edit": "Wijzgen", + "files.index.delete": "Verwijderen", + "files.index.error.disabled": "Deze pagina mag geen bestanden bevatten", + "files.add.error.max": "Het maximum aantal bestanden voor deze pagina is bereikt.", + "files.add.error.extension.missing": "Je kunt geen bestanden uploaden zonder bestandsextensie", + "files.add.error.extension.forbidden": "Bestandsextensie niet toegestaan", + "files.add.error.mime.forbidden": "Mime type niet toegestaan", + "files.add.error.htaccess": "htaccess bestanden kunnen niet geüpload worden", + "files.add.error.invisible": "Onzichtbare bestanden kunnen niet geüpload worden", + "files.add.blueprint.type.error": "Pagina laat alleen het volgende toe:", + "files.add.blueprint.size.error": "Pagina laat alleen bestanden toe met een maximum bestandsgrootte van", + "files.show.name.label": "Bestandsnaam", + "files.show.info.label": "Type / grootte / afmetingen", + "files.show.link.label": "Publieke link", + "files.show.open": "Bekijk/download bestand", + "files.show.back": "Terug", + "files.show.replace": "Vervangen", + "files.show.delete": "Verwijderen", + "files.show.error.rename": "Het bestand kan niet worden hernoemd", + "files.show.error.form": "Vul alle velden correct in", + "files.upload.drop": "Sleep bestanden hiernaartoe...", + "files.upload.click": "... of klik hier om bestanden te uploaden", + "files.replace.drop": "Sleep een bestand hiernaartoe...", + "files.replace.click": "... of klik hier om een bestand te uploaden", + "files.replace.error.type": "Het geüploade bestand moet van hetzelfde type zijn", + "files.delete.headline": "Wil je dit bestand verwijderen?", + "files.error.missing.page": "De pagina kan niet worden gevonden", + "files.error.missing.file": "Het bestand kan niet worden gevonden", + "users": "Users", + "users.index.headline": "Alle gebruikers", + "users.index.add": "Voeg een nieuwe gebruiker toe", + "users.index.edit": "Wijzigen", + "users.index.delete": "Verwijderen", + "users.form.username.label": "Gebruikersnaam", + "users.form.username.placeholder": "Jouw gebruikersnaam", + "users.form.username.help": "Toegestaan: a-z (kleine letters), 0-9 en streepjes (-)", + "users.form.username.readonly": "De gebruikersnaam kan niet worden gewijzigd", + "users.form.firstname.label": "Voornaam", + "users.form.lastname.label": "Achternaam", + "users.form.email.label": "E-mailadres", + "users.form.email.placeholder": "mail@voorbeeld.nl", + "users.form.password.label": "Wachtwoord", + "users.form.password.confirm.label": "Bevestig wachtwoord", + "users.form.password.new.label": "Nieuw wachtwoord", + "users.form.password.new.confirm.label": "Bevestig het nieuwe wachtwoord", + "users.form.password.new.help": "Laat leeg om je huidige wachtwoord te behouden", + "users.form.language.label": "Taal", + "users.form.role.label": "Rol", + "users.form.options.headline": "Account-opties", + "users.form.options.message": "E-mail versturen", + "users.form.options.delete": "Verwijder account", + "users.form.avatar.headline": "Avatar", + "users.form.avatar.upload": "Avatar uploaden", + "users.form.avatar.replace": "Avatar vervangen", + "users.form.avatar.delete": "Delete avatar", + "users.form.back": "Terug naar gebruikers", + "users.form.error.password.confirm": "Bevestig je wachtwoord", + "users.form.error.update": "De gebruiker kan niet worden gewijzigd", + "users.form.error.update.rights": "Je hebt niet voldoende rechten om deze gebruiker te wijzigen", + "users.form.error.create": "De gebruiker kan niet worden aangemaakt", + "users.form.error.permissions.title": "De 'accounts'-map heeft niet voldoende rechten.", + "users.form.error.permissions.text": "Zorg ervoor dat de map 'site/accounts' bestaat en schrijfrechten heeft.", + "users.delete.headline": "Wil je deze gebruiker verwijderen?", + "users.delete.error": "De gebruiker kan niet worden verwijderd", + "users.delete.error.permission": "Je hebt niet voldoende rechten om gebruikers te verwijderen", + "users.delete.error.permission.single": "Je hebt niet voldoende rechten om deze gebruiker te verwijderen", + "users.delete.error.lastadmin": "Je kan de laatste admin niet verwijderen", + "users.avatar.drop": "Sleep een profielfoto hiernaartoe...", + "users.avatar.click": "... of klik hier om afbeelding te selecteren", + "users.avatar.error.type": "Je kunt alleen .jpg, .png en .gif bestanden uploaden", + "users.avatar.error.folder.headline": "De avatar-map heeft geen schrijfrechten.", + "users.avatar.error.folder.text": "Zorg ervoor dat de map /assets/avatars bestaat en schrijfrechten heeft om avatars te uploaden.", + "users.avatar.error.permission": "Je hebt niet voldoende rechtsen om deze avatar te wijzigen", + "users.avatar.delete.error": "De avatar kan niet worden verwijderd", + "users.avatar.delete.error.permission": "Je hebt niet voldoende rechten om de avatar van deze gebruiker te verwijderen", + "users.avatar.delete.success": "De avatar is verwijderd", + "users.avatar.missing": "Deze gebruiker heeft geen avatar", + "users.error.missing": "De gebruiker kan niet worden gevonden", + "user.error.lastadmin": "Jij bent de enige admin. Dit kan niet gewijzigd worden.", + "form.error.missing": "Het formulier kon niet gevonden worden.", + "form.construct.error.invalid": "Ongeldige formulier construction method", + "fields.required": "Verplicht", + "fields.date.label": "Datum", + "fields.date.months": [ + "januari", + "februari", + "maart", + "april", + "mei", + "juni", + "juli", + "augustus", + "september", + "oktober", + "november", + "december" + ], + "fields.date.weekdays": [ + "Zondag", + "Maandag", + "Dinsdag", + "Woensdag", + "Donderdag", + "Vrijdag", + "Zaterdag" + ], + "fields.date.weekdays.short": [ + "Zo", + "Ma", + "Di", + "Wo", + "Do", + "Vr", + "Za" + ], + "fields.email.label": "E-mailadres", + "fields.email.placeholder": "mail@voorbeeld.nl", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Pagina", + "fields.page.placeholder": "pad/naar/pagina", + "fields.password.label": "Wachtwoord", + "fields.structure.add": "Toevoegen", + "fields.structure.add.first": "Voeg het eerste item toe", + "fields.structure.empty": "Nog geen items.", + "fields.structure.entry.error": "Item kon niet gevonden worden", + "fields.structure.cancel": "Annuleren", + "fields.structure.save": "Ok", + "fields.structure.edit": "Wijzigen", + "fields.structure.delete": "Verwijderen", + "fields.structure.delete.label": "Wil je deze entry verwijderen?", + "fields.tags.label": "Tags", + "fields.tel.label": "Telefoon", + "fields.textarea.buttons.bold.label": "Dikgedrukte tekst", + "fields.textarea.buttons.bold.text": "Dikgedrukte tekst", + "fields.textarea.buttons.italic.label": "Cursieve tekst", + "fields.textarea.buttons.italic.text": "Cursieve tekst", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "E-mailadres", + "fields.textarea.buttons.image.label": "Afbeelding", + "fields.textarea.buttons.file.label": "Bestand", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nee", + "fields.toggle.on": "Aan", + "fields.toggle.off": "Uit", + "fields.error.missing.controller": "De 'field controller' file ontbreekt", + "fields.error.missing.class": "De 'field controller' class ontbreekt", + "fields.error.route.invalid": "Ongeldige field route", + "fields.error.extended": "Het veld kan niet worden uitgebreid", + "editor.link.url.label": "URL invoegen", + "editor.link.text.label": "Link-tekst", + "editor.link.text.help": "De link-tekst is niet verplicht", + "editor.email.address.label": "E-mailadres invoegen", + "editor.email.address.placeholder": "mail@voorbeeld.nl", + "editor.email.text.label": "Link-tekst", + "editor.email.text.help": "De link-tekst is niet verplicht", + "editor.file.empty": "Deze pagina heeft geen bestanden", + "editor.image.empty": "Deze pagina heeft geen afbeeldingen", + "autocomplete.method.error": "Ongeldige autocomplete methode", + "blueprints.error.default.missing": "Default blueprint ontbreekt", + "error": "Foutmelding", + "error.headline": "Foutmelding" +} \ No newline at end of file diff --git a/panel/app/translations/nl/package.json b/panel/app/translations/nl/package.json new file mode 100644 index 0000000..2b99314 --- /dev/null +++ b/panel/app/translations/nl/package.json @@ -0,0 +1,4 @@ +{ + "title": "Nederlands", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pl/core.json b/panel/app/translations/pl/core.json new file mode 100644 index 0000000..804a70b --- /dev/null +++ b/panel/app/translations/pl/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Anuluj", + "add": "Dodaj", + "addit": "Dodaj i Edytuj", + "save": "Zapisz", + "saved": "Zapisano!", + "change": "Zmień", + "delete": "Usuń", + "insert": "Wstaw", + "ok": "Ok", + "routes.error.invalid": "Niewłaściwy adres URL panelu", + "controller.error.invalid": "Niewłaściwy kontroler", + "controller.error.action": "Niewłaściwa akcja", + "view.error.invalid": "Niewłaściwy widok:", + "options.show": "Pokaż opcje", + "options.hide": "Ukryj opcje", + "installation": "Instalacja", + "installation.check.headline": "Instalacja panelu Kirby", + "installation.check.text": "Kirby napotkał następujące problemy podczas instalacji…", + "installation.check.retry": "Ponów próbę", + "installation.check.error": "Wystąpiły jakieś problemy!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts nie ma praw do zapisu", + "installation.check.error.avatars": "/assets/avatars nie ma praw do zapisu", + "installation.check.error.blueprints": "Proszę dodać folder /site/blueprints", + "installation.check.error.content": "Folder content oraz wszystkie foldery i pliki wewnątrz muszą mieć ustawione prawa do zapisu.", + "installation.check.error.thumbs": "Folder thumbs musi mieć ustawione prawa do zapisu.", + "installation.signup.username.label": "Utwórz swoje pierwsze konto.", + "installation.signup.username.placeholder": "Nazwa użytkownika", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "Hasło", + "installation.signup.language.label": "Język", + "installation.signup.button": "Utwórz konto", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nazwa użytkownika", + "login.password.label": "Hasło", + "login.error": "Niepoprawna nazwa użytkownika lub hasło", + "login.button": "Log in", + "login.log.error.permissions": "Plik dziennika logowania nie ma ustawionych praw do zapisu.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Panel administracyjny", + "dashboard.index.pages.title": "Strony", + "dashboard.index.pages.edit": "Edytuj", + "dashboard.index.pages.add": "Dodaj", + "dashboard.index.site.title": "Adres URL Twojej strony", + "dashboard.index.account.title": "Twoje konto", + "dashboard.index.account.edit": "Edytuj", + "dashboard.index.metatags.title": "Metadane strony", + "dashboard.index.metatags.edit": "Edytuj", + "dashboard.index.history.title": "Twoje ostatnie modyfikacje", + "dashboard.index.history.text": "Twoje ostatnio zmodyfikowane strony będą wyświetlane tutaj, aby łatwo je było odnaleźć ponownie później.", + "dashboard.index.license.title": "Licencja Kirby", + "dashboard.index.license.text": "Wygląda na to, że korzystasz z Kirby na publicznym serwerze bez ważnej licencji!\nProszę, wesprzyj Kirby i (link: {buy} text: kup licencję teraz).\nJeżeli już posiadasz klucz licencyjny, po prostu dodaj go do pliku konfiguracyjnego: (link: {docs} text: site/config/config.php)", + "metatags": "Ustawienia serwisu", + "metatags.info": "Informacje nt. Kirby", + "metatags.license": "Licencja Kirby", + "metatags.version.toolkit": "Wersja toolkitu", + "metatags.version.kirby": "Wersja Kirby", + "metatags.version.panel": "Wersja panelu", + "metatags.back": "Powróć do panelu administracyjnego", + "metatags.files": "Pliki strony", + "site.delete.error": "Strona nie może zostać usunięta", + "pages.show.settings": "Ustawienia strony", + "pages.show.preview": "Otwórz podgląd", + "pages.show.template": "Szablon", + "pages.show.changeurl": "Zmień URL", + "pages.show.invisible": "Status: niewidzialna", + "pages.show.visible": "Status: widoczna", + "pages.show.changes.text": "Masz niezapisane zmiany!", + "pages.show.changes.button": "Odrzuć", + "pages.show.delete": "Usuń tę stronę", + "pages.show.subpages.title": "Strony", + "pages.show.subpages.edit": "Edytuj", + "pages.show.subpages.add": "Dodaj", + "pages.show.subpages.empty": "Ta strona nie posiada podstron", + "pages.show.files.title": "Pliki", + "pages.show.files.edit": "Edytuj", + "pages.show.files.add": "Dodaj", + "pages.show.files.empty": "Ta strona nie posiada plików", + "pages.show.error.permissions.title": "Nie ma uprawnień do zapisu dla tej strony", + "pages.show.error.permissions.text": "Sprawdź uprawnienia dla folderu content i plików znajdujących się w tym folderze.", + "pages.show.error.permissions.retry": "Spróbuj ponownie", + "pages.show.error.notitle.title": "Szablon tej strony nie posiada pola tytułu", + "pages.show.error.notitle.text": "Dodaj pole tytułu i spróbuj ponownie", + "pages.show.error.notitle.retry": "Spróbuj ponownie", + "pages.show.error.form": "Wypełnij wszystkie pola poprawnie", + "pages.add.title.label": "Dodaj nową stronę", + "pages.add.title.placeholder": "Tytuł", + "pages.add.url.label": "apendyks URL", + "pages.add.url.enter": "(wprowadź tytuł)", + "pages.add.url.close": "Zamknij", + "pages.add.url.help": "Formatowanie: małe litery a-z, 0-9 i myślnik", + "pages.add.template.label": "Szablon", + "pages.add.error.create": "Strona nie może zostać utworzona", + "pages.add.error.title": "Brakuje tytułu", + "pages.add.error.template": "Brakuje szablonu", + "pages.add.error.max.headline": "Nie można dodać nowej strony", + "pages.add.error.max.text": "Maksymalna liczba podstron dla tej strony została już osiągnięta.", + "pages.url.uid.label": "apendyks URL", + "pages.url.uid.label.option": "Utwórz na podstawie tytułu", + "pages.url.error.exists": "Strona z takim apendyksem już istnieje", + "pages.url.error.move": "Apendyks nie mógł zostać zmieniony", + "pages.url.error.rights": "Nie możesz zmienić adres URL tej strony", + "pages.template.select.label": "Szablon", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Pozycja", + "pages.toggle.invisible": "niewidoczny", + "pages.toggle.publish": "Czy na pewno chcesz zmienić status strony na **widoczna**", + "pages.toggle.hide": "Czy na pewno chcesz zmienić status strony na **niewidzialna**", + "pages.toggle.error.error": "Status dla strony błędu nie może zostać zmieniony", + "pages.delete.headline": "Czy na pewno chcesz usunąć tę stronę?", + "pages.delete.error.home.headline": "Strona główna nie może zostać usunięta", + "pages.delete.error.home.text": "Próbujesz skasować stronę główną. To nie jest możliwe i prowadziłoby do niepożądanych skutków.", + "pages.delete.error.error.headline": "Strona błędu nie może zostać usunięta", + "pages.delete.error.error.text": "Próbujesz skasować stronę błędu. To nie jest możliwe i prowadziłoby do niepożądanych skutków.", + "pages.delete.error.children.headline": "Ta strona nie może zostać usunięta", + "pages.delete.error.children.text": "Ta strona posiada podstrony i nie może zostać skasowana. Usuń najpierw wszystkie podstrony.", + "pages.delete.error.blocked.headline": "Ta strona nie może zostać usunięta", + "pages.delete.error.blocked.text": "Ta strona jest zablokowana i nie może zostać usunięta.", + "pages.search.help": "Przeszukaj strony według URL. Nawiguj po wynikach wyszukiwania strzałkami góra/dół i naciśnij enter, by przejść do wybranej strony.", + "pages.search.noresults": "Brak wyników wyszukiwania dla zapytania. Spróbuj ponownie z innym adresem URL.", + "pages.error.missing": "Strona nie została odnaleziona", + "subpages": "Strony", + "subpages.index.headline": "Strony w", + "subpages.index.back": "Wróć", + "subpages.index.add": "Dodaj nową stronę", + "subpages.index.add.first.text": "Ta strona nie posiada jeszcze żadnych podstron", + "subpages.index.add.first.button": "Dodaj pierwszą stronę", + "subpages.index.visible": "Widzialne strony", + "subpages.index.visible.help": "Przeciągnij tutaj niewidzialne strony, by zmienić kolejność/sprawić, żeby były widzialne.", + "subpages.index.invisible": "Niewidzialne strony", + "subpages.index.invisible.help": "Przeciągnij tutaj widzialne strony, aby sprawić, żeby były niewidzialne.", + "subpages.add.error": "Ta strona nie może mieć podstron", + "subpages.add.error.more": "Ta strona nie może mieć więcej podstron", + "subpages.error.missing": "Strona nie została odnaleziona", + "files": "Pliki", + "files.index.headline": "Pliki dla", + "files.index.back": "Wróć", + "files.index.upload": "Dodaj nowy plik", + "files.index.upload.first.text": "Ta strona nie posiada jeszcze żadnych plików", + "files.index.upload.first.button": "Dodaj pierwszy plik", + "files.index.edit": "Edytuj", + "files.index.delete": "Usuń", + "files.index.error.disabled": "Strona nie może mieć żadnych plików", + "files.add.error.max": "Maksymalna liczba plików dla bieżącej stronie został osiągnięty.", + "files.add.error.extension.missing": "Nie można przesyłać plików bez rozszerzenia", + "files.add.error.extension.forbidden": "Zabronione rozszerzenie pliku", + "files.add.error.mime.forbidden": "Zabroniony typ MIME", + "files.add.error.htaccess": "Plik htaccess nie może być przesłany", + "files.add.error.invisible": "Pliki ukryte nie mogą być przesłane", + "files.add.blueprint.type.error": "Strona zezwala tylko na:", + "files.add.blueprint.size.error": "Strona zezwala tylko na pliki o rozmiarze", + "files.show.name.label": "Nazwa pliku", + "files.show.info.label": "Typ / Rozmiar / Wymiary", + "files.show.link.label": "Publiczny link", + "files.show.open": "Pokaż/pobierz plik", + "files.show.back": "Wróć", + "files.show.replace": "Zamień", + "files.show.delete": "Usuń", + "files.show.error.rename": "Nazwa pliku nie mogła zostać zmieniona", + "files.show.error.form": "Wypełnij poprawnie wszystkie pola", + "files.upload.drop": "Upuść pliki tutaj", + "files.upload.click": "…lub kliknij, aby załadować", + "files.replace.drop": "Upuść plik tutaj…", + "files.replace.click": "…lub kliknij, aby zastąpić", + "files.replace.error.type": "Przesłany plik musi być plikiem tego samego typu", + "files.delete.headline": "Czy na pewno chcesz usunąć ten plik?", + "files.error.missing.page": "Strona nie została odnaleziona", + "files.error.missing.file": "Plik nie został odnaleziony", + "users": "Użytkownicy", + "users.index.headline": "Wszyscy użytkownicy", + "users.index.add": "Dodaj nowego użytkownika", + "users.index.edit": "Edytuj", + "users.index.delete": "Usuń", + "users.form.username.label": "Nazwa użytkownika", + "users.form.username.placeholder": "Twoja nazwa użytkownika", + "users.form.username.help": "Dozwolone znaki: małe litery a-z, 0-9 i myślnik", + "users.form.username.readonly": "Nazwa użytkownika nie może zostać zmieniona", + "users.form.firstname.label": "Imię", + "users.form.lastname.label": "Nazwisko", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "Hasło", + "users.form.password.confirm.label": "Potwierdź hasło", + "users.form.password.new.label": "Nowe hasło", + "users.form.password.new.confirm.label": "Potwierdź nowe hasło", + "users.form.password.new.help": "Pozostaw puste, aby zachować bieżące hasło", + "users.form.language.label": "Język", + "users.form.role.label": "Rola", + "users.form.options.headline": "Opcje konta", + "users.form.options.message": "Wyślij email", + "users.form.options.delete": "Usuń konto", + "users.form.avatar.headline": "Zdjęcie profilowe", + "users.form.avatar.upload": "Dodaj zdjęcie profilowe", + "users.form.avatar.replace": "Zmień zdjęcie profilowe", + "users.form.avatar.delete": "Usuń zdjęcie profilowe", + "users.form.back": "Wróć do użytkowników", + "users.form.error.password.confirm": "Potwierdź hasło", + "users.form.error.update": "Użytkownik nie mógł zostać zaktualizowany", + "users.form.error.update.rights": "Nie masz uprawnień do aktualizacji tego użytkownika", + "users.form.error.create": "Użytkownik nie mógł zostać utworzony", + "users.form.error.permissions.title": "Folder account nie ma praw do zapisu", + "users.form.error.permissions.text": "Upewnij się, że folder /site/accounts istnieje i posiada prawa do zapisu.", + "users.delete.headline": "Czy na pewno chcesz usunąć tego użytkownika?", + "users.delete.error": "Użytkownik nie mógł zostać usunięty", + "users.delete.error.permission": "Nie masz uprawnień do usuwania użytkowników", + "users.delete.error.permission.single": "Nie masz uprawnień do usunięcia tego użytkownika", + "users.delete.error.lastadmin": "Nie możesz usunąć jedynego administratora", + "users.avatar.drop": "Upuść zdjęcie profilowe tutaj…", + "users.avatar.click": "…lub kliknij, aby załadować", + "users.avatar.error.type": "Możesz wgrać tylko pliki JPG, PNG i GIF", + "users.avatar.error.folder.headline": "Folder avatar nie ma praw do zapisu", + "users.avatar.error.folder.text": "Utwórz folder /assets/avatars i nadaj mu prawa do zapisu, by móc dodawać zdjęcia profilowe.", + "users.avatar.error.permission": "Nie masz uprawnień, aby zmienić awatar", + "users.avatar.delete.error": "Zdjęcie profilowe nie mogło zostać usunięte", + "users.avatar.delete.error.permission": "Nie masz uprawnień, aby usunąć awatar użytkownika", + "users.avatar.delete.success": "Zdjęcie profilowe zostało usunięte", + "users.avatar.missing": "Ten użytkownik nie ma awataru", + "users.error.missing": "Użytkownik nie został odnaleziony", + "user.error.lastadmin": "Jesteś jedynym administratorem. To ustawienie nie może zostać zmienione.", + "form.error.missing": "Formularz nie może zostać odnaleziony", + "form.construct.error.invalid": "Nieprawidłowa metoda budowy formularza", + "fields.required": "Wymagane", + "fields.date.label": "Data", + "fields.date.months": [ + "Styczeń", + "Luty", + "Marzec", + "Kwiecień", + "Maj", + "Czerwiec", + "Lipiec", + "Sierpień", + "Wrzesień", + "Październik", + "Listopad", + "Grudzień" + ], + "fields.date.weekdays": [ + "Niedziela", + "Poniedziałek", + "Wtorek", + "Środa", + "Czwartek", + "Piątek", + "Sobota" + ], + "fields.date.weekdays.short": [ + "Nd", + "Pn", + "Wt", + "Śr", + "Czw", + "Pt", + "Sb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "Numer", + "fields.number.placeholder": "nr", + "fields.page.label": "Strona", + "fields.page.placeholder": "ścieżka/do/strony", + "fields.password.label": "Hasło", + "fields.structure.add": "Dodaj", + "fields.structure.add.first": "Dodaj pierwszy wpis", + "fields.structure.empty": "Nie ma jeszcze żadnych wpisów.", + "fields.structure.entry.error": "Element nie mógł zostać odnaleziony", + "fields.structure.cancel": "Anuluj", + "fields.structure.save": "Zapisz", + "fields.structure.edit": "Edytuj", + "fields.structure.delete": "Usuń", + "fields.structure.delete.label": "Czy na pewno chcesz usunąć ten wpis?", + "fields.tags.label": "Tagi", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Pogrubiony tekst", + "fields.textarea.buttons.bold.text": "Pogrubiony tekst", + "fields.textarea.buttons.italic.label": "Kursywa", + "fields.textarea.buttons.italic.text": "Kursywa", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Obrazek", + "fields.textarea.buttons.file.label": "Plik", + "fields.toggle.yes": "Tak", + "fields.toggle.no": "Nie", + "fields.toggle.on": "Włącz", + "fields.toggle.off": "Wyłącz", + "fields.error.missing.controller": "Brak pliku field controller", + "fields.error.missing.class": "Brak klasy field controller", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "Pole nie może zostać rozszerzone", + "editor.link.url.label": "Wstaw URL", + "editor.link.text.label": "Tekst linku", + "editor.link.text.help": "Tekst linku jest opcjonalny", + "editor.email.address.label": "Wstaw adres email", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "Tekst linku", + "editor.email.text.help": "Tekst linku jest opcjonalny", + "editor.file.empty": "Ta strona nie ma plików", + "editor.image.empty": "Ta strona nie ma obrazków", + "autocomplete.method.error": "Nieprawidłowa metoda autouzupełniania", + "blueprints.error.default.missing": "Brak domyślnego szablonu", + "error": "Błąd", + "error.headline": "Błąd" +} \ No newline at end of file diff --git a/panel/app/translations/pl/package.json b/panel/app/translations/pl/package.json new file mode 100644 index 0000000..d30e51f --- /dev/null +++ b/panel/app/translations/pl/package.json @@ -0,0 +1,4 @@ +{ + "title": "Polski", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pt_BR/core.json b/panel/app/translations/pt_BR/core.json new file mode 100644 index 0000000..e674f45 --- /dev/null +++ b/panel/app/translations/pt_BR/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Adicionar", + "addit": "Adicionar & Editar", + "save": "Salvar", + "saved": "Salvo!", + "change": "Alterar", + "delete": "Deletar", + "insert": "Inserir", + "ok": "Ok", + "routes.error.invalid": "URL do Painel inválida", + "controller.error.invalid": "Controller inválido", + "controller.error.action": "Ação inválida", + "view.error.invalid": "View inválida: ", + "options.show": "Exibir opções", + "options.hide": "Ocultar opções", + "installation": "Instalação", + "installation.check.headline": "Instalação do Painel Kirby", + "installation.check.text": "Kirby encontrou os seguintes problemas durante a instalação…", + "installation.check.retry": "Tentar novamente", + "installation.check.error": "Temos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts não tem permissão de escrita", + "installation.check.error.avatars": "/assets/avatars não tem permissão de escrita", + "installation.check.error.blueprints": "Por favor, adicione uma pasta /site/blueprints", + "installation.check.error.content": "A pasta \"content\" e todas subpastas e arquivos devem ter permissão de escrita.", + "installation.check.error.thumbs": "A pasta \"thumbs\" deve ter permissão de escrita.", + "installation.signup.username.label": "Crie sua primeira conta", + "installation.signup.username.placeholder": "Usuário", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemplo.com", + "installation.signup.password.label": "Senha", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Crie sua conta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Usuário", + "login.password.label": "Senha", + "login.error": "Usuário ou senha inválido", + "login.button": "Log in", + "login.log.error.permissions": "O arquivo de log do login não tem permissão de escrita ", + "logout": "Log out", + "topbar.error.class.definition": "Definição \"topbar\" não existe para classe: ", + "dashboard": "Painel", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Adicionar", + "dashboard.index.site.title": "URL do seu site", + "dashboard.index.account.title": "Sua conta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variáveis do site", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Atualizações recentes", + "dashboard.index.history.text": "Últimas páginas modificadas serão exibidas aquí para facilitar seu acesso futuro.", + "dashboard.index.license.title": "Licença do Kirby", + "dashboard.index.license.text": "Parece que você está rodando o Kirby em um servidor público sem uma licença válida!\n\nPor favor, apoie o Kirby (link: {buy} text: comprando uma licença agora)\n\nCaso possua uma licença, basta adiciona-la ao seu arquivo config: (link: {docs} text: site/config/config.php)", + "metatags": "Variáveis do site", + "metatags.info": "Informações do Kirby", + "metatags.license": "Licença do Kirby ", + "metatags.version.toolkit": "Versão do Toolkit", + "metatags.version.kirby": "Versão do Kirby", + "metatags.version.panel": "Versão do Painel", + "metatags.back": "Voltar ao Painel", + "metatags.files": "Arquivos do site", + "site.delete.error": "Este site não pode ser deletado", + "pages.show.settings": "Configurações de página", + "pages.show.preview": "Abrir preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Mudar URL", + "pages.show.invisible": "Status: invisível", + "pages.show.visible": "Status: visível", + "pages.show.changes.text": "Você possui alterações não salvas!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Deletar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Adicionar", + "pages.show.subpages.empty": "Esta página não contém subpáginas", + "pages.show.files.title": "Arquivos", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Adicionar", + "pages.show.files.empty": "Esta página não contém arquivos", + "pages.show.error.permissions.title": "Esta página não tem permissão de escrita", + "pages.show.error.permissions.text": "Favor conferir as permissões da pasta \"content\" e todos seus arquivos", + "pages.show.error.permissions.retry": "Tentar novamente", + "pages.show.error.notitle.title": "O blueprint não possui um campo \"title\"", + "pages.show.error.notitle.text": "Favor adicionar um campo \"title\" e tentar novamente", + "pages.show.error.notitle.retry": "Tentar novamente", + "pages.show.error.form": "Favor preencher todos campos corretamente", + "pages.add.title.label": "Adicionar página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL-apêndice", + "pages.add.url.enter": "(digite um título)", + "pages.add.url.close": "Fechar", + "pages.add.url.help": "Formato: a-z minúsculas, 0-9 e hífens", + "pages.add.template.label": "Template", + "pages.add.error.create": "A página não pode ser criada", + "pages.add.error.title": "Falta o título", + "pages.add.error.template": "Falta o template", + "pages.add.error.max.headline": "Não se permite novas páginas", + "pages.add.error.max.text": "Número máximo de subpáginas para a página atual foi atingido.", + "pages.url.uid.label": "URL-apêndice", + "pages.url.uid.label.option": "Criar a partir do título", + "pages.url.error.exists": "Uma página com mesmo apêndice já existe", + "pages.url.error.move": "O apêndice não pôde ser alterado", + "pages.url.error.rights": "Você não pode alterar a URL desta página", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posição", + "pages.toggle.invisible": "Invisível", + "pages.toggle.publish": "Quer mesmo mudar o status desta página para **visível?**", + "pages.toggle.hide": "Quer mesmo mudar o status desta página para **invisível?**", + "pages.toggle.error.error": "O status da página de erro não pode ser alterado", + "pages.delete.headline": "Quer mesmo deletar esta página?", + "pages.delete.error.home.headline": "A página \"home\" não pode ser deletada", + "pages.delete.error.home.text": "Você está tentando deletar a página \"home\". O que não é possível e podería causar efeitos indesejados.", + "pages.delete.error.error.headline": "A página \"error\" não pode ser deletada", + "pages.delete.error.error.text": "Você está tentando deletar a página \"error\". O que não é possível e podería causar efeitos indesejados.", + "pages.delete.error.children.headline": "A página não pode ser deletada", + "pages.delete.error.children.text": "Esta página contém subpáginas e não pode ser deletada. Favor deletar todas subpáginas antes.", + "pages.delete.error.blocked.headline": "A página não pode ser deletada", + "pages.delete.error.blocked.text": "A página está bloqueada e não pode ser deletada.", + "pages.search.help": "Procurar páginas por URL. Navegue pelos resultados usando as teclas \"acima\" e \"abaixo\" e pressione \"enter\" para saltar à página selecionada.", + "pages.search.noresults": "Busca sem resultados. Favor tentar novamente com outra URL.", + "pages.error.missing": "Página não encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas em", + "subpages.index.back": "Voltar", + "subpages.index.add": "Adicionar nova página", + "subpages.index.add.first.text": "Página ainda sem subpáginas", + "subpages.index.add.first.button": "Adicione a primeira página", + "subpages.index.visible": "Páginas visíveis", + "subpages.index.visible.help": "Arraste as páginas invisíveis aquí para ordenar/torna-las visíveis.", + "subpages.index.invisible": "Páginas invisíveis", + "subpages.index.invisible.help": "Arraste as páginas visíveis aquí para desordenar/torna-las invisíveis", + "subpages.add.error": "Esta página não pode ter sub-páginas ", + "subpages.add.error.more": "Esta página não pode ter mais sub-páginas ", + "subpages.error.missing": "Página não encontrada", + "files": "Arquivos", + "files.index.headline": "Arquivos para", + "files.index.back": "Voltar", + "files.index.upload": "Subir novo arquivo", + "files.index.upload.first.text": "Página ainda sem arquivos", + "files.index.upload.first.button": "Suba o primeiro arquivo", + "files.index.edit": "Editar", + "files.index.delete": "Deletar", + "files.index.error.disabled": "Esta página não pode ter nenhum arquivo", + "files.add.error.max": "O numero máximo de arquivos desta página foi atingido.", + "files.add.error.extension.missing": "Você não pode subir arquivos sem extensão ", + "files.add.error.extension.forbidden": "Extensão de arquivo não permitida", + "files.add.error.mime.forbidden": "\"mime type\" não permitido", + "files.add.error.htaccess": "Arquivos \"htaccess\" não podem ser subidos", + "files.add.error.invisible": "Arquivos invisíveis não podem ser subidos", + "files.add.blueprint.type.error": "Esta página permite apenas: ", + "files.add.blueprint.size.error": "Esta página permite arquivos com tamanho de ", + "files.show.name.label": "Nome do arquivo", + "files.show.info.label": "Tipo / Tamanho / Dimensões", + "files.show.link.label": "Link público", + "files.show.open": "Exibir/baixar arquivo", + "files.show.back": "Voltar", + "files.show.replace": "Substituir", + "files.show.delete": "Deletar", + "files.show.error.rename": "O arquivo não pode ser renomeado", + "files.show.error.form": "Favor preencher todos campos corretamente", + "files.upload.drop": "Arraste os arquivos aquí…", + "files.upload.click": "…ou clique para subir", + "files.replace.drop": "Arraste o arquivo aquí…", + "files.replace.click": "…ou clique para substituir", + "files.replace.error.type": "O arquivo subido deve ser do mesmo tipo", + "files.delete.headline": "Quer mesmo deletar este arquivo?", + "files.error.missing.page": "Página não encontrada", + "files.error.missing.file": "Arquivo não encontrado", + "users": "Usuários", + "users.index.headline": "Todos usuários", + "users.index.add": "Adicionar novo usuário", + "users.index.edit": "Editar", + "users.index.delete": "Deletar", + "users.form.username.label": "Usuário", + "users.form.username.placeholder": "Seu nome de usuário", + "users.form.username.help": "Caracteres permitidos: a-z mininúsculas, 0-9 e hífens", + "users.form.username.readonly": "O nome de usuário nao pode ser alterado", + "users.form.firstname.label": "Primeiro nome", + "users.form.lastname.label": "Último nome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemplo.com", + "users.form.password.label": "Senha", + "users.form.password.confirm.label": "Confimar senha", + "users.form.password.new.label": "Nova senha", + "users.form.password.new.confirm.label": "Confirmar nova senha", + "users.form.password.new.help": "Deixe em branco para manter a senha atual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Papel", + "users.form.options.headline": "Opções de conta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Deletar conta", + "users.form.avatar.headline": "Foto do perfil", + "users.form.avatar.upload": "Subir foto do perfil", + "users.form.avatar.replace": "Substituir foto do perfil", + "users.form.avatar.delete": "Deletar foto do perfil", + "users.form.back": "Voltar para usuários", + "users.form.error.password.confirm": "Favor confirmar a senha", + "users.form.error.update": "O usuário não pode ser atualizado", + "users.form.error.update.rights": "Você não tem permissão para atualizar este usuário", + "users.form.error.create": "O usuário não pode ser criado", + "users.form.error.permissions.title": "A pasta \"account\" não tem permissão de escrita", + "users.form.error.permissions.text": "Favor se certificar que /site/accounts existe e possui permissão de escrita.", + "users.delete.headline": "Quer mesmo deletar este usuário?", + "users.delete.error": "Este usuário nao pode ser deletado", + "users.delete.error.permission": "Você não tem permissão para deletar usuários", + "users.delete.error.permission.single": "Você não tem permissão para deletar este usuário ", + "users.delete.error.lastadmin": "Você não pode deletar o último usuário administrador ", + "users.avatar.drop": "Arraste a foto do perfil aquí…", + "users.avatar.click": "…ou clique para subir", + "users.avatar.error.type": "Você pode subir somente arquivos JPG, PNG and GIF", + "users.avatar.error.folder.headline": "A pasta \"avatar\" não tem permissão de escrita", + "users.avatar.error.folder.text": "Favor criar a pasta /assets/avatars e permita que seja escrita para subir fotos do perfil.", + "users.avatar.error.permission": "Você não pode alterar o avatar", + "users.avatar.delete.error": "A foto do perfil não pode ser deletada", + "users.avatar.delete.error.permission": "Você não tem permissão para deletar o avatar deste usuário", + "users.avatar.delete.success": "A foto do perfil foi deletada", + "users.avatar.missing": "Este usuário não possui avatar", + "users.error.missing": "Usuário não encontrado", + "user.error.lastadmin": "Você é o único administrador. Isto não pode ser alterado.", + "form.error.missing": "O formulário não foi encontrado ", + "form.construct.error.invalid": "Método inválido de construção de formulário ", + "fields.required": "Requerido", + "fields.date.label": "Data", + "fields.date.months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "fields.date.weekdays": [ + "Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "caminho/da/pagina", + "fields.password.label": "Senha", + "fields.structure.add": "Adicionar", + "fields.structure.add.first": "Adicionar primeira entrada", + "fields.structure.empty": "Sem entradas ainda.", + "fields.structure.entry.error": "Item não encontrado ", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Salvar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Deletar", + "fields.structure.delete.label": "Quer mesmo deletar esta entrada?", + "fields.tags.label": "Tags", + "fields.tel.label": "Fone", + "fields.textarea.buttons.bold.label": "Texto negrito", + "fields.textarea.buttons.bold.text": "Texto negrito", + "fields.textarea.buttons.italic.label": "Texto itálico", + "fields.textarea.buttons.italic.text": "Texto itálico", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagem", + "fields.textarea.buttons.file.label": "Arquivo", + "fields.toggle.yes": "Sim", + "fields.toggle.no": "Não", + "fields.toggle.on": "Ligado", + "fields.toggle.off": "Desligado", + "fields.error.missing.controller": "O arquivo do \"field controller\" não foi encontrado ", + "fields.error.missing.class": "A classe do \"field controller\" não foi encontrada ", + "fields.error.route.invalid": "\"field route\" inválida ", + "fields.error.extended": "O \"field\" não pode ser extendido ", + "editor.link.url.label": "Inserir URL", + "editor.link.text.label": "Texto do link", + "editor.link.text.help": "O texto do link é opcional", + "editor.email.address.label": "Inserir endereço de email", + "editor.email.address.placeholder": "mail@exemplo.com", + "editor.email.text.label": "Texto do link", + "editor.email.text.help": "O texto do link é opcional", + "editor.file.empty": "Esta página não tem arquivos", + "editor.image.empty": "Esta página não tem imagens", + "autocomplete.method.error": "Método de auto-completar inválido", + "blueprints.error.default.missing": "Blueprint \"default\" não encontrada ", + "error": "Erro", + "error.headline": "Erro" +} \ No newline at end of file diff --git a/panel/app/translations/pt_BR/package.json b/panel/app/translations/pt_BR/package.json new file mode 100644 index 0000000..885ba0c --- /dev/null +++ b/panel/app/translations/pt_BR/package.json @@ -0,0 +1,4 @@ +{ + "title": "Português (Brasileiro)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/pt_PT/core.json b/panel/app/translations/pt_PT/core.json new file mode 100644 index 0000000..891471d --- /dev/null +++ b/panel/app/translations/pt_PT/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Cancelar", + "add": "Adicionar", + "addit": "Adicionar e editar", + "save": "Gravar", + "saved": "Gravado!", + "change": "Mudar", + "delete": "Apagar", + "insert": "Inserir", + "ok": "Ok", + "routes.error.invalid": "URL de panel inválida", + "controller.error.invalid": "Controlador inválido", + "controller.error.action": "Ação inválida", + "view.error.invalid": "Vista inválida:", + "options.show": "Exibir opções", + "options.hide": "Ocultar opções", + "installation": "Instalação", + "installation.check.headline": "Instalação do Painel Kirby", + "installation.check.text": "Kirby encontrou os seguintes problemas durante a instalação…", + "installation.check.retry": "Tenta novamente", + "installation.check.error": "Temos problemas!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts não tem permissão de escrita", + "installation.check.error.avatars": "/assets/avatars não tem permissão de escrita", + "installation.check.error.blueprints": "Por favor, adiciona uma pasta /site/blueprints", + "installation.check.error.content": "A pasta \"content\" e todas as subpastas e ficheiros devem ter permissão de escrita.", + "installation.check.error.thumbs": "A pasta \"thumbs\" deve ter permissão de escrita.", + "installation.signup.username.label": "Cria a tua primeira conta", + "installation.signup.username.placeholder": "Utilizador", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@exemplo.com", + "installation.signup.password.label": "Palavra-passe", + "installation.signup.language.label": "Idioma", + "installation.signup.button": "Cria a tua conta", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Utilizador", + "login.password.label": "Palavra-passe", + "login.error": "Utilizador ou Palavra-passe inválida", + "login.button": "Log in", + "login.log.error.permissions": "Ficheiro de log do login não é descritível.", + "logout": "Log out", + "topbar.error.class.definition": "Falta a definição topbar da classe:", + "dashboard": "Painel", + "dashboard.index.pages.title": "Páginas", + "dashboard.index.pages.edit": "Editar", + "dashboard.index.pages.add": "Adicionar", + "dashboard.index.site.title": "URL do teu site", + "dashboard.index.account.title": "A tua conta", + "dashboard.index.account.edit": "Editar", + "dashboard.index.metatags.title": "Variáveis do site", + "dashboard.index.metatags.edit": "Editar", + "dashboard.index.history.title": "Atualizações recentes", + "dashboard.index.history.text": "As últimas páginas modificadas por ti serão exibidas aqui para facilitar a sua consulta no futuro.", + "dashboard.index.license.title": "Licença Kirby", + "dashboard.index.license.text": "Aparentemente estas a executar Kirby num servidor público sem licença válida!\n\nPor favor apoia Kirby e (link: {compra} text: adquira agóra uma licença)\n\nSe já adquiriste uma licença, simplesmente adiciona-a ao teu ficheiro de configuração: (link: {docs} text: site/config/config.php)", + "metatags": "Variáveis do site", + "metatags.info": "Informação Kirby", + "metatags.license": "Licença Kirby", + "metatags.version.toolkit": "Versão toolkit", + "metatags.version.kirby": "Versão Kirby", + "metatags.version.panel": "Versão panel", + "metatags.back": "Voltar ao Painel", + "metatags.files": "Ficheiros do site", + "site.delete.error": "A site não pode ser apagada", + "pages.show.settings": "Configurações de página", + "pages.show.preview": "Abrir preview", + "pages.show.template": "Template", + "pages.show.changeurl": "Mudar URL", + "pages.show.invisible": "Estatuto: invisível", + "pages.show.visible": "Estatuto: visível", + "pages.show.changes.text": "Tens alteraçoes ainda não gravadas!", + "pages.show.changes.button": "Descartar", + "pages.show.delete": "Apagar esta página", + "pages.show.subpages.title": "Páginas", + "pages.show.subpages.edit": "Editar", + "pages.show.subpages.add": "Adicionar", + "pages.show.subpages.empty": "Esta página não contém subpáginas", + "pages.show.files.title": "Ficheiros", + "pages.show.files.edit": "Editar", + "pages.show.files.add": "Adicionar", + "pages.show.files.empty": "Esta página não contém ficheiros", + "pages.show.error.permissions.title": "Esta página não tem permissão de escrita", + "pages.show.error.permissions.text": "Por favor confere as permissões da pasta \"content\" e todos seus ficheiros", + "pages.show.error.permissions.retry": "Tentar novamente", + "pages.show.error.notitle.title": "O blueprint não possui um campo \"title\"", + "pages.show.error.notitle.text": "Por favor adiciona um campo \"title\" e tenta novamente", + "pages.show.error.notitle.retry": "Tenta novamente", + "pages.show.error.form": "Por favor preenche todos os campos corretamente", + "pages.add.title.label": "Adicionar página", + "pages.add.title.placeholder": "Título", + "pages.add.url.label": "URL-apêndice", + "pages.add.url.enter": "(introduz um título)", + "pages.add.url.close": "Fechar", + "pages.add.url.help": "Formato: a-z minúsculas, 0-9 e hífens", + "pages.add.template.label": "Template", + "pages.add.error.create": "Não foi possível criar a página", + "pages.add.error.title": "Falta o título", + "pages.add.error.template": "Falta o template", + "pages.add.error.max.headline": "Não são permitidas novas páginas", + "pages.add.error.max.text": "Número máximo de subpáginas para a página atual foi atingido.", + "pages.url.uid.label": "URL-apêndice", + "pages.url.uid.label.option": "Criar a partir do título", + "pages.url.error.exists": "Já existe uma página com mesmo apêndice", + "pages.url.error.move": "O apêndice não pôde ser alterado", + "pages.url.error.rights": "Não é possível mudar o URL desta página", + "pages.template.select.label": "Template", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Posição", + "pages.toggle.invisible": "Invisível", + "pages.toggle.publish": "Queres mesmo alterar o estatuto desta página para **visível**?", + "pages.toggle.hide": "Queres mesmo alterar o estatuto desta página para **invisível**?", + "pages.toggle.error.error": "O estatuto da pagina erro não pode ser alterado", + "pages.delete.headline": "Queres mesmo apagar esta página?", + "pages.delete.error.home.headline": "A página \"home\" não pode ser apagada", + "pages.delete.error.home.text": "Estás a tentar apagar a página \"home\". O que não é possível e poderia causar efeitos indesejados.", + "pages.delete.error.error.headline": "A página \"error\" não pode ser apagada", + "pages.delete.error.error.text": "Estás a tentar apagar a página \"error\". O que não é possível e poderia causar efeitos indesejados.", + "pages.delete.error.children.headline": "A página não pode ser apagada", + "pages.delete.error.children.text": "Esta página contém subpáginas e não pode ser apagada. Por favor apaga todas as subpáginas antes.", + "pages.delete.error.blocked.headline": "A página não pode ser apagada", + "pages.delete.error.blocked.text": "A página está bloqueada e não pode ser apagada.", + "pages.search.help": "Procurar páginas por URL. Navega pelos resultados usando as teclas \"cima\" e \"baixo\" e pressiona \"enter\" para saltar para a página selecionada.", + "pages.search.noresults": "Pesquisa sem resultados. Por favor tenta novamente com outra URL.", + "pages.error.missing": "Página não encontrada", + "subpages": "Páginas", + "subpages.index.headline": "Páginas em", + "subpages.index.back": "Voltar", + "subpages.index.add": "Adicionar nova página", + "subpages.index.add.first.text": "Página ainda sem subpáginas", + "subpages.index.add.first.button": "Adiciona a primeira página", + "subpages.index.visible": "Páginas visíveis", + "subpages.index.visible.help": "Arrasta as páginas invisíveis aqui para ordenar/torna-las visíveis.", + "subpages.index.invisible": "Páginas invisíveis", + "subpages.index.invisible.help": "Arrasta as páginas visíveis aqui para desordenar/torna-las invisíveis", + "subpages.add.error": "Página sem permissão de ter paginas inferiores", + "subpages.add.error.more": "Esta página não pode ter mais paginas inferiores", + "subpages.error.missing": "Página não encontrada", + "files": "Ficheiros", + "files.index.headline": "Ficheiros para", + "files.index.back": "Voltar", + "files.index.upload": "Faz upload dum novo ficheiro", + "files.index.upload.first.text": "Página ainda sem ficheiros", + "files.index.upload.first.button": "Faz upload do primeiro ficheiro", + "files.index.edit": "Editar", + "files.index.delete": "Apagar", + "files.index.error.disabled": "Página sem permissão de ter ficheiros", + "files.add.error.max": "O máximo de ficheiros foi atingido para esta página.", + "files.add.error.extension.missing": "Não podes fazer upload sem extensão", + "files.add.error.extension.forbidden": "Extensão proibida", + "files.add.error.mime.forbidden": "Tipo mime proibido", + "files.add.error.htaccess": "Não se pode fazer upload de ficheiros htaccess", + "files.add.error.invisible": "Não se pode fazer upload de ficheiros invisíveis", + "files.add.blueprint.type.error": "Pagina apenas permite:", + "files.add.blueprint.size.error": "A pagina apenas permite tamanho de", + "files.show.name.label": "Nome do ficheiro", + "files.show.info.label": "Tipo / Tamanho / Dimensões", + "files.show.link.label": "Link público", + "files.show.open": "Exibir/Download ficheiro", + "files.show.back": "Voltar", + "files.show.replace": "Substituir", + "files.show.delete": "Apagar", + "files.show.error.rename": "O ficheiro não pode ser renomeado", + "files.show.error.form": "Por favor preenche os todos campos corretamente", + "files.upload.drop": "Arrasta os ficheiros para aqui…", + "files.upload.click": "…ou clica para fazer upload", + "files.replace.drop": "Arrasta o ficheiros para aqui…", + "files.replace.click": "…ou clica para substituir", + "files.replace.error.type": "O ficheiro que fizeste upload deve ser do mesmo tipo", + "files.delete.headline": "Queres mesmo apagar este ficheiro?", + "files.error.missing.page": "Página não encontrada", + "files.error.missing.file": "Ficheiro não encontrado", + "users": "Utilizadores", + "users.index.headline": "Todos os utilizadores", + "users.index.add": "Adicionar novo utilizador", + "users.index.edit": "Editar", + "users.index.delete": "Apagar", + "users.form.username.label": "Utilizador", + "users.form.username.placeholder": "Nome do utilizador", + "users.form.username.help": "Caracteres permitidos: a-z mininúsculas, 0-9 e hífens", + "users.form.username.readonly": "O nome de utilizador nao pode ser alterado", + "users.form.firstname.label": "Primeiro nome", + "users.form.lastname.label": "Último nome", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@exemplo.com", + "users.form.password.label": "Palavra-passe", + "users.form.password.confirm.label": "Confima a Palavra-passe", + "users.form.password.new.label": "Nova Palavra-passe", + "users.form.password.new.confirm.label": "Confirma a nova Palavra-passe", + "users.form.password.new.help": "Deixa em branco para manter a Palavra-passe atual", + "users.form.language.label": "Idioma", + "users.form.role.label": "Role", + "users.form.options.headline": "Opções de conta", + "users.form.options.message": "Enviar email", + "users.form.options.delete": "Apagar conta", + "users.form.avatar.headline": "Foto do perfil", + "users.form.avatar.upload": "Upload foto do perfil", + "users.form.avatar.replace": "Substituir foto do perfil", + "users.form.avatar.delete": "Apagar foto do perfil", + "users.form.back": "Voltar para utilizadores", + "users.form.error.password.confirm": "Por favor confirmar a Palavra-passe", + "users.form.error.update": "O utilizador não pode ser atualizado", + "users.form.error.update.rights": "Sem autorização para fazeres update a este utilizador", + "users.form.error.create": "O utilizador não pode ser criado", + "users.form.error.permissions.title": "O folder \"account\" não tem permissão de escrita", + "users.form.error.permissions.text": "Por favor certifica-te que /site/accounts existe e possui permissão de escrita.", + "users.delete.headline": "Queres mesmo apagar este utilizador?", + "users.delete.error": "Este utilizador nao pode ser apagado", + "users.delete.error.permission": "Não tens autorização para apagar utilizadores", + "users.delete.error.permission.single": "Não tens autorização para apagar este utilizador", + "users.delete.error.lastadmin": "Não é possivel apagar o ultimo admin", + "users.avatar.drop": "Arrasta a foto do perfil aqui…", + "users.avatar.click": "…ou clica para fazer upload", + "users.avatar.error.type": "Apenas podes fazer upload de ficheiros JPG, PNG and GIF", + "users.avatar.error.folder.headline": "O folder \"avatar\" não tem permissão de escrita", + "users.avatar.error.folder.text": "Por favor cria o folder /assets/avatars e garante que tem permissão de escrita para poder fazer upload de fotos de perfil.", + "users.avatar.error.permission": "Não tens autorização para mudar o avatar", + "users.avatar.delete.error": "A foto do perfil não pode ser apagada", + "users.avatar.delete.error.permission": "Não tens autorização para apagar o avatar deste utilizador", + "users.avatar.delete.success": "A foto do perfil foi apagada", + "users.avatar.missing": "Este utilizador não tem avatar", + "users.error.missing": "Utilizador não encontrado", + "user.error.lastadmin": "Só es admin. Isto não pode ser mudado.", + "form.error.missing": "Não foi possível encontrar o formulário", + "form.construct.error.invalid": "Método inválido de construção do formulário", + "fields.required": "Obrigatório", + "fields.date.label": "Data", + "fields.date.months": [ + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "fields.date.weekdays": [ + "Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado" + ], + "fields.date.weekdays.short": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sáb" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@exemplo.com", + "fields.number.label": "Número", + "fields.number.placeholder": "#", + "fields.page.label": "Página", + "fields.page.placeholder": "caminho/da/pagina", + "fields.password.label": "Palavra-passe", + "fields.structure.add": "Adicionar", + "fields.structure.add.first": "Adicionar primeira entrada", + "fields.structure.empty": "Sem entradas ainda.", + "fields.structure.entry.error": "O item não foi encontrado", + "fields.structure.cancel": "Cancelar", + "fields.structure.save": "Gravar", + "fields.structure.edit": "Editar", + "fields.structure.delete": "Apagar", + "fields.structure.delete.label": "Queres mesmo apagar esta entrada?", + "fields.tags.label": "Tags", + "fields.tel.label": "Fone", + "fields.textarea.buttons.bold.label": "Texto negrito", + "fields.textarea.buttons.bold.text": "Texto negrito", + "fields.textarea.buttons.italic.label": "Texto itálico", + "fields.textarea.buttons.italic.text": "Texto itálico", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagem", + "fields.textarea.buttons.file.label": "Ficheiro", + "fields.toggle.yes": "Sim", + "fields.toggle.no": "Não", + "fields.toggle.on": "Ligado", + "fields.toggle.off": "Desligado", + "fields.error.missing.controller": "Falta o ficheiro do controlador de campo ", + "fields.error.missing.class": "Falta a classe do controlador de campo", + "fields.error.route.invalid": "Rota de campo inválida", + "fields.error.extended": "Não é possível extender o campo", + "editor.link.url.label": "Inserir URL", + "editor.link.text.label": "Texto do link", + "editor.link.text.help": "O texto do link é opcional", + "editor.email.address.label": "Inserir endereço de email", + "editor.email.address.placeholder": "mail@exemplo.com", + "editor.email.text.label": "Texto do link", + "editor.email.text.help": "O texto do link é opcional", + "editor.file.empty": "Esta página não tem ficheiros", + "editor.image.empty": "Esta página não tem imagens", + "autocomplete.method.error": "Método inválido de auto-contemplação", + "blueprints.error.default.missing": "Falta do blueprint padrão", + "error": "Erro", + "error.headline": "Erro" +} \ No newline at end of file diff --git a/panel/app/translations/pt_PT/package.json b/panel/app/translations/pt_PT/package.json new file mode 100644 index 0000000..733fd36 --- /dev/null +++ b/panel/app/translations/pt_PT/package.json @@ -0,0 +1,4 @@ +{ + "title": "Português (Portugal)", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ro/core.json b/panel/app/translations/ro/core.json new file mode 100644 index 0000000..f783c75 --- /dev/null +++ b/panel/app/translations/ro/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Renunţă", + "add": "Adaugă", + "addit": "Add & Edit", + "save": "Salvează", + "saved": "Salvat!", + "change": "Modifică", + "delete": "Şterge", + "insert": "Inserează", + "ok": "Ok", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "Arată opţiunile", + "options.hide": "Ascunde opţiunile", + "installation": "Instalare", + "installation.check.headline": "Instalarea panoului de administrare", + "installation.check.text": "Sistemul a întâmpinat următoarele probleme în timpul instalării:", + "installation.check.retry": "Ignoră", + "installation.check.error": "Sunt câteva probleme!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Directorul /site/accounts nu are permisiuni de scriere.", + "installation.check.error.avatars": "Directorul /assets/avatars nu are permisiuni de scriere.", + "installation.check.error.blueprints": "Creează te rog directorul /site/blueprints!", + "installation.check.error.content": "Directorul /content şi conţinutul lui trebuie să aibă permisiuni de scriere.", + "installation.check.error.thumbs": "Directorul /thumbs trebuie să aibă permisiuni de scriere.", + "installation.signup.username.label": "Creează primul cont de utilizator!", + "installation.signup.username.placeholder": "Nume utilizator", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "email@exemplu.com", + "installation.signup.password.label": "Parola", + "installation.signup.language.label": "Limba", + "installation.signup.button": "Creează contul", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Nume utilizator", + "login.password.label": "Parola", + "login.error": "Nume de utilizator greşit, sau parolă invalidă.", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Panoul de control", + "dashboard.index.pages.title": "Pagini", + "dashboard.index.pages.edit": "Editează", + "dashboard.index.pages.add": "Adaugă", + "dashboard.index.site.title": "Adresa URL a saitului", + "dashboard.index.account.title": "Contul tău", + "dashboard.index.account.edit": "Editează", + "dashboard.index.metatags.title": "Variabile sait", + "dashboard.index.metatags.edit": "Editează", + "dashboard.index.history.title": "Ultimele pagini modificate", + "dashboard.index.history.text": "Paginile modificate recent vor fi afişate aici pentru a putea fi găsite uşor mai târziu.", + "dashboard.index.license.title": "Licenţa Kirby", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "Variabile sait", + "metatags.info": "Info Kirby", + "metatags.license": "Licenţa Kirby", + "metatags.version.toolkit": "Versiune Toolkit", + "metatags.version.kirby": "Versiune Kirby", + "metatags.version.panel": "Versiune Panel", + "metatags.back": "Înapoi la panoul de control", + "metatags.files": "Fişiere sait", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "Setări pagină", + "pages.show.preview": "Previzualizare", + "pages.show.template": "Tip pagină", + "pages.show.changeurl": "Modifică URL", + "pages.show.invisible": "Status: invizibilă", + "pages.show.visible": "Status: vizibilă", + "pages.show.changes.text": "Există modificări nesalvate!", + "pages.show.changes.button": "Renunţă", + "pages.show.delete": "Şterge această pagină", + "pages.show.subpages.title": "Pagini", + "pages.show.subpages.edit": "Editează", + "pages.show.subpages.add": "Adaugă", + "pages.show.subpages.empty": "Această pagină nu are subpagini.", + "pages.show.files.title": "Fişiere", + "pages.show.files.edit": "Editează", + "pages.show.files.add": "Adaugă", + "pages.show.files.empty": "Această pagină nu conţine fişiere", + "pages.show.error.permissions.title": "Pagina nu are permisiuni de scriere.", + "pages.show.error.permissions.text": "Verifică permisiunile directorului /content si a conţinutului său.", + "pages.show.error.permissions.retry": "Ignoră", + "pages.show.error.notitle.title": "Şablonul nu conţine un câmp pentru titlu.", + "pages.show.error.notitle.text": "Adaugă câmpul pentru titlu în şablon şi incearcă din nou.", + "pages.show.error.notitle.retry": "Ignoră", + "pages.show.error.form": "Completează corect toate câmpurile, te rog!", + "pages.add.title.label": "Adaugă o pagina nouă", + "pages.add.title.placeholder": "Titlu", + "pages.add.url.label": "Terminaţia URL", + "pages.add.url.enter": "(completează titlul)", + "pages.add.url.close": "Închide", + "pages.add.url.help": "Caractere permise: litere mici de la a la z, cifre şi cratime.", + "pages.add.template.label": "Tip pagină", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "Lipseşte titlul", + "pages.add.error.template": "Nu a fost selectat tipul paginii!", + "pages.add.error.max.headline": "Nu sunt permise pagini noi!", + "pages.add.error.max.text": "A fost atins numărul maxim de subpagini pentru pagina aceasta.", + "pages.url.uid.label": "Terminaţia URL", + "pages.url.uid.label.option": "Foloseşte titlul", + "pages.url.error.exists": "Există deja o pagină cu aceeaşi terminaţie URL!", + "pages.url.error.move": "Terminaţia URL nu poate fi modificată!", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "Tip pagină", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Sigur vrei sa faci **vizibilă** această pagină?", + "pages.toggle.hide": "Sigur vrei sa faci **invizibilă** această pagină?", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "Sigur vrei sa ştergi această pagină?", + "pages.delete.error.home.headline": "Pagina principală nu poate fi ştearsă!", + "pages.delete.error.home.text": "Ai încercat să ştergi pagina principală a saitului. Acest lucru nu e posibil pentru că ar apărea efecte nedorite.", + "pages.delete.error.error.headline": "Pagina de eroare nu poate fi ştearsă!", + "pages.delete.error.error.text": "Ai încercat să ştergi pagina de eroare a saitului. Acest lucru nu e posibil pentru că ar apărea efecte nedorite.", + "pages.delete.error.children.headline": "Pagina nu poate fi ştearsă!", + "pages.delete.error.children.text": "Această pagină conţine subpagini şi nu poate fi ştearsă. Şterge te rog subpaginile mai întâi.", + "pages.delete.error.blocked.headline": "Pagina nu poate fi ştearsă!", + "pages.delete.error.blocked.text": "Această pagină este blocată şi nu poate fi ştearsă.", + "pages.search.help": "Caută pagini în funcţie de URL. Navighează printre rezultatele afişate cu ajutorul săgeţilor de pe tastatură şi apasă Enter pentru a accesa pagina căutată.", + "pages.search.noresults": "Nu există rezultate pentru căutarea făcută. Încearcă din nou folosind un URL diferit.", + "pages.error.missing": "Pagina nu poate fi găsită!", + "subpages": "Pagini", + "subpages.index.headline": "Subpagini", + "subpages.index.back": "Înapoi", + "subpages.index.add": "Adaugă o pagină nouă", + "subpages.index.add.first.text": "Această pagină nu conţine încă subpagini.", + "subpages.index.add.first.button": "Adaugă o primă subpagină", + "subpages.index.visible": "Pagini vizibile", + "subpages.index.visible.help": "Trage aici pagini pentru a le face vizibile.", + "subpages.index.invisible": "Pagini invizibile", + "subpages.index.invisible.help": "Trage aici pagini pentru a le face invizibile.", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "Pagina nu a putut fi găsită!", + "files": "Fişiere", + "files.index.headline": "Fişiere", + "files.index.back": "Înapoi", + "files.index.upload": "Încarcă un nou fişier", + "files.index.upload.first.text": "Această pagină nu conţine încă niciun fişier.", + "files.index.upload.first.button": "Încarcă un prim fişier", + "files.index.edit": "Editează", + "files.index.delete": "Şterge", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Nume fişier", + "files.show.info.label": "Tip / Mărime / Dimensiuni", + "files.show.link.label": "Calea către imagine", + "files.show.open": "Afişează / descarcă fişierul", + "files.show.back": "Înapoi", + "files.show.replace": "Înlocuieşte", + "files.show.delete": "Şterge", + "files.show.error.rename": "Fişierul nu a putut fi redenumit!", + "files.show.error.form": "Completează toate câmpurile corect, te rog!", + "files.upload.drop": "Trage fişiere aici…", + "files.upload.click": "…sau click pentru a le încărca", + "files.replace.drop": "Trage un fişier aici…", + "files.replace.click": "…sau click pentru a-l înlocui", + "files.replace.error.type": "Fişierul încărcat trebuie să fie de acelaşi tip!", + "files.delete.headline": "Sigur vrei să ştergi acest fişier?", + "files.error.missing.page": "Pagina nu a putut fi găsită!", + "files.error.missing.file": "Fişierul nu a putut fi găsit!", + "users": "Utilizatori", + "users.index.headline": "Toţi utilizatorii", + "users.index.add": "Adaugă un utilizator nou", + "users.index.edit": "Editează", + "users.index.delete": "Şterge", + "users.form.username.label": "Nume utilizator", + "users.form.username.placeholder": "Numele tău de utilizator", + "users.form.username.help": "Caractere permise: litere mici de la a la z, cifre şi cratime.", + "users.form.username.readonly": "Numele de utilizator nu poate fi schimbat!", + "users.form.firstname.label": "Prenume", + "users.form.lastname.label": "Nume", + "users.form.email.label": "Email", + "users.form.email.placeholder": "email@exemplu.com", + "users.form.password.label": "Parola", + "users.form.password.confirm.label": "Confirmare parolă", + "users.form.password.new.label": "Parolă nouă", + "users.form.password.new.confirm.label": "Confirmarea parolei noi", + "users.form.password.new.help": "Lasă necompletat pentru a păstra parola curentă.", + "users.form.language.label": "Limba", + "users.form.role.label": "Rol", + "users.form.options.headline": "Opţiuni cont", + "users.form.options.message": "Trimite email", + "users.form.options.delete": "Şterge contul", + "users.form.avatar.headline": "Poza de profil", + "users.form.avatar.upload": "Încarcă o poză de profil", + "users.form.avatar.replace": "Înlocuieşte poza de profil", + "users.form.avatar.delete": "Şterge poza de profil", + "users.form.back": "Înapoi la utilizatori", + "users.form.error.password.confirm": "Confirmă parola, te rog!", + "users.form.error.update": "Contul de utilizator nu a putut fi actualizat!", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Contul de utilizator nu a putut fi creat!", + "users.form.error.permissions.title": "Directorul /site/accounts nu are permisiuni de scriere.", + "users.form.error.permissions.text": "Verifică dacă directorul /site/accounts există şi are permisiuni de scriere.", + "users.delete.headline": "Sigur vrei să ştergi acest cont de utilizator?", + "users.delete.error": "Contul de utilizator nu a putut fi şters!", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "Trage o poză de profil aici…", + "users.avatar.click": "…sau click pentru încărcare", + "users.avatar.error.type": "Poţi încărca doar fişiere JPG, PNG sau GIF.", + "users.avatar.error.folder.headline": "Directorul /assets/avatars nu are permisiuni de scriere.", + "users.avatar.error.folder.text": "Verifică dacă directorul /assets/avatars există şi are permisiuni de scriere.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Poza de profil nu a putut fi ştearsă!", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Poza de profil a fost ştearsă!", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "Contul de utilizator nu a putut fi găsit!", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Obligatoriu", + "fields.date.label": "Data", + "fields.date.months": [ + "Ianuarie", + "Februarie", + "Martie", + "Aprilie", + "Mai", + "Iunie", + "Iulie", + "August", + "Septembrie", + "Octombrie", + "Noiembrie", + "Decembrie" + ], + "fields.date.weekdays": [ + "Duminica", + "Luni", + "Marţi", + "Miercuri", + "Joi", + "Vineri", + "Sâmbătă" + ], + "fields.date.weekdays.short": [ + "Dum", + "Lun", + "Mar", + "Mie", + "Joi", + "Vin", + "Sâm" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "email@exemplu.com", + "fields.number.label": "Număr", + "fields.number.placeholder": "#", + "fields.page.label": "Pagină", + "fields.page.placeholder": "calea/către/pagină", + "fields.password.label": "Parola", + "fields.structure.add": "Adaugă", + "fields.structure.add.first": "Completează formularul!", + "fields.structure.empty": "Nicio inserţie.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Renunţă", + "fields.structure.save": "Salvează", + "fields.structure.edit": "Editează", + "fields.structure.delete": "Şterge", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Etichete", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Text îngroşat", + "fields.textarea.buttons.bold.text": "Text îngroşat", + "fields.textarea.buttons.italic.label": "Text înclinat", + "fields.textarea.buttons.italic.text": "Text înclinat", + "fields.textarea.buttons.link.label": "Link", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "Imagine", + "fields.textarea.buttons.file.label": "Fişier", + "fields.toggle.yes": "Da", + "fields.toggle.no": "Nu", + "fields.toggle.on": "On", + "fields.toggle.off": "Off", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Inserează URL", + "editor.link.text.label": "Link text", + "editor.link.text.help": "Linkul text este opţional.", + "editor.email.address.label": "Inserează adresa email", + "editor.email.address.placeholder": "email@exemplu.com", + "editor.email.text.label": "Link text", + "editor.email.text.help": "Linkul text este opţional.", + "editor.file.empty": "Această pagină nu conţine fişiere.", + "editor.image.empty": "Această pagină nu conţine imagini.", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Eroare", + "error.headline": "Eroare" +} \ No newline at end of file diff --git a/panel/app/translations/ro/package.json b/panel/app/translations/ro/package.json new file mode 100644 index 0000000..a0e3e8a --- /dev/null +++ b/panel/app/translations/ro/package.json @@ -0,0 +1,4 @@ +{ + "title": "Română", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/ru/core.json b/panel/app/translations/ru/core.json new file mode 100644 index 0000000..c58a509 --- /dev/null +++ b/panel/app/translations/ru/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Отменить", + "add": "Добавить", + "addit": "Добавить и Изменить", + "save": "Сохранить", + "saved": "Сохранено", + "change": "Изменить", + "delete": "Удалить", + "insert": "Вставить", + "ok": "Ок", + "routes.error.invalid": "Неверный URL Панели", + "controller.error.invalid": "Неверный контроллер", + "controller.error.action": "Неверное действие", + "view.error.invalid": "Неверный вид:", + "options.show": "Показать опции", + "options.hide": "Скрыть опции", + "installation": "Установка", + "installation.check.headline": "Установка панели Kirby", + "installation.check.text": "Во время установки Kirby возникли следующие проблемы…", + "installation.check.retry": "Повторить", + "installation.check.error": "Не все прошло так гладко :(", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts не доступно для записи", + "installation.check.error.avatars": "/assets/avatars не доступно для записи", + "installation.check.error.blueprints": "Пожалуйста, создайте папку /site/blueprints", + "installation.check.error.content": "Папка content в все вложенные папки и файлы должны быть доступны для записи.", + "installation.check.error.thumbs": "Папка thumbs должна быть доступна для записи.", + "installation.signup.username.label": "Создайте первый аккаунт пользователя", + "installation.signup.username.placeholder": "Логин", + "installation.signup.email.label": "Эл.почта", + "installation.signup.email.placeholder": "pochta@domen.com", + "installation.signup.password.label": "Пароль", + "installation.signup.language.label": "Язык", + "installation.signup.button": "Создать аккаунт", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Логин", + "login.password.label": "Пароль", + "login.error": "Неверный логин или пароль", + "login.button": "Log in", + "login.log.error.permissions": "Файл журнала входов не доступен для записи.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Панель управления", + "dashboard.index.pages.title": "Страницы", + "dashboard.index.pages.edit": "Настроить", + "dashboard.index.pages.add": "Добавить", + "dashboard.index.site.title": "Адрес вашего сайта", + "dashboard.index.account.title": "Ваш аккаунт", + "dashboard.index.account.edit": "Настроить", + "dashboard.index.metatags.title": "Переменные сайта", + "dashboard.index.metatags.edit": "Настроить", + "dashboard.index.history.title": "Ваши последние правки", + "dashboard.index.history.text": "Последние измененные вами страницы будут показаны здесь для быстрого доступа к ним.", + "dashboard.index.license.title": "Лицензия Kirby", + "dashboard.index.license.text": "Кажется, вы запустили Kirby на общедоступном сервере без действующей лицензии!\n\nПожалуйста, поддержите Kirby и (link: {buy} text: купите лицензию)\n\nЕсли у вас уже есть лицензионный ключ, просто добавьте его в ваш конфигурационный файл: (link: {docs} text: site/config/config.php)", + "metatags": "Переменные сайта", + "metatags.info": "Информация о Kirby", + "metatags.license": "Лицензия Kirby", + "metatags.version.toolkit": "Версия Инструментария", + "metatags.version.kirby": "Версия Kirby", + "metatags.version.panel": "Версия панели", + "metatags.back": "Назад в панель управления", + "metatags.files": "Файлы сайта", + "site.delete.error": "Сайт не может быть удален", + "pages.show.settings": "Настройки страницы", + "pages.show.preview": "Предпросмотр", + "pages.show.template": "Шаблон", + "pages.show.changeurl": "Изменить ссылку (ЧПУ)", + "pages.show.invisible": "Статус: не отображается", + "pages.show.visible": "Статус: отображается", + "pages.show.changes.text": "Вы не сохранили изменения!", + "pages.show.changes.button": "Сброс", + "pages.show.delete": "Удалить эту страницу", + "pages.show.subpages.title": "Страницы", + "pages.show.subpages.edit": "Настроить", + "pages.show.subpages.add": "Добавить", + "pages.show.subpages.empty": "Для этой страницы нет подстраниц", + "pages.show.files.title": "Файлы", + "pages.show.files.edit": "Настроить", + "pages.show.files.add": "Добавить", + "pages.show.files.empty": "Для этой страницы нет файлов", + "pages.show.error.permissions.title": "Эта страница не доступна для записи", + "pages.show.error.permissions.text": "Пожалуйста, проверьте права для папки content и всех файлов.", + "pages.show.error.permissions.retry": "Повторить", + "pages.show.error.notitle.title": "В этом шаблоне формы должно быть поле для названия", + "pages.show.error.notitle.text": "Пожалуйста, заполните название и повторите снова", + "pages.show.error.notitle.retry": "Повторить", + "pages.show.error.form": "Пожалуйста, заполните все необходимые поля корректно", + "pages.add.title.label": "Создать новую страницу", + "pages.add.title.placeholder": "Название", + "pages.add.url.label": "понятная ссылка (ЧПУ)", + "pages.add.url.enter": "(введите название страницы)", + "pages.add.url.close": "Закрыть", + "pages.add.url.help": "Формат: нижние латинские буквы, цифры и дефисы", + "pages.add.template.label": "Шаблон", + "pages.add.error.create": "Страница не может быть создана", + "pages.add.error.title": "Отсутствует название", + "pages.add.error.template": "Отсутствует шаблон", + "pages.add.error.max.headline": "Предельное количество страниц", + "pages.add.error.max.text": "Для данной страницы достигнут максимальный предел подстраниц.", + "pages.url.uid.label": "ссылка (ЧПУ)", + "pages.url.uid.label.option": "сформировать", + "pages.url.error.exists": "Страница с такой же понятной ссылкой (ЧПУ) уже существует", + "pages.url.error.move": "Понятная ссылка (ЧПУ) не может быть изменена", + "pages.url.error.rights": "Вы не можете изменить URL этой страницы", + "pages.template.select.label": "Шаблон", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Позиция", + "pages.toggle.invisible": "не отображается", + "pages.toggle.publish": "Вы действительно хотите изменить статус этой страницы на **отображается?**", + "pages.toggle.hide": "Вы действительно хотите изменить статус этой страницы на **не отображается?**", + "pages.toggle.error.error": "Статус страницы ошибки не может быть изменен", + "pages.delete.headline": "Вы действительно хотите удалить эту страницу?", + "pages.delete.error.home.headline": "Индексная (домашняя) страница не может быть удалена", + "pages.delete.error.home.text": "Вы пытаетесь удалить индексную (домашнюю) страницу. Это невозможно, так как может привести к непредсказуемым результатам.", + "pages.delete.error.error.headline": "Страница ошибок (404) не может быть удалена", + "pages.delete.error.error.text": "Вы пытаетесь удалить страницу ошибок (Error 404 Page). Это невозможно, так как может привести к непредсказуемым результатам.", + "pages.delete.error.children.headline": "Страница не может быть удалена", + "pages.delete.error.children.text": "Для этой страницы существуют подстраницы. Пожалуйста, удалите сначала подстраницы.", + "pages.delete.error.blocked.headline": "Эта страница не может быть удалена", + "pages.delete.error.blocked.text": "Эта страница заблокирована и не может быть удалена в настоящий момент.", + "pages.search.help": "Поиск страниц по ссылкам. Для перемещения по результатам поиска используйте стрелки на клавиатуре ВВЕРХ и ВНИЗ. Для открытия страницы, нажмите ВВОД.", + "pages.search.noresults": "Нет результатов по вашему запросу. Пожалуйста, проверьте строку поиска.", + "pages.error.missing": "Страница не найдена", + "subpages": "Страницы", + "subpages.index.headline": "Страниц для", + "subpages.index.back": "Назад", + "subpages.index.add": "Добавить новую страницу", + "subpages.index.add.first.text": "Для этой страницы пока нет подстраниц", + "subpages.index.add.first.button": "Добавить первую страницу", + "subpages.index.visible": "Видимые страницы", + "subpages.index.visible.help": "Перетащите невидимые страницы сюда для их публикации (сортировки в меню).", + "subpages.index.invisible": "Невидимые страницы", + "subpages.index.invisible.help": "Перетащите видимые страницы сюда для их сокрытия (удаления из меню).", + "subpages.add.error": "Эта страница не имеет подстраниц", + "subpages.add.error.more": "Эта страница не может иметь больше подстраниц", + "subpages.error.missing": "Страница не найдена", + "files": "Файлы", + "files.index.headline": "Файлов для", + "files.index.back": "назад", + "files.index.upload": "Закачать новый файл", + "files.index.upload.first.text": "Для этой страницы пока нет файлов", + "files.index.upload.first.button": "Закачать первый файл", + "files.index.edit": "Настроить", + "files.index.delete": "Удалить", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "Имя файла", + "files.show.info.label": "Тип / размер / Разрешение", + "files.show.link.label": "Публичная ссылка", + "files.show.open": "Показать/скачать файл", + "files.show.back": "Назад", + "files.show.replace": "Заменить", + "files.show.delete": "Удалить", + "files.show.error.rename": "Файл не может быть переименован", + "files.show.error.form": "Пожалуйста, заполните все необходимые поля корректно", + "files.upload.drop": "Перетащите файлы сюда…", + "files.upload.click": "…или кликните для выбора", + "files.replace.drop": "Перетащите файлы сюда…", + "files.replace.click": "…или кликните для выбора", + "files.replace.error.type": "Закачиваемый файл должен иметь такое же расширение (тип)", + "files.delete.headline": "Вы действительно хотите удалить файл?", + "files.error.missing.page": "Страница не найдена", + "files.error.missing.file": "Файл не найден", + "users": "Пользователи", + "users.index.headline": "Все пользователи", + "users.index.add": "Добавить нового пользователя", + "users.index.edit": "Настроить", + "users.index.delete": "Удалить", + "users.form.username.label": "Логин", + "users.form.username.placeholder": "Ваш логин", + "users.form.username.help": "Формат: нижние латинские буквы, цифры и дефисы", + "users.form.username.readonly": "Логин не может быть изменен", + "users.form.firstname.label": "Имя", + "users.form.lastname.label": "Фамилия", + "users.form.email.label": "Эл.почта", + "users.form.email.placeholder": "pochta@domen.com", + "users.form.password.label": "Пароль", + "users.form.password.confirm.label": "Подтвердите пароль", + "users.form.password.new.label": "Новый пароль", + "users.form.password.new.confirm.label": "Подтвердите новый пароль", + "users.form.password.new.help": "Оставьте пустым, чтобы не менять пароль", + "users.form.language.label": "Язык", + "users.form.role.label": "Роль", + "users.form.options.headline": "Опции аккаунта", + "users.form.options.message": "Отправить эл.почту", + "users.form.options.delete": "Удалить аккаунт", + "users.form.avatar.headline": "Аватар (фото)", + "users.form.avatar.upload": "Закачать картинку для аккаунта", + "users.form.avatar.replace": "Заменить картинку для аккаунта", + "users.form.avatar.delete": "Удалить картинку для аккаунта", + "users.form.back": "назад к аккаунтам", + "users.form.error.password.confirm": "Пожалуйста, подтвердите пароль", + "users.form.error.update": "Аккаунт не может быть изменен", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "Аккаунт не может быть создан", + "users.form.error.permissions.title": "Папка account не доступна для записи", + "users.form.error.permissions.text": "Пожалуйста, убедитесь, что папка /site/accounts существует и доступна для записи.", + "users.delete.headline": "Вы действительно хотите удалить аккаунт?", + "users.delete.error": "Аккаунт не может быть удален", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "Вы не можете удалить единственного администратора", + "users.avatar.drop": "Перетащите картинку для аккаунта сюда…", + "users.avatar.click": "…или кликните для выбора", + "users.avatar.error.type": "Формат файлов картинок может быть JPG, PNG или GIF", + "users.avatar.error.folder.headline": "Папка avatar не доступна для записи", + "users.avatar.error.folder.text": "Пожалуйста, убедитесь, что папка /assets/avatars существует и доступна для записи перед добавлением аватаров (фото) к аккаунтам.", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "Аватар (фото) к аккаунту не может быть удален", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "Аватар (фото) к аккаунту удален", + "users.avatar.missing": "У пользователя нет аватара", + "users.error.missing": "Аккаунт не найден", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "Необходимо", + "fields.date.label": "Дата", + "fields.date.months": [ + "Январь", + "Февраль", + "Март", + "Апрель", + "Май", + "Июнь", + "Июль", + "Август", + "Сентябрь", + "Октябрь", + "Ноябрь", + "Декабрь" + ], + "fields.date.weekdays": [ + "Воскресенье", + "Понедельник", + "Вторник", + "Среда", + "Четверг", + "Пятница", + "Суббота" + ], + "fields.date.weekdays.short": [ + "Вс", + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб" + ], + "fields.email.label": "Эл.почта", + "fields.email.placeholder": "pochta@domen.com", + "fields.number.label": "Номер", + "fields.number.placeholder": "#", + "fields.page.label": "Страница", + "fields.page.placeholder": "путь/к/странице", + "fields.password.label": "Пароль", + "fields.structure.add": "Добавить", + "fields.structure.add.first": "Добавить первую запись", + "fields.structure.empty": "Пока нет записей.", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "Отмена", + "fields.structure.save": "Сохранить", + "fields.structure.edit": "Настроить", + "fields.structure.delete": "Удалить", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "Тэги", + "fields.tel.label": "Телефон", + "fields.textarea.buttons.bold.label": "Жирный шрифт", + "fields.textarea.buttons.bold.text": "Жирный шрифт", + "fields.textarea.buttons.italic.label": "Наклонный шрифт", + "fields.textarea.buttons.italic.text": "Наклонный шрифт", + "fields.textarea.buttons.link.label": "Ссылка", + "fields.textarea.buttons.email.label": "Эл.почта", + "fields.textarea.buttons.image.label": "Картинка", + "fields.textarea.buttons.file.label": "Файл", + "fields.toggle.yes": "Да", + "fields.toggle.no": "Нет", + "fields.toggle.on": "Вкл", + "fields.toggle.off": "Выкл", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "Вставить ссылку", + "editor.link.text.label": "Текст ссылки", + "editor.link.text.help": "Текст ссылки не обязателен", + "editor.email.address.label": "Введите адрес эл.почты", + "editor.email.address.placeholder": "pochta@domen.com", + "editor.email.text.label": "Текст ссылки эл.почты", + "editor.email.text.help": "Текст ссылки эл.почты не обязателен", + "editor.file.empty": "Для этой страницы нет файлов", + "editor.image.empty": "Для этой страницы нет картинок", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "Ошибка", + "error.headline": "Ошибка" +} \ No newline at end of file diff --git a/panel/app/translations/ru/package.json b/panel/app/translations/ru/package.json new file mode 100644 index 0000000..6d6c6ec --- /dev/null +++ b/panel/app/translations/ru/package.json @@ -0,0 +1,4 @@ +{ + "title": "Русский (Russian)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/sv_SE/core.json b/panel/app/translations/sv_SE/core.json new file mode 100644 index 0000000..e5cafc8 --- /dev/null +++ b/panel/app/translations/sv_SE/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "Avbryt", + "add": "Lägg till", + "addit": "Lägg till & redigera", + "save": "Spara", + "saved": "Sparad!", + "change": "Spara ändring", + "delete": "Radera", + "insert": "Infoga", + "ok": "Ok", + "routes.error.invalid": "Ogiltig URL för panel", + "controller.error.invalid": "Ogiltig kontroll", + "controller.error.action": "Ogiltig åtgärd", + "view.error.invalid": "Ogiltig vy:", + "options.show": "Visa alternativ", + "options.hide": "Göm alternativ", + "installation": "Installation", + "installation.check.headline": "Kirby Panel-installation", + "installation.check.text": "Kirby påträffade följande fel under installationen…", + "installation.check.retry": "Försök igen", + "installation.check.error": "Det finns några problem!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "Mappen /site/accounts saknar skrivrättigheter", + "installation.check.error.avatars": "Mappen /assets/avatars saknar skrivrättigheter", + "installation.check.error.blueprints": "Vänligen lägg till en mapp för /site/blueprints", + "installation.check.error.content": "Innehållet i mappen, alla dess filer och undermappar, måste ha skrivrättigheter.", + "installation.check.error.thumbs": "Mappen /thumbs måste ha skrivrättigheter.", + "installation.signup.username.label": "Skapa ditt första konto", + "installation.signup.username.placeholder": "Användarnamn", + "installation.signup.email.label": "E-mail", + "installation.signup.email.placeholder": "namn@exempel.se", + "installation.signup.password.label": "Lösenord", + "installation.signup.language.label": "Språk", + "installation.signup.button": "Skapa ett konto", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "Användarnamn", + "login.password.label": "Lösenord", + "login.error": "Fel användarnamn eller lösenord", + "login.button": "Log in", + "login.log.error.permissions": "Logg-filen för inloggningar är ej skrivbar.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "Kontrollpanel", + "dashboard.index.pages.title": "Sidor", + "dashboard.index.pages.edit": "Ändra", + "dashboard.index.pages.add": "Lägg till", + "dashboard.index.site.title": "Webbplatsens webbadress", + "dashboard.index.account.title": "Ditt konto", + "dashboard.index.account.edit": "Ändra", + "dashboard.index.metatags.title": "Webbplatsens metadata", + "dashboard.index.metatags.edit": "Ändra", + "dashboard.index.history.title": "Senast ändrade sidor", + "dashboard.index.history.text": "Dina senast ändrade sidor kommer att listas här för att underlätta att hitta dem igen senare.", + "dashboard.index.license.title": "Kirbylicens", + "dashboard.index.license.text": "Det verkar som att du kör Kirby på en publik server utan giltig licens. \n\nVänligen, stöd Kirby genom att (link: {buy} text: köpa en licens nu).\n\nOm du redan har en licens, se till att lägga till den i din konfigurationsfil: (link: {docs} text: site/config/config.php)", + "metatags": "Webbplatsens metadata", + "metatags.info": "Information om Kirby", + "metatags.license": "Kirbylicens", + "metatags.version.toolkit": "Version av Toolkit", + "metatags.version.kirby": "Version av Kirby", + "metatags.version.panel": "Version av Panel", + "metatags.back": "Tillbaka till kontrollpanelen", + "metatags.files": "Webbplatsfiler", + "site.delete.error": "Sidan kan inte raderas", + "pages.show.settings": "Inställningar för sida", + "pages.show.preview": "Förhandsvisa sida", + "pages.show.template": "Mall", + "pages.show.changeurl": "Ändra webbadress", + "pages.show.invisible": "Dold i meny", + "pages.show.visible": "Synlig i meny", + "pages.show.changes.text": "Du har ändringar som inte sparats!", + "pages.show.changes.button": "Skrota", + "pages.show.delete": "Radera sida", + "pages.show.subpages.title": "Undersidor", + "pages.show.subpages.edit": "Ändra", + "pages.show.subpages.add": "Lägg till", + "pages.show.subpages.empty": "Den här sidan har inga undersidor.", + "pages.show.files.title": "Filer", + "pages.show.files.edit": "Ändra", + "pages.show.files.add": "Lägg till", + "pages.show.files.empty": "Den här sidan har inga filer.", + "pages.show.error.permissions.title": "Sidan är ej skrivbar", + "pages.show.error.permissions.text": "Vänligen kontrollera skriv- och läsrättigheter för mappen /content samt dess underliggande filer och mappar.", + "pages.show.error.permissions.retry": "Försök igen", + "pages.show.error.notitle.title": "Blueprinten har inget titel-fält", + "pages.show.error.notitle.text": "Vänligen lägg till ett titel-fält och försök igen", + "pages.show.error.notitle.retry": "Försök igen", + "pages.show.error.form": "vänligen fyll i alla fält korrekt", + "pages.add.title.label": "Lägg till en ny sida", + "pages.add.title.placeholder": "Titel", + "pages.add.url.label": "Tillägg i webbadress", + "pages.add.url.enter": "(skriv in din titel)", + "pages.add.url.close": "Stäng", + "pages.add.url.help": "Format: gemener a-z, 0-9 och vanliga streck", + "pages.add.template.label": "Mall", + "pages.add.error.create": "Sidan kan ej skapas", + "pages.add.error.title": "Titel saknas", + "pages.add.error.template": "Mall saknas", + "pages.add.error.max.headline": "Nya sidor är ej tillåtet", + "pages.add.error.max.text": "Det maximala antalet undersidor för den här sidan har nåtts.", + "pages.url.uid.label": "Tillägg i webbadress", + "pages.url.uid.label.option": "Skapa utifrån titel", + "pages.url.error.exists": "En sida med samma webbadress existerar redan", + "pages.url.error.move": "Tillägget i webbadress kan ej ändras", + "pages.url.error.rights": "Du kan inte ändra URL:en på denna sidan", + "pages.template.select.label": "Mall", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "dold", + "pages.toggle.publish": "Är du säker på att du vill ändra sidan status till **synlig i meny?**", + "pages.toggle.hide": "Är du säker på att du vill ändra sidan status till **dold i meny?**", + "pages.toggle.error.error": "Statusen för felsidan kan inte ändras", + "pages.delete.headline": "Vill du verkligen radera sidan?", + "pages.delete.error.home.headline": "Startsidan kan ej raderas", + "pages.delete.error.home.text": "Du försöker radera startsidan. Det är inte möjligt och får oönskad effekt.", + "pages.delete.error.error.headline": "Felsidan kan inte tas bort", + "pages.delete.error.error.text": "Du försöker ta bort felsidan. Det är inte möjligt och får oönskad effekt.", + "pages.delete.error.children.headline": "Den här sidan kan ej raderas", + "pages.delete.error.children.text": "Den här sidan har undersidor och kan ej raderas. Vänlig radera alla undersidor först.", + "pages.delete.error.blocked.headline": "Den här sidan kan ej raderas", + "pages.delete.error.blocked.text": "Den här sidan är låst och kan ej raderas.", + "pages.search.help": "Sök bland alla sidor via dess webbadress. Navigera upp och ner genom sökresultaten med piltangenterna och tryck enter-/retur-knapp för att redigera markerad sida.", + "pages.search.noresults": "Det finns inga resultat på din sökning. Var vänlig och försök igen med en annan webbadress.", + "pages.error.missing": "Sidan kan ej hittas", + "subpages": "Alla sidor", + "subpages.index.headline": "Alla undersidor i", + "subpages.index.back": "Tillbaka", + "subpages.index.add": "Lägg till en ny sida", + "subpages.index.add.first.text": "Den här sidan har inga undersidor än", + "subpages.index.add.first.button": "Lägg till den första sidan", + "subpages.index.visible": "Synliga sidor i meny", + "subpages.index.visible.help": "Dra dolda sidor hit för att sortera dem/göra dem synliga.", + "subpages.index.invisible": "Dolda sidor (visas ej i meny)", + "subpages.index.invisible.help": "Dra synliga sidor hit för att dölja dem.", + "subpages.add.error": "Den här sidan kan inte ha undersidor", + "subpages.add.error.more": "Den här sidan kan inte ha fler undersidor", + "subpages.error.missing": "Sidan kan ej hittas", + "files": "Filer", + "files.index.headline": "Filer för", + "files.index.back": "Tillbaka", + "files.index.upload": "Ladda upp en ny fil", + "files.index.upload.first.text": "Den här sidan har inga filer än", + "files.index.upload.first.button": "Ladda upp första filen", + "files.index.edit": "Ändra", + "files.index.delete": "Radera", + "files.index.error.disabled": "Den här sidan kan inte ha några filer", + "files.add.error.max": "Det maximala antalet filer för den här sidan har nåtts", + "files.add.error.extension.missing": "Du kan inte ladda upp filer utan filändelse", + "files.add.error.extension.forbidden": "Filändelsen är ej tillåten", + "files.add.error.mime.forbidden": "Ogiltig MIME-typ", + "files.add.error.htaccess": "Htaccess-filer är ej tillåtet att ladda upp.", + "files.add.error.invisible": "Osynliga filer är ej tillåtet att ladda upp.", + "files.add.blueprint.type.error": "Sidan tillåter endast:", + "files.add.blueprint.size.error": "Sidan tillåter endast en filstorlek på", + "files.show.name.label": "Filnamn", + "files.show.info.label": "Filtyp / storlek / dimensioner", + "files.show.link.label": "Publik webbadress", + "files.show.open": "Visa/ladda ner fil", + "files.show.back": "Tillbaka", + "files.show.replace": "Ersätt fil", + "files.show.delete": "Radera", + "files.show.error.rename": "Filen kan ej döpas om", + "files.show.error.form": "Var vänlig och fyll i alla fält korrekt", + "files.upload.drop": "Släpp filer här…", + "files.upload.click": "…eller klicka för att ladda upp", + "files.replace.drop": "Släpp en fil här …", + "files.replace.click": "… eller klicka för att välja en ny fil", + "files.replace.error.type": "Den uppladdade filen måste vara av samma filtyp", + "files.delete.headline": "Vill du verkligen ta bort denna fil?", + "files.error.missing.page": "Sidan kan ej hittas", + "files.error.missing.file": "Filen kan ej hittas", + "users": "Användare", + "users.index.headline": "Alla användare", + "users.index.add": "Lägg till en ny användare", + "users.index.edit": "Ändra", + "users.index.delete": "Radera", + "users.form.username.label": "Användarnamn", + "users.form.username.placeholder": "Ditt användarnamn", + "users.form.username.help": "Tillåtna tecken: gemener a-z, 0-9 och streck", + "users.form.username.readonly": "Användarnamnet kan ej ändras", + "users.form.firstname.label": "Förnamn", + "users.form.lastname.label": "Efternamn", + "users.form.email.label": "E-mail", + "users.form.email.placeholder": "namn@exampel.se", + "users.form.password.label": "Lösenord", + "users.form.password.confirm.label": "Bekräfta lösenord", + "users.form.password.new.label": "Nytt lösenord", + "users.form.password.new.confirm.label": "Bekräfta nytt lösenord", + "users.form.password.new.help": "Lämna blankt för att behålla nuvarande lösenord", + "users.form.language.label": "Språk", + "users.form.role.label": "Roll", + "users.form.options.headline": "Kontoinställningar", + "users.form.options.message": "Skicka mail", + "users.form.options.delete": "Radera konto", + "users.form.avatar.headline": "Profilbild", + "users.form.avatar.upload": "Ladda upp profilbild", + "users.form.avatar.replace": "Byt ut profilbild", + "users.form.avatar.delete": "Radera profilbild", + "users.form.back": "Tillbaka till användare", + "users.form.error.password.confirm": "Vänlig bekräfta ditt lösenord", + "users.form.error.update": "Användaren kan ej uppdateras", + "users.form.error.update.rights": "Du får inte uppdatera den här användaren", + "users.form.error.create": "Användaren kan ej skapas", + "users.form.error.permissions.title": "Mappen är ej skrivbar", + "users.form.error.permissions.text": "Var vänlig och kontroller att mappen sites/accounts existerar och har skrivrättigheter.", + "users.delete.headline": "Vill du verkligen radera användaren?", + "users.delete.error": "Användaren kan ej raderas", + "users.delete.error.permission": "Du får inte radera användare", + "users.delete.error.permission.single": "Du får inte radera den här användaren", + "users.delete.error.lastadmin": "Du kan inte radera sista administratören", + "users.avatar.drop": "Släpp en profilbild här…", + "users.avatar.click": "…eller klicka för att ladda upp", + "users.avatar.error.type": "Du kan bara ladda upp bilder i följande filformat: JPG, PNG och GIF", + "users.avatar.error.folder.headline": "avatar-mappen är inte skrivbar", + "users.avatar.error.folder.text": "Var vänlig och kontrollera att mappen /assets/avatars existerar och har skrivrättigheter för att kunna ladda upp profilbilder.", + "users.avatar.error.permission": "Du får inte ändra avataren", + "users.avatar.delete.error": "Profilbilden kan ej raderas", + "users.avatar.delete.error.permission": "Du får inte radera avataren för den här användaren", + "users.avatar.delete.success": "Profilbilden har raderas", + "users.avatar.missing": "Den här användare saknar avatar", + "users.error.missing": "Användaren kan ej hittas", + "user.error.lastadmin": "Du är ensam administratör. Det går inte att ändra.", + "form.error.missing": "Formuläret kan ej hittas", + "form.construct.error.invalid": "Ogiltig metod för konstruktion av formulär", + "fields.required": "Ifyllt fält krävs.", + "fields.date.label": "Datum", + "fields.date.months": [ + "Januari", + "Februari", + "Mars", + "April", + "Maj", + "Juni", + "Juli", + "Augusti", + "September", + "Oktober", + "November", + "December" + ], + "fields.date.weekdays": [ + "Söndag", + "Måndag", + "Tisdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lördag" + ], + "fields.date.weekdays.short": [ + "Sön", + "Mån", + "Tis", + "Ons", + "Tor", + "Fre", + "Lör" + ], + "fields.email.label": "E-mail", + "fields.email.placeholder": "namn@exempel.se", + "fields.number.label": "Nummer", + "fields.number.placeholder": "#", + "fields.page.label": "Sida", + "fields.page.placeholder": "sökväg/till/sida", + "fields.password.label": "Lösenord", + "fields.structure.add": "Lägg till", + "fields.structure.add.first": "Lägg till första posten", + "fields.structure.empty": "Inga poster än.", + "fields.structure.entry.error": "Artikeln kan ej hittas", + "fields.structure.cancel": "Avbryt", + "fields.structure.save": "Spara", + "fields.structure.edit": "Ändra", + "fields.structure.delete": "Radera", + "fields.structure.delete.label": "Vill du verkligen radera den här posten?", + "fields.tags.label": "Taggar", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Fet text", + "fields.textarea.buttons.bold.text": "Fet text", + "fields.textarea.buttons.italic.label": "Kursiv text", + "fields.textarea.buttons.italic.text": "Kursiv text", + "fields.textarea.buttons.link.label": "Länk", + "fields.textarea.buttons.email.label": "E-mail", + "fields.textarea.buttons.image.label": "Bilder", + "fields.textarea.buttons.file.label": "Fil", + "fields.toggle.yes": "Ja", + "fields.toggle.no": "Nej", + "fields.toggle.on": "På", + "fields.toggle.off": "Av", + "fields.error.missing.controller": "Filen för fältkontroll saknas", + "fields.error.missing.class": "Klassen för fältkontroll saknas", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "Fältet kan inte förlängas", + "editor.link.url.label": "Lägg till webbadress", + "editor.link.text.label": "Länktext", + "editor.link.text.help": "Länktext är frivillig", + "editor.email.address.label": "Lägg till en emailaddress", + "editor.email.address.placeholder": "mail@exempel.se", + "editor.email.text.label": "Länktext", + "editor.email.text.help": "Länktext är frivillig", + "editor.file.empty": "Den här sidan saknar filer", + "editor.image.empty": "Den här sidan saknar bilder", + "autocomplete.method.error": "Ogiltig metod för automatisk komplettering", + "blueprints.error.default.missing": "Standard-blueprint saknas", + "error": "Fel", + "error.headline": "Error" +} \ No newline at end of file diff --git a/panel/app/translations/sv_SE/package.json b/panel/app/translations/sv_SE/package.json new file mode 100644 index 0000000..6b56c58 --- /dev/null +++ b/panel/app/translations/sv_SE/package.json @@ -0,0 +1,4 @@ +{ + "title": "Svenska", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/tr/core.json b/panel/app/translations/tr/core.json new file mode 100644 index 0000000..c57e840 --- /dev/null +++ b/panel/app/translations/tr/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "İptal", + "add": "Ekle", + "addit": "Ekle & Düzenle", + "save": "Kaydet", + "saved": "Kaydedildi!", + "change": "Değiştir", + "delete": "Sil", + "insert": "Ekle", + "ok": "Tamam", + "routes.error.invalid": "Geçersiz Panel Adresi", + "controller.error.invalid": "Geçersiz denetleyici", + "controller.error.action": "Geçersiz eylem", + "view.error.invalid": "Geçersiz görünüm:", + "options.show": "Seçenekleri göster", + "options.hide": "Seçenekleri gizle", + "installation": "Kurulum", + "installation.check.headline": "Kirby Panel Kurulumu", + "installation.check.text": "Kirby kurulum aşamasında belirtilen sorunla karşılaştı…", + "installation.check.retry": "Tekrar Dene", + "installation.check.error": "Bazı sorunlar mevcut!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts klasörü yazılabilir değil", + "installation.check.error.avatars": "/assets/avatars klasörü yazılabilir değil", + "installation.check.error.blueprints": "Lütfen şu klasörü oluşturun : /site/blueprints", + "installation.check.error.content": "Content adındaki klasör ve içindeki tüm klasörler, dosyalar yazılabilir olmalı.", + "installation.check.error.thumbs": "thumb klasörü yazılabilir olmalı", + "installation.signup.username.label": "İlk hesabını oluştur", + "installation.signup.username.placeholder": "Kullanıcı Adı", + "installation.signup.email.label": "E-Posta", + "installation.signup.email.placeholder": "eposta@ornek.com", + "installation.signup.password.label": "Şifre", + "installation.signup.language.label": "Dil", + "installation.signup.button": "Hesabını oluştur", + "login": "Giriş", + "login.welcome": "Lütfen yeni hesabınız ile giriş yapın", + "login.username.label": "Kullanıcı Adı", + "login.password.label": "Şifre", + "login.error": "Geçersiz kullanıcı adı veya şifre", + "login.button": "Giriş", + "login.log.error.permissions": "Giriş günlük kayıt dosyası yazılabilir değil", + "logout": "Güvenli çıkış", + "topbar.error.class.definition": "Sınıfın üstbar tanımlaması eksik:", + "dashboard": "Kontrol Paneli", + "dashboard.index.pages.title": "Sayfalar", + "dashboard.index.pages.edit": "Düzenle", + "dashboard.index.pages.add": "Ekle", + "dashboard.index.site.title": "Website Adresi", + "dashboard.index.account.title": "Hesap Bilgilerin", + "dashboard.index.account.edit": "Düzenle", + "dashboard.index.metatags.title": "Site Bilgileri", + "dashboard.index.metatags.edit": "Düzenle", + "dashboard.index.history.title": "Son güncellemelerin", + "dashboard.index.history.text": "Son güncellemelerin, daha sonra tekrar kolayca bulabilmen için burada görünecek", + "dashboard.index.license.title": "Kirby lisans", + "dashboard.index.license.text": "Kirby'yi canlı sunucuda geçerli bir lisansınız olmadan kullanıyor görünüyorsunuz!\n\nLütfen, Kirby'yi destekleyin ve (link: {buy} text: bir lisans satın alın)\n\nEğer bir lisans anahtarınız var ise, bunu yapılandırma dosyanıza ekleyin: (link: {docs} text: site/config/config.php)", + "metatags": "Site seçenekleri", + "metatags.info": "Kirby bilgi", + "metatags.license": "Kirby lisans", + "metatags.version.toolkit": "Toolkit lisans", + "metatags.version.kirby": "Kirby versiyon", + "metatags.version.panel": "Panel versiyon", + "metatags.back": "Kontrol paneline geri dön", + "metatags.files": "Site dosyaları", + "site.delete.error": "Site silinemez", + "pages.show.settings": "Sayfa ayarları", + "pages.show.preview": "Önizlemeyi aç", + "pages.show.template": "Şablon", + "pages.show.changeurl": "Web adresini değiştir", + "pages.show.invisible": "Durum: görünmez", + "pages.show.visible": "Durum: görünür", + "pages.show.changes.text": "Kaydedilmemiş değişiklikleriniz var!", + "pages.show.changes.button": "Vazgeç", + "pages.show.delete": "Bu sayfayı sil", + "pages.show.subpages.title": "Alt sayfalar", + "pages.show.subpages.edit": "Düzenle", + "pages.show.subpages.add": "Ekle", + "pages.show.subpages.empty": "Bu sayfanın şu an bir alt sayfası yok", + "pages.show.files.title": "Dosyalar", + "pages.show.files.edit": "Düzenle", + "pages.show.files.add": "Ekle", + "pages.show.files.empty": "Bu sayfanın şu an bir dosyası yok", + "pages.show.error.permissions.title": "Sayfa yazılabilir değil", + "pages.show.error.permissions.text": "Lütfen content klasörünün ve içindekilerinin izin yapılandırmasını kontrol ediniz", + "pages.show.error.permissions.retry": "Tekrar Dene", + "pages.show.error.notitle.title": "Blueprint'in başlık alanı yok", + "pages.show.error.notitle.text": "Lütfen bir başlık girin ve tekrar deneyin", + "pages.show.error.notitle.retry": "Tekrar Dene", + "pages.show.error.form": "Lütfen gerekli tüm alanları doğru bir şekilde doldurunuz", + "pages.add.title.label": "Yeni bir alt sayfa ekle", + "pages.add.title.placeholder": "Başlık", + "pages.add.url.label": "Web Adres-uzantısı", + "pages.add.url.enter": "(bir başlık yaz)", + "pages.add.url.close": "Kapat", + "pages.add.url.help": "İzin verilen karakterler: a-z küçük harfler, 0-9 ve normal kesik çizgiler", + "pages.add.template.label": "Şablon", + "pages.add.error.create": "Sayfa oluşturulamadı", + "pages.add.error.title": "Başlık bulunamadı", + "pages.add.error.template": "Şablon bulunamadı", + "pages.add.error.max.headline": "Yeni sayfalar eklemeye izin verilmedi", + "pages.add.error.max.text": "Mevcut sayfa sahip olabilecek maksimum alt sayfa sayısına erişti", + "pages.url.uid.label": "Web Adres-uzantısı", + "pages.url.uid.label.option": "Başlıktan oluştur", + "pages.url.error.exists": "Bir sayfa hali hazırda aynı web adres-uzantısına sahip", + "pages.url.error.move": "Web adres-uzantısı değiştirilemedi", + "pages.url.error.rights": "Bu sayfanın adresini değiştirilemez", + "pages.template.select.label": "Şablon", + "pages.template.warning.text": "Şablonu değiştirdiğinizde ilgili alanlar da güncellenecek", + "pages.template.warning.removed": "Kaldırılan alanlar", + "pages.template.warning.replaced": "Değiştirilen alanlar", + "pages.template.warning.added": "Eklenen alanlar", + "pages.template.error": "Bu sayfa için şablon değiştirilemez", + "pages.toggle.position": "Pozisyon", + "pages.toggle.invisible": "görünmez", + "pages.toggle.publish": "Bu sayfanın durumunu **görünür** olarak değiştirmek istediğinizden emin misiniz?", + "pages.toggle.hide": "Bu sayfanın durumunu **görünmez** olarak değiştirmek istediğinizden emin misiniz?", + "pages.toggle.error.error": "Hata sayfasının durumu değiştirilemez", + "pages.delete.headline": "Bu sayfayı silmek istediğinizden emin misiniz?", + "pages.delete.error.home.headline": "Anasayfa silinemez", + "pages.delete.error.home.text": "Anasayfayı silmeyi deniyorsunuz. Fakat bu mümkün değil, çünkü istenilmeyen etkilere neden olur.", + "pages.delete.error.error.headline": "Hata sayfası silinemedi", + "pages.delete.error.error.text": "Hata sayfasını silmeyi deniyorsunuz. Fakat bu mümkün değil, çünkü istenilmeyen etkilere neden olur.", + "pages.delete.error.children.headline": "Bu sayfa silinemedi", + "pages.delete.error.children.text": "Bu sayfa alt sayfalara sahip olduğundan dolayı silinemdi. Lütfen önce bu sayfanın alt sayfalarını siliniz.", + "pages.delete.error.blocked.headline": "Bu sayfa silinemedi", + "pages.delete.error.blocked.text": "Bu sayfa kilitli ve silinemez", + "pages.search.help": "Sayfaları web adres uzantılarına göre arayınız. Arama sonuçlarından istediğiniz sayfayı klavyenizin yukarı ve aşağı tuşları ile seçip enter tuşuna bastıktan sonra aradığınız sayfaya direk yönleneceksiniz", + "pages.search.noresults": "Herhangi bir sonuç bulunamadı. Lütfen farklı bir web adres-uzantısı ismi deneyiniz.", + "pages.error.missing": "Sayfa bulunamadı", + "subpages": "Alt Sayfalar", + "subpages.index.headline": "Alt Sayfalar:", + "subpages.index.back": "Geri", + "subpages.index.add": "Yeni bir alt sayfa ekle", + "subpages.index.add.first.text": "Bu sayfanın henüz bir alt sayfası yok", + "subpages.index.add.first.button": "İlk alt sayfanı ekle", + "subpages.index.visible": "Websitesinde gösterilecek alt sayfalar", + "subpages.index.visible.help": "Buraya websitesinde gösterilecek alt sayfaları sürükleyip bırakabilirsiniz", + "subpages.index.invisible": "Websitesinden gizlenecek alt sayfalar", + "subpages.index.invisible.help": "Buraya websitesinden gizlenecek alt sayfaları sürükleyip bırakabilirsiniz", + "subpages.add.error": "Bu sayfaya alt sayfa eklenemez", + "subpages.add.error.more": "Bu sayfaya daha fazla alt sayfa eklenemez", + "subpages.error.missing": "Sayfa bulunamadı", + "files": "Dosyalar", + "files.index.headline": "Dosyalar:", + "files.index.back": "Geri", + "files.index.upload": "Yeni bir dosya yükle", + "files.index.upload.first.text": "Bu sayfanın henüz bir dosyası yok", + "files.index.upload.first.button": "İlk dosyayı yükle", + "files.index.edit": "Düzenle", + "files.index.delete": "Sil", + "files.index.error.disabled": "Bu sayfaya dosya eklenemez", + "files.add.error.max": "Bu sayfa için maximum dosya limitine ulaşıldı", + "files.add.error.extension.missing": "Uzantısı olmayan dosya yüklenemez", + "files.add.error.extension.forbidden": "İzin verilmeyen dosya uzantısı", + "files.add.error.mime.forbidden": "İzin verilmeyen dosya tanımlayıcısı", + "files.add.error.htaccess": "htaccess dosyası yüklenemez", + "files.add.error.invisible": "Görünmez dosyalar yüklenemez", + "files.add.blueprint.type.error": "Sayfanın desteklediği formatlar:", + "files.add.blueprint.size.error": "Sayfanın izin verdiği boyut:", + "files.show.name.label": "Dosya İsmi", + "files.show.info.label": "Dosya Formatı / Büyüklüğü / Boyutları", + "files.show.link.label": "Dosyanın Web Adres Uzantısı", + "files.show.open": "Göster/indir", + "files.show.back": "Geri", + "files.show.replace": "Değiştir", + "files.show.delete": "Sil", + "files.show.error.rename": "Dosya ismi yeniden adlandırılamadı", + "files.show.error.form": "Lütfen gerekli tüm alanları doğru bir şekilde doldurunuz", + "files.upload.drop": "Dosyaları buraya sürükle bırak…", + "files.upload.click": "…veya cihazından başka bir dosya yüklemek için buraya tıkla", + "files.replace.drop": "Buraya bir dosya sürükle bırak…", + "files.replace.click": "…veya cihazındaki başka bir dosya ile değiştirmek için buraya tıkla", + "files.replace.error.type": "Yüklenecek dosyalar aynı dosya formatında olmalı", + "files.delete.headline": "Bu dosyayı silmek istediğinizden emin misiniz?", + "files.error.missing.page": "Sayfa bulunamadı", + "files.error.missing.file": "Dosya bulunamadı", + "users": "Kullanıcılar", + "users.index.headline": "Bütün kullanıcılar", + "users.index.add": "Yeni bir kullanıcı ekle", + "users.index.edit": "Düzenle", + "users.index.delete": "Sil", + "users.form.username.label": "Kullanıcı Adı", + "users.form.username.placeholder": "Kullanıcı adı seç", + "users.form.username.help": "İzin verilen karakterler: a-z küçük harfler, 0-9 ve normal kesik çizgiler", + "users.form.username.readonly": "Kullancı adı değiştirilemez", + "users.form.firstname.label": "Ad", + "users.form.lastname.label": "Soyad", + "users.form.email.label": "E-Posta", + "users.form.email.placeholder": "eposta@ornek.com", + "users.form.password.label": "Şifre", + "users.form.password.confirm.label": "Şifre Tekrarı", + "users.form.password.new.label": "Yeni Şifre", + "users.form.password.new.confirm.label": "Yeni şifre tekrarı", + "users.form.password.new.help": "Mevcut şifreyi boş bırak", + "users.form.language.label": "Dil", + "users.form.role.label": "Rol", + "users.form.options.headline": "Kullanıcı Ayarları", + "users.form.options.message": "E-Posta gönder", + "users.form.options.delete": "Bu hesabı sil", + "users.form.avatar.headline": "Profil resmi", + "users.form.avatar.upload": "Profil resmi yükle", + "users.form.avatar.replace": "Profil resmini değiştir", + "users.form.avatar.delete": "Profil resmini sil", + "users.form.back": "Kullanıcılar paneline geri dön", + "users.form.error.password.confirm": "Lütfen şifreyi doğrulayın", + "users.form.error.update": "Kullanıcı güncellenemedi", + "users.form.error.update.rights": "Bu kullanıcıyı güncellemek için yetkiniz yok", + "users.form.error.create": "Kullanıcı oluşturulamadı", + "users.form.error.permissions.title": "Account klasörü yazılabilir değil", + "users.form.error.permissions.text": "Lütfen /site/accounts klasörünün mevcut olduğundan ve yazılabilir olduğundan emin olun.", + "users.delete.headline": "Bu kullanıcıyı silmek istediğinizden emin misiniz?", + "users.delete.error": "Kullanıcı silinemedi", + "users.delete.error.permission": "Kullanıcıları silme yetkiniz yok", + "users.delete.error.permission.single": "Bu kullanıcıyı silme yetkiniz yok", + "users.delete.error.lastadmin": "Son yönetici kullanıcıyı silemezsiniz", + "users.avatar.drop": "Buraya bir profil resmi sürükle bırak…", + "users.avatar.click": "…veya cihazından başka bir profil resmi yüklemek için buraya tıkla", + "users.avatar.error.type": "Sadece JPG, PNG and GIF formatındaki dosyaları yükleyebilirsin", + "users.avatar.error.folder.headline": "Avatar klasörü yazılabilir değil", + "users.avatar.error.folder.text": "Lütfen /assets/avatars adlı klasör oluştur ve profil resmini yükleyebilmek için yazılabilir izni ver.", + "users.avatar.error.permission": "Avatarı değiştirme yetkiniz yok", + "users.avatar.delete.error": "Profil resmi silinemedi", + "users.avatar.delete.error.permission": "Bu kullanıcının avatarını silme yetkiniz yok", + "users.avatar.delete.success": "Profil resmi silindi", + "users.avatar.missing": "Bu kullanıcının avatarı yok", + "users.error.missing": "Kullanıcı bulunamadı", + "user.error.lastadmin": "Tek yönetici sensin. Bu değiştirilemez", + "form.error.missing": "Form bulunamadı", + "form.construct.error.invalid": "Geçersiz form kurucu metodu", + "fields.required": "Zorunlu", + "fields.date.label": "Tarih", + "fields.date.months": [ + "Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık" + ], + "fields.date.weekdays": [ + "Pazar", + "Pazartesi", + "Salı", + "Çarşamba", + "Perşembe", + "Cuma", + "Cumartesi" + ], + "fields.date.weekdays.short": [ + "Paz", + "Pzt", + "Sal", + "Çar", + "Per", + "Cum", + "Cmt" + ], + "fields.email.label": "E-Posta", + "fields.email.placeholder": "eposta@ornek.com", + "fields.number.label": "Numara", + "fields.number.placeholder": "#", + "fields.page.label": "Sayfa", + "fields.page.placeholder": "adres/yolu/sayfa", + "fields.password.label": "Şifre", + "fields.structure.add": "Ekle", + "fields.structure.add.first": "İlk girdini ekle", + "fields.structure.empty": "Henüz bir girdi yok", + "fields.structure.entry.error": "Öğe bulunamadı", + "fields.structure.cancel": "İptal", + "fields.structure.save": "Kaydet", + "fields.structure.edit": "Düzenle", + "fields.structure.delete": "Sil", + "fields.structure.delete.label": "Bu girdiyi silmek istediğinizden emin misiniz?", + "fields.tags.label": "Anahtar Kelimeler", + "fields.tel.label": "Telefon", + "fields.textarea.buttons.bold.label": "Kalın yazı", + "fields.textarea.buttons.bold.text": "Kalın yazı", + "fields.textarea.buttons.italic.label": "Eğik yazı", + "fields.textarea.buttons.italic.text": "Eğik yazı", + "fields.textarea.buttons.link.label": "Bağlantı", + "fields.textarea.buttons.email.label": "E-Posta", + "fields.textarea.buttons.image.label": "Resim", + "fields.textarea.buttons.file.label": "Dosya", + "fields.toggle.yes": "Evet", + "fields.toggle.no": "Hayır", + "fields.toggle.on": "Açık", + "fields.toggle.off": "Kapalı", + "fields.error.missing.controller": "Alan denetleyici dosyası eksik", + "fields.error.missing.class": "Alan kontrol sınıfı eksik", + "fields.error.route.invalid": "Geçersiz alan rotası", + "fields.error.extended": "Bu alan genişletilemez", + "editor.link.url.label": "Web Adresi Ekle", + "editor.link.text.label": "Bağlantı yazısı", + "editor.link.text.help": "Bağlantı yazısı isteğe bağlı", + "editor.email.address.label": "E-Posta Adresi Ekle", + "editor.email.address.placeholder": "eposta@ornek.com", + "editor.email.text.label": "Bağlantı yazısı", + "editor.email.text.help": "Bağlantı yazısı isteğe bağlı", + "editor.file.empty": "Bu sayfanın henüz bir dosyası yok", + "editor.image.empty": "Bu sayfanın henüz bir resmi yok", + "autocomplete.method.error": "Geçersiz otomatik doldurma metodu", + "blueprints.error.default.missing": "Varsayılan taslak dosyası eksik", + "error": "Hata", + "error.headline": "Hata" +} \ No newline at end of file diff --git a/panel/app/translations/tr/package.json b/panel/app/translations/tr/package.json new file mode 100644 index 0000000..1be89b1 --- /dev/null +++ b/panel/app/translations/tr/package.json @@ -0,0 +1,4 @@ +{ + "title": "Türkçe", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/zh_CN/core.json b/panel/app/translations/zh_CN/core.json new file mode 100644 index 0000000..dd52dec --- /dev/null +++ b/panel/app/translations/zh_CN/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "取消", + "add": "新增", + "addit": "新增并编辑", + "save": "保存", + "saved": "已保存!", + "change": "更改", + "delete": "删除", + "insert": "插入", + "ok": "确认", + "routes.error.invalid": "无效的控制面板URL", + "controller.error.invalid": "无效的控制器", + "controller.error.action": "无效行为", + "view.error.invalid": "无效视图:", + "options.show": "显示选项", + "options.hide": "隐藏选项", + "installation": "安装", + "installation.check.headline": "安装Kirby控制面板", + "installation.check.text": "Kirby在安装过程中遇到以下问题...", + "installation.check.retry": "重试", + "installation.check.error": "存在一些问题!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "/site/accounts不可写入", + "installation.check.error.avatars": "/assets/avatars不可写入", + "installation.check.error.blueprints": "请新增/site/blueprints文件夹", + "installation.check.error.content": "content文件夹及其内容必须可写入", + "installation.check.error.thumbs": "thumbs文件夹必须可写入", + "installation.signup.username.label": "创建首个账户", + "installation.signup.username.placeholder": "用户名", + "installation.signup.email.label": "邮箱", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "密码", + "installation.signup.language.label": "语言", + "installation.signup.button": "创建账户", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "用户名", + "login.password.label": "密码", + "login.error": "无效的用户名或密码", + "login.button": "Log in", + "login.log.error.permissions": "登陆的log file不可写入", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "仪表盘", + "dashboard.index.pages.title": "页面", + "dashboard.index.pages.edit": "编辑", + "dashboard.index.pages.add": "新增", + "dashboard.index.site.title": "网址", + "dashboard.index.account.title": "您的账户", + "dashboard.index.account.edit": "编辑", + "dashboard.index.metatags.title": "网站选项", + "dashboard.index.metatags.edit": "编辑", + "dashboard.index.history.title": "最近更新", + "dashboard.index.history.text": "您最近修改的页面会显示在这里,以便查找。", + "dashboard.index.license.title": "Kirby", + "dashboard.index.license.text": "好像您在一个公共服务器上运行没有许可证的Kirby!\n请支持一下Kirby,购买一个许可证。\n如果您已有许可证号,请把它添加到config文件中。", + "metatags": "网站选项", + "metatags.info": "Kirby信息", + "metatags.license": "Kirby", + "metatags.version.toolkit": "工具箱版本", + "metatags.version.kirby": "Kirby版本", + "metatags.version.panel": "控制面板版本", + "metatags.back": "返回控制面板", + "metatags.files": "网站文件", + "site.delete.error": "该网站不能被删除", + "pages.show.settings": "页面设置", + "pages.show.preview": "打开预览", + "pages.show.template": "模版", + "pages.show.changeurl": "更改URL", + "pages.show.invisible": "状态:不可见", + "pages.show.visible": "状态:可见", + "pages.show.changes.text": "您有未保存的修改!", + "pages.show.changes.button": "放弃", + "pages.show.delete": "删除此页", + "pages.show.subpages.title": "页面", + "pages.show.subpages.edit": "编辑", + "pages.show.subpages.add": "新增", + "pages.show.subpages.empty": "此页面没有子页面", + "pages.show.files.title": "文件", + "pages.show.files.edit": "编辑", + "pages.show.files.add": "新增", + "pages.show.files.empty": "此页面没有文件", + "pages.show.error.permissions.title": "此页面不可写入", + "pages.show.error.permissions.text": "请检查content文件夹和所有文件的修改权限", + "pages.show.error.permissions.retry": "重试", + "pages.show.error.notitle.title": "该blueprint没有标题栏", + "pages.show.error.notitle.text": "请新增标题栏并重试", + "pages.show.error.notitle.retry": "重试", + "pages.show.error.form": "请正确填写所有栏", + "pages.add.title.label": "新增页面", + "pages.add.title.placeholder": "标题", + "pages.add.url.label": "URL后缀", + "pages.add.url.enter": "(输入标题)", + "pages.add.url.close": "关闭", + "pages.add.url.help": "格式要求:小写字母a-z、数字0-9及连字符-", + "pages.add.template.label": "模版", + "pages.add.error.create": "无法新增页面", + "pages.add.error.title": "标题缺失", + "pages.add.error.template": "模版缺失", + "pages.add.error.max.headline": "新页面不允许被创建", + "pages.add.error.max.text": "当前页面的子页面数量已达上限", + "pages.url.uid.label": "URL后缀", + "pages.url.uid.label.option": "从标题创建", + "pages.url.error.exists": "含有相同后缀的页面已存在", + "pages.url.error.move": "该后缀不能被修改", + "pages.url.error.rights": "您不能更改该页面的URL", + "pages.template.select.label": "模版", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "位置", + "pages.toggle.invisible": "隐藏", + "pages.toggle.publish": "确定更改该页面的状态为可见状态?", + "pages.toggle.hide": "确定更改该页面的状态为不可见状态?", + "pages.toggle.error.error": "error页面的状态不能更改", + "pages.delete.headline": "确定删除该页面?", + "pages.delete.error.home.headline": "home页面不能被删除", + "pages.delete.error.home.text": "您正在尝试删除home页面。", + "pages.delete.error.error.headline": "error页面不能删除", + "pages.delete.error.error.text": "您正在尝试删除error页面,该行为不被允许,否则会产生不利后果。", + "pages.delete.error.children.headline": "该页面不能删除", + "pages.delete.error.children.text": "该页含有子页面,不可删除。请先删除子页面。", + "pages.delete.error.blocked.headline": "该页面不能删除", + "pages.delete.error.blocked.text": "该页面已锁定,不可删除", + "pages.search.help": "通过URL搜索页面。使用上下键进行选择,按回车键进入相应页面。", + "pages.search.noresults": "当前请求没有搜索结果。请重试。", + "pages.error.missing": "该页面找不到", + "subpages": "页面", + "subpages.index.headline": "Pages in", + "subpages.index.back": "返回", + "subpages.index.add": "新增页面", + "subpages.index.add.first.text": "该页面还没有子页面", + "subpages.index.add.first.button": "添加首个页面", + "subpages.index.visible": "可见页面", + "subpages.index.visible.help": "拖拽隐藏页面到这里以排序/使其可见", + "subpages.index.invisible": "隐藏页面", + "subpages.index.invisible.help": "拖拽可见页面到这里以撤销分类/使其隐藏", + "subpages.add.error": "该页面没有拥有子页面权限", + "subpages.add.error.more": "该页面不能再添加子页面。", + "subpages.error.missing": "该页面找不到", + "files": "文件", + "files.index.headline": "Files for", + "files.index.back": "返回", + "files.index.upload": "上传新文件", + "files.index.upload.first.text": "该页面还没有文件", + "files.index.upload.first.button": "上传首份文件", + "files.index.edit": "编辑", + "files.index.delete": "删除", + "files.index.error.disabled": "该页面不允许包含任何文件", + "files.add.error.max": "当前页面的文件数量已达上限", + "files.add.error.extension.missing": "您不可上传没有扩展名的文件", + "files.add.error.extension.forbidden": "禁用文件扩展名", + "files.add.error.mime.forbidden": "禁用MIME类型", + "files.add.error.htaccess": "htaccess文件不能被上传", + "files.add.error.invisible": "隐藏页面不能被上传", + "files.add.blueprint.type.error": "该页只允许:", + "files.add.blueprint.size.error": "页面只允许文件大小为", + "files.show.name.label": "文件名", + "files.show.info.label": "类型/大小/尺寸", + "files.show.link.label": "公共链接", + "files.show.open": "显示/下载文件", + "files.show.back": "返回", + "files.show.replace": "替换", + "files.show.delete": "删除", + "files.show.error.rename": "该文件不能被重命名", + "files.show.error.form": "请正确填写所有栏", + "files.upload.drop": "拖拽文件到这里...", + "files.upload.click": "或点击来上传", + "files.replace.drop": "拖拽文件到这里...", + "files.replace.click": "...或点击来替换", + "files.replace.error.type": "上传文件必须是同类型", + "files.delete.headline": "您确定删除这个文件?", + "files.error.missing.page": "该页面找不到", + "files.error.missing.file": "文件找不到", + "users": "用户", + "users.index.headline": "所有用户", + "users.index.add": "添加新用户", + "users.index.edit": "编辑", + "users.index.delete": "删除", + "users.form.username.label": "用户名", + "users.form.username.placeholder": "您的用户名", + "users.form.username.help": "允许字符:小写字母[a-z]和连字符[-]", + "users.form.username.readonly": "用户名不可更改", + "users.form.firstname.label": "名", + "users.form.lastname.label": "姓", + "users.form.email.label": "邮箱", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "密码", + "users.form.password.confirm.label": "确认密码", + "users.form.password.new.label": "新密码", + "users.form.password.new.confirm.label": "确认新密码", + "users.form.password.new.help": "保留此处空白以保留当前密码", + "users.form.language.label": "语言", + "users.form.role.label": "角色", + "users.form.options.headline": "账户选项", + "users.form.options.message": "发送邮件", + "users.form.options.delete": "删除帐户", + "users.form.avatar.headline": "用户资料图片", + "users.form.avatar.upload": "上传用户资料图片", + "users.form.avatar.replace": "替换用户资料图片", + "users.form.avatar.delete": "删除用户资料图片", + "users.form.back": "回到用户", + "users.form.error.password.confirm": "请核实密码", + "users.form.error.update": "无法更新该用户", + "users.form.error.update.rights": "您没有更新该用户权限", + "users.form.error.create": "该用户无法创建", + "users.form.error.permissions.title": "账户文件夹不可写入", + "users.form.error.permissions.text": "请确保/site/accounts 文件夹存在且可写入", + "users.delete.headline": "确定删除该用户?", + "users.delete.error": "该用户无法删除", + "users.delete.error.permission": "您没有删除用户权限", + "users.delete.error.permission.single": "您没有删除该用户权限", + "users.delete.error.lastadmin": "您不能删除仅有的admin账户", + "users.avatar.drop": "拖拽用户资料图片到这里...", + "users.avatar.click": "或点击来上传", + "users.avatar.error.type": "只能上传JPG、PNG或GIF格式文件", + "users.avatar.error.folder.headline": "avatar文件夹不可写入", + "users.avatar.error.folder.text": "请新建/assets/avatats文件夹并使其可写入,以上传用户资料图片。", + "users.avatar.error.permission": "您没有修改头像的权限", + "users.avatar.delete.error": "该用户资料图片不能被删除。", + "users.avatar.delete.error.permission": "您没有删除这个用户头像的权限", + "users.avatar.delete.success": "该用户资料图片已删除。", + "users.avatar.missing": "该用户没有头像", + "users.error.missing": "该用户找不到", + "user.error.lastadmin": "您是仅有的admin用户,不可更改。", + "form.error.missing": "表单不存在", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必填", + "fields.date.label": "日期", + "fields.date.months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "fields.date.weekdays": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "fields.date.weekdays.short": [ + "周日", + "周一", + "周二", + "周三", + "周四", + "周五", + "周六" + ], + "fields.email.label": "邮箱", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "数字", + "fields.number.placeholder": "#", + "fields.page.label": "页面", + "fields.page.placeholder": "path/to/page", + "fields.password.label": "密码", + "fields.structure.add": "新增", + "fields.structure.add.first": "Add the first entry", + "fields.structure.empty": "No entries yet.", + "fields.structure.entry.error": "该条目无法找到", + "fields.structure.cancel": "取消", + "fields.structure.save": "确认", + "fields.structure.edit": "编辑", + "fields.structure.delete": "删除", + "fields.structure.delete.label": "确定删除这项纪录?", + "fields.tags.label": "标签", + "fields.tel.label": "电话", + "fields.textarea.buttons.bold.label": "粗体文本", + "fields.textarea.buttons.bold.text": "粗体文本", + "fields.textarea.buttons.italic.label": "斜体文本", + "fields.textarea.buttons.italic.text": "斜体文本", + "fields.textarea.buttons.link.label": "链接", + "fields.textarea.buttons.email.label": "邮箱", + "fields.textarea.buttons.image.label": "图像", + "fields.textarea.buttons.file.label": "文件", + "fields.toggle.yes": "是", + "fields.toggle.no": "否", + "fields.toggle.on": "开启", + "fields.toggle.off": "关闭", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "插入URL", + "editor.link.text.label": "链接文本", + "editor.link.text.help": "链接文本可选", + "editor.email.address.label": "插入邮箱地址", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "链接文本", + "editor.email.text.help": "链接文本可选", + "editor.file.empty": "该页面没有文件", + "editor.image.empty": "该页没有图像", + "autocomplete.method.error": "无效的自动完成方法", + "blueprints.error.default.missing": "默认blueprint缺失", + "error": "错误", + "error.headline": "错误" +} \ No newline at end of file diff --git a/panel/app/translations/zh_CN/package.json b/panel/app/translations/zh_CN/package.json new file mode 100644 index 0000000..52082f2 --- /dev/null +++ b/panel/app/translations/zh_CN/package.json @@ -0,0 +1,4 @@ +{ + "title": "简体中文(大陆)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/translations/zh_TW/core.json b/panel/app/translations/zh_TW/core.json new file mode 100644 index 0000000..1f1e082 --- /dev/null +++ b/panel/app/translations/zh_TW/core.json @@ -0,0 +1,311 @@ +{ + "cancel": "取消", + "add": "新增", + "addit": "Add & Edit", + "save": "儲存", + "saved": "已儲存", + "change": "Change", + "delete": "刪除", + "insert": "插入", + "ok": "好", + "routes.error.invalid": "Invalid Panel URL", + "controller.error.invalid": "Invalid controller", + "controller.error.action": "Invalid action", + "view.error.invalid": "Invalid view:", + "options.show": "顯示選項", + "options.hide": "隱藏選項", + "installation": "安裝", + "installation.check.headline": "安裝Kirby控制台", + "installation.check.text": "Kirby在安裝過程中遇到以下問題…", + "installation.check.retry": "重試", + "installation.check.error": "這裡發生了一些問題!", + "installation.check.error.allowed": "You are only allowed to run the panel installer on a local machine or by setting the option panel.install to true in your /site/config/config.php", + "installation.check.error.accounts": "您沒有「/site/accounts」資料夾的修改權限", + "installation.check.error.avatars": "您沒有「/assets/avatars」資料夾的修改權限", + "installation.check.error.blueprints": "請新增「/site/blueprints」資料夾", + "installation.check.error.content": "您沒有「/content」資料夾的修改權限", + "installation.check.error.thumbs": "您沒有「/thumbs」資料夾的修改權限", + "installation.signup.username.label": "建立首位使用者", + "installation.signup.username.placeholder": "帳號", + "installation.signup.email.label": "Email", + "installation.signup.email.placeholder": "mail@example.com", + "installation.signup.password.label": "密碼", + "installation.signup.language.label": "慣用語言", + "installation.signup.button": "建立", + "login": "Log in", + "login.welcome": "Please log in with your new account", + "login.username.label": "帳號", + "login.password.label": "密碼", + "login.error": "帳號與密碼不正確!", + "login.button": "Log in", + "login.log.error.permissions": "Login log file is not writable.", + "logout": "Log out", + "topbar.error.class.definition": "Missing topbar definition for class:", + "dashboard": "控制台", + "dashboard.index.pages.title": "頁面", + "dashboard.index.pages.edit": "編輯", + "dashboard.index.pages.add": "新增", + "dashboard.index.site.title": "網址", + "dashboard.index.account.title": "您的帳號", + "dashboard.index.account.edit": "編輯", + "dashboard.index.metatags.title": "網站資料", + "dashboard.index.metatags.edit": "編輯", + "dashboard.index.history.title": "頁面編輯紀錄", + "dashboard.index.history.text": "您最近修改的頁面會顯示在這裡,方便您再次編輯。", + "dashboard.index.license.title": "Kirby license", + "dashboard.index.license.text": "It seems you are running Kirby on a public server without a valid license!\n\nPlease, support Kirby and (link: {buy} text: buy a license now)\n\nIf you already have a license key, just add it to your config file: (link: {docs} text: site/config/config.php)", + "metatags": "網站資料", + "metatags.info": "Kirby info", + "metatags.license": "Kirby license", + "metatags.version.toolkit": "Toolkit version", + "metatags.version.kirby": "Kirby version", + "metatags.version.panel": "Panel version", + "metatags.back": "返回控制台", + "metatags.files": "Site files", + "site.delete.error": "The site cannot be deleted", + "pages.show.settings": "頁面設定", + "pages.show.preview": "在新分頁中瀏覽此頁面", + "pages.show.template": "頁面模板", + "pages.show.changeurl": "更改頁面網址", + "pages.show.invisible": "Status: invisible", + "pages.show.visible": "Status: visible", + "pages.show.changes.text": "You have unsaved changes!", + "pages.show.changes.button": "Discard", + "pages.show.delete": "刪除此頁面", + "pages.show.subpages.title": "子頁面", + "pages.show.subpages.edit": "編輯", + "pages.show.subpages.add": "新增", + "pages.show.subpages.empty": "目前沒有子頁面", + "pages.show.files.title": "附加檔案", + "pages.show.files.edit": "編輯", + "pages.show.files.add": "上傳", + "pages.show.files.empty": "目前沒有附加檔案", + "pages.show.error.permissions.title": "您沒有修改此頁面的權限", + "pages.show.error.permissions.text": "請確認「/content」資料夾的修改權限", + "pages.show.error.permissions.retry": "重試", + "pages.show.error.notitle.title": "模板的「blueprint」中沒有「title」欄位", + "pages.show.error.notitle.text": "請在新增「title」欄位後重試", + "pages.show.error.notitle.retry": "重試", + "pages.show.error.form": "請正確填入所有欄位", + "pages.add.title.label": "新增頁面", + "pages.add.title.placeholder": "頁面標題", + "pages.add.url.label": "頁面網址", + "pages.add.url.enter": "(從頁面標題輸入)", + "pages.add.url.close": "關閉", + "pages.add.url.help": "可用字元:英文小寫 a-z、數字 0-9以及分號「-」", + "pages.add.template.label": "頁面模板", + "pages.add.error.create": "The page could not be created", + "pages.add.error.title": "沒有頁面標題", + "pages.add.error.template": "沒有頁面模板", + "pages.add.error.max.headline": "無法新增頁面", + "pages.add.error.max.text": "子頁面數量已達上限", + "pages.url.uid.label": "頁面網址", + "pages.url.uid.label.option": "從頁面標題輸入", + "pages.url.error.exists": "已有重複的頁面網址", + "pages.url.error.move": "無法更改頁面網址", + "pages.url.error.rights": "You cannot change the URL of this page", + "pages.template.select.label": "頁面模板", + "pages.template.warning.text": "The following fields will change, when you switch the template", + "pages.template.warning.removed": "Removed fields", + "pages.template.warning.replaced": "Replaced fields", + "pages.template.warning.added": "Added fields", + "pages.template.error": "The template for this page cannot be changed", + "pages.toggle.position": "Position", + "pages.toggle.invisible": "invisible", + "pages.toggle.publish": "Do you really want to change the status of this page to **visible?**", + "pages.toggle.hide": "Do you really want to change the status of this page to **invisible?**", + "pages.toggle.error.error": "The status of the error page cannot be changed", + "pages.delete.headline": "確認刪除此頁面?", + "pages.delete.error.home.headline": "不可刪除首頁", + "pages.delete.error.home.text": "如果刪除首頁將導致不可預期的後果。", + "pages.delete.error.error.headline": "不可刪除出錯頁面", + "pages.delete.error.error.text": "如果刪除出錯頁面將無法重新引導訪客。", + "pages.delete.error.children.headline": "無法刪除頁面", + "pages.delete.error.children.text": "此頁面底下有其他子頁面,請先刪除所有子頁面。", + "pages.delete.error.blocked.headline": "頁面不可被刪除", + "pages.delete.error.blocked.text": "此為保護頁面", + "pages.search.help": "透過頁面網址搜尋(可用上下鍵選擇並按 Enter 鍵進入)", + "pages.search.noresults": "沒有符合關鍵字的搜尋結果", + "pages.error.missing": "找不到頁面", + "subpages": "頁面", + "subpages.index.headline": "所有子頁面 in", + "subpages.index.back": "返回", + "subpages.index.add": "新增頁面", + "subpages.index.add.first.text": "目前沒有子頁面", + "subpages.index.add.first.button": "新增", + "subpages.index.visible": "可見頁面", + "subpages.index.visible.help": "可拖放隱藏頁面到這裡", + "subpages.index.invisible": "隱藏頁面", + "subpages.index.invisible.help": "可拖放頁面到這裡將它們隱藏", + "subpages.add.error": "This page is not allowed to have subpages", + "subpages.add.error.more": "This page cannot have any more subpages", + "subpages.error.missing": "找不到頁面", + "files": "附加檔案", + "files.index.headline": "附加檔案 for", + "files.index.back": "返回", + "files.index.upload": "上傳附加檔案", + "files.index.upload.first.text": "目前沒有附加檔案", + "files.index.upload.first.button": "上傳", + "files.index.edit": "編輯", + "files.index.delete": "刪除", + "files.index.error.disabled": "The page is not allowed to have any files", + "files.add.error.max": "The maximum number of files for the current page has been reached.", + "files.add.error.extension.missing": "You cannot upload files without extension", + "files.add.error.extension.forbidden": "Forbidden file extension", + "files.add.error.mime.forbidden": "Forbidden mime type", + "files.add.error.htaccess": "htaccess files cannot be uploaded", + "files.add.error.invisible": "Invisible files cannot be uploaded", + "files.add.blueprint.type.error": "Page only allows:", + "files.add.blueprint.size.error": "Page only allows file size of", + "files.show.name.label": "檔案名稱", + "files.show.info.label": "檔案格式 / 大小 / 尺寸", + "files.show.link.label": "公開連結", + "files.show.open": "開啟/下載", + "files.show.back": "返回", + "files.show.replace": "更換", + "files.show.delete": "刪除", + "files.show.error.rename": "無法重新命名檔案", + "files.show.error.form": "請正確填入所有欄位", + "files.upload.drop": "請拖放檔案到這裡", + "files.upload.click": "...或按此上傳", + "files.replace.drop": "請拖放檔案到這裡", + "files.replace.click": "...或按此上傳", + "files.replace.error.type": "必須上傳相同格式的檔案", + "files.delete.headline": "確認刪除檔案?", + "files.error.missing.page": "找不到頁面", + "files.error.missing.file": "找不到檔案", + "users": "使用者", + "users.index.headline": "所有使用者", + "users.index.add": "新增使用者", + "users.index.edit": "編輯", + "users.index.delete": "刪除", + "users.form.username.label": "帳號", + "users.form.username.placeholder": "帳號", + "users.form.username.help": "可用字元:英文小寫 a-z、數字 0-9以及分號「-」", + "users.form.username.readonly": "帳號不可更改", + "users.form.firstname.label": "名字", + "users.form.lastname.label": "姓氏", + "users.form.email.label": "Email", + "users.form.email.placeholder": "mail@example.com", + "users.form.password.label": "密碼", + "users.form.password.confirm.label": "再次確認密碼", + "users.form.password.new.label": "更改密碼", + "users.form.password.new.confirm.label": "再次確認密碼", + "users.form.password.new.help": "若要更改密碼請填入新密碼", + "users.form.language.label": "慣用語言", + "users.form.role.label": "權限", + "users.form.options.headline": "使用者選項", + "users.form.options.message": "傳送郵件", + "users.form.options.delete": "刪除使用者", + "users.form.avatar.headline": "使用者照片", + "users.form.avatar.upload": "上傳使用者照片", + "users.form.avatar.replace": "更換使用者照片", + "users.form.avatar.delete": "刪除使用者照片", + "users.form.back": "返回使用者名單", + "users.form.error.password.confirm": "請確認密碼無誤", + "users.form.error.update": "無法更改使用者", + "users.form.error.update.rights": "You are not allowed to update this user", + "users.form.error.create": "無法新增使用者", + "users.form.error.permissions.title": "您沒有「/site/accounts」資料夾的修改權限", + "users.form.error.permissions.text": "請確認「/site/accounts」資料夾是否存在並具有修改權限", + "users.delete.headline": "確認刪除使用者?", + "users.delete.error": "該使用者不可被刪除", + "users.delete.error.permission": "You are not allowed to delete users", + "users.delete.error.permission.single": "You are not allowed to delete this user", + "users.delete.error.lastadmin": "You cannot delete the last admin", + "users.avatar.drop": "請拖放照片到這裡", + "users.avatar.click": "...或按此上傳", + "users.avatar.error.type": "只能使用 JPG、PNG 和 GIF 格式的圖片", + "users.avatar.error.folder.headline": "您沒有「/assets/avatars」資料夾的修改權限", + "users.avatar.error.folder.text": "請確認「/assets/avatars」資料夾是否存在並具有修改權限", + "users.avatar.error.permission": "You are not allowed to change the avatar", + "users.avatar.delete.error": "無法刪除使用者照片", + "users.avatar.delete.error.permission": "You are not allowed to delete the avatar of this user", + "users.avatar.delete.success": "已刪除使用者照片", + "users.avatar.missing": "This user has no avatar", + "users.error.missing": "找不到使用者", + "user.error.lastadmin": "You are the only admin. This cannot be changed.", + "form.error.missing": "The form cannot be found", + "form.construct.error.invalid": "Invalid form construction method", + "fields.required": "必要欄位", + "fields.date.label": "日期", + "fields.date.months": [ + "一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月" + ], + "fields.date.weekdays": [ + "週日", + "週一", + "週二", + "週三", + "週四", + "週五", + "週六" + ], + "fields.date.weekdays.short": [ + "日", + "一", + "二", + "三", + "四", + "五", + "六" + ], + "fields.email.label": "Email", + "fields.email.placeholder": "mail@example.com", + "fields.number.label": "數字", + "fields.number.placeholder": "#", + "fields.page.label": "頁面", + "fields.page.placeholder": "頁面路徑", + "fields.password.label": "密碼", + "fields.structure.add": "新增", + "fields.structure.add.first": "新增第一筆資料", + "fields.structure.empty": "還沒有資料", + "fields.structure.entry.error": "The item could not be found", + "fields.structure.cancel": "取消", + "fields.structure.save": "儲存", + "fields.structure.edit": "編輯", + "fields.structure.delete": "刪除", + "fields.structure.delete.label": "Do you really want to delete this entry?", + "fields.tags.label": "標籤", + "fields.tel.label": "電話", + "fields.textarea.buttons.bold.label": "粗體", + "fields.textarea.buttons.bold.text": "粗體", + "fields.textarea.buttons.italic.label": "斜體", + "fields.textarea.buttons.italic.text": "斜體", + "fields.textarea.buttons.link.label": "連結", + "fields.textarea.buttons.email.label": "Email", + "fields.textarea.buttons.image.label": "圖片", + "fields.textarea.buttons.file.label": "檔案", + "fields.toggle.yes": "是", + "fields.toggle.no": "否", + "fields.toggle.on": "開啟", + "fields.toggle.off": "關閉", + "fields.error.missing.controller": "The field controller file is missing", + "fields.error.missing.class": "The field controller class is missing", + "fields.error.route.invalid": "Invalid field route", + "fields.error.extended": "The field cannot be extended", + "editor.link.url.label": "插入網址連結", + "editor.link.text.label": "連結文字", + "editor.link.text.help": "連結文字可選填", + "editor.email.address.label": "插入 Email 連結", + "editor.email.address.placeholder": "mail@example.com", + "editor.email.text.label": "連結文字", + "editor.email.text.help": "連結文字可選填", + "editor.file.empty": "無可用附加檔案", + "editor.image.empty": "無可用附加圖片檔案", + "autocomplete.method.error": "Invalid autocomplete method", + "blueprints.error.default.missing": "Missing default blueprint", + "error": "錯誤", + "error.headline": "錯誤" +} \ No newline at end of file diff --git a/panel/app/translations/zh_TW/package.json b/panel/app/translations/zh_TW/package.json new file mode 100644 index 0000000..7b5d8f1 --- /dev/null +++ b/panel/app/translations/zh_TW/package.json @@ -0,0 +1,4 @@ +{ + "title": "繁體中文(台灣)‎", + "direction": "ltr" +} \ No newline at end of file diff --git a/panel/app/views/auth/block.php b/panel/app/views/auth/block.php new file mode 100644 index 0000000..043e05a --- /dev/null +++ b/panel/app/views/auth/block.php @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/panel/app/views/auth/error.php b/panel/app/views/auth/error.php new file mode 100644 index 0000000..17804a9 --- /dev/null +++ b/panel/app/views/auth/error.php @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/panel/app/views/auth/login.php b/panel/app/views/auth/login.php new file mode 100644 index 0000000..0bebad8 --- /dev/null +++ b/panel/app/views/auth/login.php @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/panel/app/views/avatars/delete.php b/panel/app/views/avatars/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/avatars/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/dashboard/index.php b/panel/app/views/dashboard/index.php new file mode 100644 index 0000000..2ffb4d5 --- /dev/null +++ b/panel/app/views/dashboard/index.php @@ -0,0 +1,49 @@ +
    + +
    + + $widget): ?> + +
    + +

    + + + + href=""> + + + + + + + + + + + + + + + + data-shortcut="" href=""> + + href=""> + + + + + + + + +

    + + + +
    + + +
    + +
    \ No newline at end of file diff --git a/panel/app/views/error/index.php b/panel/app/views/error/index.php new file mode 100644 index 0000000..d9be728 --- /dev/null +++ b/panel/app/views/error/index.php @@ -0,0 +1,4 @@ +
    +

    +

    +
    \ No newline at end of file diff --git a/panel/app/views/error/modal.php b/panel/app/views/error/modal.php new file mode 100644 index 0000000..681eedf --- /dev/null +++ b/panel/app/views/error/modal.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/panel/app/views/files/delete.php b/panel/app/views/files/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/files/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/files/edit.php b/panel/app/views/files/edit.php new file mode 100644 index 0000000..57e4370 --- /dev/null +++ b/panel/app/views/files/edit.php @@ -0,0 +1,88 @@ +
    + +
    + + + + + extension() == 'svg'): ?> + + canHavePreview()): ?> + <?php __($file->filename()) ?> + + + filename()) ?> + type() . ' / ' . $file->niceSize()) ?> + + + + +
    + + + +
    + + + + \ No newline at end of file diff --git a/panel/app/views/files/index.php b/panel/app/views/files/index.php new file mode 100644 index 0000000..23068be --- /dev/null +++ b/panel/app/views/files/index.php @@ -0,0 +1,104 @@ +
    + +

    + + isSite()): ?> + + + title()) ?> + + ( count() ?> ) + + + + + + + + hasFiles() and $page->canHaveMoreFiles()): ?> + + + + + +

    + + count()): ?> + + + + +
    +
    +

    + canHaveMoreFiles()) : ?> + + + + +
    +
    + + + +
    + + + + \ No newline at end of file diff --git a/panel/app/views/installation/index.php b/panel/app/views/installation/index.php new file mode 100644 index 0000000..41e2725 --- /dev/null +++ b/panel/app/views/installation/index.php @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/panel/app/views/options/index.php b/panel/app/views/options/index.php new file mode 100644 index 0000000..30e48f9 --- /dev/null +++ b/panel/app/views/options/index.php @@ -0,0 +1,62 @@ +
    + + + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/pages/add.php b/panel/app/views/pages/add.php new file mode 100644 index 0000000..9299152 --- /dev/null +++ b/panel/app/views/pages/add.php @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/panel/app/views/pages/delete.php b/panel/app/views/pages/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/pages/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/pages/edit.php b/panel/app/views/pages/edit.php new file mode 100644 index 0000000..d8c497f --- /dev/null +++ b/panel/app/views/pages/edit.php @@ -0,0 +1,31 @@ +
    + + + +
    +
    + + isWritable()): ?> +
    +

    + +

    +
    +

    +
    +
    + + + +
    +
    + + + + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/pages/template.php b/panel/app/views/pages/template.php new file mode 100644 index 0000000..21d06e5 --- /dev/null +++ b/panel/app/views/pages/template.php @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/panel/app/views/pages/toggle.php b/panel/app/views/pages/toggle.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/pages/toggle.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/pages/url.php b/panel/app/views/pages/url.php new file mode 100644 index 0000000..e9e1dc3 --- /dev/null +++ b/panel/app/views/pages/url.php @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/panel/app/views/search/results.php b/panel/app/views/search/results.php new file mode 100644 index 0000000..5338d9b --- /dev/null +++ b/panel/app/views/search/results.php @@ -0,0 +1,35 @@ +count()): ?> +
    + +
    + + +count()): ?> +
    + +
    + \ No newline at end of file diff --git a/panel/app/views/subpages/index.php b/panel/app/views/subpages/index.php new file mode 100644 index 0000000..8af0578 --- /dev/null +++ b/panel/app/views/subpages/index.php @@ -0,0 +1,157 @@ +
    +

    + + isSite()): ?> + + + title()) ?> + + + + + + + + + children()->count()): ?> + + + + + + + +

    + + hasChildren()): ?> +
    + +
    +

    + + ( total() ?> ) + +

    + +
    +
    + pages() as $subpage): ?> + $page, 'subpage' => $subpage)) ?> + +
    +
    + + pagination() ?> + + total()): ?> +
    + +
    + + +
    +

    + + ( total() ?> ) + +

    + +
    +
    + + pages() as $subpage): ?> + $page, 'subpage' => $subpage)) ?> + + +
    +
    + + pagination() ?> + + total()): ?> +
    + +
    + + +
    + +
    + + + + + +
    +
    +

    + + + +
    +
    + + + + +
    + + \ No newline at end of file diff --git a/panel/app/views/users/delete.php b/panel/app/views/users/delete.php new file mode 100644 index 0000000..fd34175 --- /dev/null +++ b/panel/app/views/users/delete.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/panel/app/views/users/edit.php b/panel/app/views/users/edit.php new file mode 100644 index 0000000..06b643e --- /dev/null +++ b/panel/app/views/users/edit.php @@ -0,0 +1,108 @@ +
    + + + +
    +
    + +
    +

    + +

    +
    +

    +
    +
    +
    + + + +
    +
    + +
    + + \ No newline at end of file diff --git a/panel/app/views/users/index.php b/panel/app/views/users/index.php new file mode 100644 index 0000000..3072f7a --- /dev/null +++ b/panel/app/views/users/index.php @@ -0,0 +1,60 @@ +
    + +

    + + + ( pagination()->items() ?> ) + + + + + + + + +

    + +
    + +
    + + isCurrent()): ?> + + +
    + +
    + + + +
    \ No newline at end of file diff --git a/panel/app/widgets/account/account.html.php b/panel/app/widgets/account/account.html.php new file mode 100644 index 0000000..ffc5ef7 --- /dev/null +++ b/panel/app/widgets/account/account.html.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/panel/app/widgets/account/account.php b/panel/app/widgets/account/account.php new file mode 100644 index 0000000..d369ed8 --- /dev/null +++ b/panel/app/widgets/account/account.php @@ -0,0 +1,22 @@ +user(); + +return array( + 'title' => array( + 'text' => l('dashboard.index.account.title'), + 'link' => $user->url('edit'), + ), + 'options' => array( + array( + 'text' => l('dashboard.index.account.edit'), + 'icon' => 'pencil', + 'link' => $user->url('edit') + ) + ), + 'html' => function() use($user) { + return tpl::load(__DIR__ . DS . 'account.html.php', array( + 'user' => $user + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/history/history.html.php b/panel/app/widgets/history/history.html.php new file mode 100644 index 0000000..4d28c24 --- /dev/null +++ b/panel/app/widgets/history/history.html.php @@ -0,0 +1,18 @@ + diff --git a/panel/app/widgets/history/history.php b/panel/app/widgets/history/history.php new file mode 100644 index 0000000..fd50020 --- /dev/null +++ b/panel/app/widgets/history/history.php @@ -0,0 +1,13 @@ + array( + 'text' => l('dashboard.index.history.title'), + 'link' => false, + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'history.html.php', array( + 'history' => panel()->user()->history()->get() + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/license/license.html.php b/panel/app/widgets/license/license.html.php new file mode 100644 index 0000000..ae0bb43 --- /dev/null +++ b/panel/app/widgets/license/license.html.php @@ -0,0 +1,15 @@ + +
    +
    + +
    +
    \ No newline at end of file diff --git a/panel/app/widgets/license/license.php b/panel/app/widgets/license/license.php new file mode 100644 index 0000000..006904a --- /dev/null +++ b/panel/app/widgets/license/license.php @@ -0,0 +1,25 @@ +license(); + +if($license->type() == 'trial' and !$license->local()) { + + return array( + 'title' => array( + 'text' => l('dashboard.index.license.title'), + 'link' => false, + 'compressed' => false + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'license.html.php', array( + 'text' => kirbytext(str::template(l('dashboard.index.license.text'), array( + 'buy' => 'http://getkirby.com/buy', + 'docs' => 'http://getkirby.com/docs/installation/license-code' + ))) + )); + } + ); + +} else { + return false; +} \ No newline at end of file diff --git a/panel/app/widgets/pages/pages.html.php b/panel/app/widgets/pages/pages.html.php new file mode 100644 index 0000000..785f4a3 --- /dev/null +++ b/panel/app/widgets/pages/pages.html.php @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/panel/app/widgets/pages/pages.php b/panel/app/widgets/pages/pages.php new file mode 100644 index 0000000..a2ffd01 --- /dev/null +++ b/panel/app/widgets/pages/pages.php @@ -0,0 +1,36 @@ +site(); +$options = array(); + +if($site->canHaveSubpages()) { + $options[] = array( + 'text' => l('dashboard.index.pages.edit'), + 'icon' => 'pencil', + 'link' => $site->url('subpages') + ); +} + +if($addbutton = $site->addButton()) { + $options[] = array( + 'text' => l('dashboard.index.pages.add'), + 'icon' => 'plus-circle', + 'link' => $addbutton->url(), + 'modal' => $addbutton->modal(), + 'key' => '+', + ); +} + +return array( + 'title' => array( + 'text' => l('dashboard.index.pages.title'), + 'link' => $site->url('subpages'), + 'compressed' => true + ), + 'options' => $options, + 'html' => function() use($site) { + return tpl::load(__DIR__ . DS . 'pages.html.php', array( + 'pages' => $site->children()->paginated('sidebar') + )); + } +); \ No newline at end of file diff --git a/panel/app/widgets/site/site.html.php b/panel/app/widgets/site/site.html.php new file mode 100644 index 0000000..5f4c1d9 --- /dev/null +++ b/panel/app/widgets/site/site.html.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/panel/app/widgets/site/site.php b/panel/app/widgets/site/site.php new file mode 100644 index 0000000..304f28b --- /dev/null +++ b/panel/app/widgets/site/site.php @@ -0,0 +1,12 @@ + array( + 'text' => l('dashboard.index.site.title'), + 'link' => url(), + 'target' => '_blank', + ), + 'html' => function() { + return tpl::load(__DIR__ . DS . 'site.html.php'); + } +); \ No newline at end of file diff --git a/panel/assets/css/form.min.css b/panel/assets/css/form.min.css new file mode 100644 index 0000000..e3daf73 --- /dev/null +++ b/panel/assets/css/form.min.css @@ -0,0 +1 @@ +.field-with-headline:first-child{padding-top:0}.field-with-headline{counter-increment:count;padding-top:6em}.field-with-headline .hgroup span{padding-left:1.5em}.field-with-headline .hgroup:before{position:absolute;content:counter(count,decimal-leading-zero);left:0;color:#8dae28;font-weight:400}.field-with-image select{margin-left:3rem}.field-with-image .input-preview{position:absolute;top:2px;left:2px;bottom:2px;width:2.75em;background:url(../images/pattern.png)}.field-with-image .input-preview figure{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:center center;background-size:cover}.structure{padding-bottom:.5em}.structure-entry{background:#fff;border:2px solid #ddd;margin-bottom:.5em}.structure-readonly .structure-entry{background:#efefef;color:#777}.structure-entry:last-child{margin-bottom:0}.structure-entry-content{padding:1em 1.5em;border-bottom:1px solid #efefef}.structure[data-sortable=true] .structure-entry-content{cursor:move}.structure-entry-options .btn{padding:.75em 1.5em;width:50%;float:left;border-right:1px solid #efefef}.structure-empty{padding:1.5em;background:#ddd}.fileview-sidebar .structure-empty{background:0 0;border-radius:5px;border:1px dashed #ddd;padding:1rem 1.5rem 1.25rem}.structure-empty a{border-bottom:2px solid #aaa;margin-left:.5em}.fileview-sidebar .structure-empty a{display:inline-block;margin-left:0}.structure-empty a:hover{border-color:#000}.structure-add-button{cursor:pointer}.structure-table{width:100%;border-spacing:0;border:2px solid #ddd;border-bottom:1px solid #ddd;border-right:1px solid #ddd;table-layout:fixed}.structure-table td,.structure-table th{background:#fff;border-bottom:1px solid #ddd;border-right:1px solid #ddd;text-align:left;vertical-align:top}.structure-table th{padding:.5em;font-weight:400;color:#777;font-style:italic}.structure-table td a{display:block;padding:.5em;overflow:hidden;width:100%;text-overflow:ellipsis;cursor:move}.structure-table-options{width:3rem;text-align:center}.structure-table .structure-table-options a{text-align:center;cursor:pointer}.structure-sortable-helper{border-top:1px solid #ddd;border-left:1px solid #ddd}.field-counter{position:absolute;z-index:-1;right:0;top:0;text-align:right;font-size:.9em;line-height:1.66666666666667}.field-counter.outside-range{color:#b3000a} \ No newline at end of file diff --git a/panel/assets/css/panel.min.css b/panel/assets/css/panel.min.css new file mode 100644 index 0000000..b65c47f --- /dev/null +++ b/panel/assets/css/panel.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";body.ltr .shiv-left:after,body.rtl .shiv-right:after{left:-2em}#nprogress,.field-icon{pointer-events:none}.dashboard-items,.input-list-item,.nav>li{list-style:none}.btn-with-icon,.cut,.dashboard-item,.draggable-helper,.dropdown-list>li,.field-buttons,.tag .tag-label{white-space:nowrap}.breadcrumb-label:after,.breadcrumb-link:after,.breadcrumb-link:before,.breadcrumb-list:after,.cf:after,.field-buttons:after,.field-buttons:before,.hgroup a:after,.languages-toggle span:after,.shiv:after,.sidebar-toggle:after,.topbar .message-content:after,body>.message .message-content:after,form.loading:after{content:""}.cut,.dashboard-item-text,.dropdown-list>li>a,.input-list-item .input,.uid-preview{text-overflow:ellipsis}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:400;src:url(../fonts/sourcesanspro-400.woff2) format("woff2"),url(../fonts/sourcesanspro-400.woff) format("woff")}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:600;src:url(../fonts/sourcesanspro-600.woff2) format("woff2"),url(../fonts/sourcesanspro-600.woff) format("woff")}@font-face{font-family:'Source Sans Pro';font-style:italic;font-weight:400;src:url(../fonts/sourcesanspro-400-italic.woff2) format("woff2"),url(../fonts/sourcesanspro-400-italic.woff) format("woff")}*,:after,:before{margin:0;padding:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.text figure,.text p{margin-bottom:1.5em}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}abbr{border:none}img{max-width:100%}img.lazy{opacity:0;-webkit-transition:opacity .3s;-moz-transition:opacity .3s;-ms-transition:opacity .3s;transition:opacity .3s}img.lazy.has-loaded{opacity:1}body,html{height:100%}body.rtl{direction:rtl}.cf:after{display:table;clear:both}.cut{overflow-x:hidden}.hidden{display:none!important}.nav>li>a,.text figure,.uid-preview,hr{display:block}.grey{background:#efefef}.white{background:#fff}.app{padding-top:3em;background:#efefef}.section{padding:1.5em}.bars{position:relative;min-height:100%}.draggable-helper{position:fixed;z-index:10000;background:#000;padding:.25em 1em;color:#fff;width:auto!important;list-style:none;border-radius:3px;cursor:pointer}.draggable-helper a{color:#fff!important}.draggable-helper-with-image{border:2px solid #000;padding:0;line-height:0;border-radius:0;width:79px;height:79px;background:url(../images/pattern.png) #000}.draggable-helper-with-image img{position:relative;z-index:1;width:75px;height:75px;object-fit:cover}.uid-preview{max-width:20em;overflow:hidden;-ms-word-break:break-word;word-break:break-word}.shiv:after{position:absolute;width:2em;height:100%;top:0}body.ltr .shiv-right:after,body.rtl .shiv-left:after{right:-2em}.shiv-white{background:#fff}body.ltr .shiv-white:after{background:-webkit-linear-gradient(left,rgba(255,255,255,0),#fff);background:-moz-linear-gradient(left,rgba(255,255,255,0),#fff);background:-ms-linear-gradient(left,rgba(255,255,255,0),#fff);background:linear-gradient(left,rgba(255,255,255,0),#fff)}body.rtl .shiv-white:after{background:-webkit-linear-gradient(right,rgba(255,255,255,0),#fff);background:-moz-linear-gradient(right,rgba(255,255,255,0),#fff);background:-ms-linear-gradient(right,rgba(255,255,255,0),#fff);background:linear-gradient(right,rgba(255,255,255,0),#fff)}.shiv-grey{background:#efefef}body.ltr .shiv-grey:after{background:-webkit-linear-gradient(left,rgba(239,239,239,0),#efefef);background:-moz-linear-gradient(left,rgba(239,239,239,0),#efefef);background:-ms-linear-gradient(left,rgba(239,239,239,0),#efefef);background:linear-gradient(left,rgba(239,239,239,0),#efefef)}body.rtl .shiv-grey:after{background:-webkit-linear-gradient(right,rgba(239,239,239,0),#efefef);background:-moz-linear-gradient(right,rgba(239,239,239,0),#efefef);background:-ms-linear-gradient(right,rgba(239,239,239,0),#efefef);background:linear-gradient(right,rgba(239,239,239,0),#efefef)}#nprogress .bar{background:#fff;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.alpha,.beta,.delta,.epsilon,.gamma,.text h1,.text h2,.text h3,.text h4,.text h5,.text h6,.zeta,h1,h2,h3,h4,h5,h6{font-size:1em;font-weight:600}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color:inherit}*{font-size:100%;font-family:"Source Sans Pro","Apple LiGothic Medium","Microsoft JhengHei UI","Helvetica Neue",Arial,sans-serif}.label,.text{font-size:1em}.light{font-weight:300}.strong,b,strong{font-weight:600}a{text-decoration:none;color:#000}.marginalia{color:#777}hr{height:2px;background:#ddd;border:0}.text{line-height:1.5em}.text a{font-weight:400;border-bottom:2px solid #ddd}.text a:hover{border-color:#000}.text mark{background:#8dae28;padding:0 5px;color:#fff}body.ltr .text blockquote{padding-left:1.5em;border-left:6px solid #ddd}body.rtl .text blockquote{padding-right:1.5em;border-right:6px solid #ddd}body.ltr .nav-icon-left,body.rtl .nav-icon-right{border-right:1px solid #555}.text hr{margin:1.5em 0}.text ul{margin-bottom:1.5em}.text ul ol,.text ul ul{margin-bottom:0}body.ltr .text ul{margin-left:1em}body.rtl .text ul{margin-right:1em}.text ol{margin-bottom:1.5em}.text ol ol,.text ol ul{margin-bottom:0}body.ltr .text ol{margin-left:1.25em}body.rtl .text ol{margin-right:1.25em}.btn,.nav-icon{display:inline-block;line-height:1em}body.ltr .nav-bar>li{float:left}body.rtl .nav-bar>li{float:right}.nav-icon{background:#000;color:#fff;padding:1em 0;text-align:center;height:3em;width:4em}.nav-icon:hover{color:#999}body.ltr .nav-icon-right,body.rtl .nav-icon-left{border-left:1px solid #555}.btn{background:0 0;border:0;cursor:pointer;outline:0;vertical-align:middle;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none}.dashboard-section,.field-grid-item,.tag,.tag-input{vertical-align:top}.input,.input-with-selectbox .selectbox{-webkit-appearance:none;-moz-appearance:none}.btn::-moz-focus-inner{border:0;padding:0}.btn-rounded{font-weight:600;border-radius:5em;border:2px solid #000;padding:.4em 1.5em}.btn-rounded:focus,.btn-rounded:hover{background:#000;color:#fff}.btn-negative{border-color:#b3000a;color:#b3000a}.btn-negative:focus,.btn-negative:hover{background:#b3000a}.btn-positive{border-color:#8dae28;color:#8dae28}.btn-positive:focus,.btn-positive:hover{background:#8dae28}.btn-with-icon{color:#777}body.ltr .btn-with-icon{text-align:left}body.rtl .btn-with-icon{text-align:right}.buttons-centered,.dropload-text,.dropzone-text,.field-buttons,.field-buttons .btn,.field-icon .icon,.field-icon span,.message-toggle i{text-align:center}.btn-with-icon .icon,.btn-with-icon:focus,.btn-with-icon:hover{color:#000}.btn-addit i{line-height:0}.btn-wide{min-width:10em}fieldset{border:0}form.loading:after{position:fixed;top:0;left:0;right:0;bottom:0;cursor:wait;z-index:10000}.label{position:relative;font-weight:600;display:block;line-height:1.5em;padding-bottom:.5em}.label abbr{color:#8dae28;border:0}body.ltr .label abbr{padding-left:.25em}body.rtl .label abbr{padding-right:.25em}.label-option{position:absolute;top:0;font-weight:400;color:#777;line-height:1.5em}.field,.field-content{position:relative}body.ltr .label-option{right:0}body.rtl .label-option{left:0}.label-option .icon,.label-option:hover{color:#000}.field{margin-bottom:1.5em}.field-help{display:block;font-size:.9em;padding:.5em 0;font-style:italic}.field-help .pw-suggestion{background:#dedede;padding:.15em .75em;border-radius:1em;line-height:1em;font-size:1em;font-family:Courier,monospace;font-style:normal;cursor:pointer}.field-help .pw-suggestion:hover{background:#8DAE28;color:#fff}.field-with-error .label,.field-with-error .label abbr{color:#b3000a}.field-help .pw-reload{position:relative;top:3px;border:0;margin-left:.5em}.field-with-error .input:focus{border-color:#000}.field-with-info{margin-bottom:0}.field-with-line{clear:both;margin:1.5em 0 2.5em}@media screen and (min-width:60em){.field-with-line{margin:3.5em 0 4.5em}}.field-icon{position:absolute;top:2px;bottom:2px;width:3em;background:#fff}body.ltr .field-icon{right:2px;border-left:1px dashed #ddd}body.rtl .field-icon{left:2px;border-right:1px dashed #ddd}.field-icon .icon{position:absolute;top:50%;left:0;right:0;margin-top:-7px;color:#777}.field-icon:hover .icon{color:#8dae28}body.ltr .field-with-icon .input{padding-right:3.5em}body.rtl .field-with-icon .input{padding-left:3.5em}.field-icon span{display:block;padding:0;line-height:3em;font-size:.8em;color:#777}.field-is-readonly .field-icon{background:#efefef}body.ltr .field-is-readonly .field-icon{border-left-color:#ccc}body.rtl .field-is-readonly .field-icon{border-right-color:#ccc}.field-is-disabled{opacity:.5}.field-is-disabled .label{color:#555}.field-buttons{position:absolute;bottom:2px;left:2px;right:2px;border-top:1px solid #efefef;background:#fff;line-height:0;overflow-y:hidden}.field-buttons:after,.field-buttons:before{position:absolute;top:0;bottom:0;width:1em;pointer-events:0;z-index:1}.breadcrumb-list:after,.file-preview object,.fileview-preview-link object,.input-is-readonly .selectbox-wrapper,.input-is-readonly[type=checkbox],.input-is-readonly[type=radio],.is-disabled .pika-button,body.over *{pointer-events:none}.field-buttons:before{left:0;background:-webkit-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:-moz-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:-ms-linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0));background:linear-gradient(left,rgba(0,0,0,.05),rgba(255,255,255,0))}.field-buttons:after{right:0;background:-webkit-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:-moz-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:-ms-linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0));background:linear-gradient(right,rgba(0,0,0,.05),rgba(255,255,255,0))}@media screen and (min-width:30em){.field-buttons:after,.field-buttons:before{display:none}}.field-buttons ul{overflow:auto}.input-with-checkbox,.input-with-radio,.input-with-selectbox,.selectbox-wrapper,.topbar .message{overflow:hidden}.field-buttons li{display:inline-block;border-right:1px solid #efefef;float:none!important}.field-buttons li:last-child{border-right:0}.field-buttons .btn{padding:.5em 1.5em;display:block;line-height:1em;width:100%}.field-buttons .btn:hover{color:#000}.field-with-buttons .input{min-height:10em;padding-bottom:1.5em!important}body.ltr .field-grid{margin-left:-1.5em}body.rtl .field-grid{margin-right:-1.5em}.field-grid-item{display:inline-block;width:100%}body.ltr .field-grid-item{padding-left:1.5em}body.rtl .field-grid-item{padding-right:1.5em}@media screen and (min-width:60em){.field-grid-item-1-2,.field-grid-item-2-4{width:50%}.field-grid-item-1-4{width:25%}.field-grid-item-3-4{width:75%}.field-grid-item-1-3{width:33.3333333%}.field-grid-item-2-3{width:66.666666%}.field-grid-item-1-5{width:20%}.field-grid-item-2-5{width:40%}.field-grid-item-3-5{width:60%}.field-grid-item-4-5{width:80%}}.input,.range,.selectbox{width:100%}.input{padding:.5em;font-size:1em;line-height:1.5em;font-weight:400;border:2px solid #ddd;background:#fff;display:block;-ms-appearance:none;appearance:none;border-radius:0;min-height:2.75em}.input:-webkit-autofill{box-shadow:0 0 0 1000px #fff inset!important}.input:focus{outline:0;border-color:#8dae28}.input.over{border-color:#000}textarea.input{resize:none}.input:invalid,.input:required{box-shadow:none}.input-is-readonly{background:#efefef;border-color:#ddd;color:#777}.input-is-readonly.input-is-focused,.input-is-readonly:focus{border-color:#ddd}.input-is-focused{border-color:#8dae28}.input-with-radio label{cursor:pointer}.input-with-radio .radio{position:relative;top:-1px}body.ltr .input-with-radio .radio{margin:0 1em 0 .25em}body.rtl .input-with-radio .radio{margin:0 .25em 0 1em}body.ltr .input-with-checkbox .checkbox{float:left;margin:.3em 1em .3em .25em}body.rtl .input-with-checkbox .checkbox{float:right;margin:.3em .25em .3em 1}.selectbox-wrapper{height:1.5em}.input-with-selectbox{cursor:pointer;padding:.5em;line-height:1em}body.ltr .input-with-selectbox .selectbox-wrapper{margin-right:-3em}body.rtl .input-with-selectbox .selectbox-wrapper{margin-left:-3em}.input-with-selectbox .selectbox{line-height:1.5em;display:block;cursor:pointer;appearance:none;border:0;background:0 0;outline:0;border-radius:0}.input-with-selectbox .selectbox:-moz-focusring{color:transparent;text-shadow:0 0 0 #000}.input-with-fileupload{line-height:1em;padding:.75em .5em}.input-with-tags{padding:3px}.input-with-tags.input-is-readonly .tag{opacity:.25}body.ltr .field-with-icon .input-with-tags{padding-right:3.25em}body.rtl .field-with-icon .input-with-tags{padding-left:3.25em}.tag-input{margin:3px;display:block;-moz-appearance:none;-webkit-appearance:none;-ms-appearance:none;appearance:none;border:0;padding:.25em;width:auto;outline:0;background:0 0}@media screen and (min-width:30em){.tag-input{display:inline-block}}.input-list-item{margin-bottom:.5em}.input-list-item .input{white-space:nowrap;overflow:hidden}.input-with-items{padding:0}.input-with-items .item{border-bottom:1px solid #efefef}.input-with-items .item:last-child{border-bottom:0}.buttons{margin:.5em 0}body.ltr .buttons .btn-cancel{float:left}body.rtl .buttons .btn-cancel{float:right}body.ltr .buttons .btn-submit{float:right;margin-left:1rem}body.rtl .buttons .btn-submit{float:left;margin-right:1rem}.btn-submit .btn:first-child{border-top-right-radius:0;border-bottom-right-radius:0;padding-right:1rem}.btn-submit .btn:last-child{border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;padding-left:1rem}.buttons-centered .btn{margin:0 .5em}.buttons-centered .btn-cancel,.buttons-centered .btn-submit{float:none!important}#form-field-username{text-transform:lowercase}@media screen and (min-width:50em){.mainbar .form{padding:0 1.5em 9em}.mainbar .form .buttons{position:fixed;bottom:0;background:#efefef;background:-webkit-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:-moz-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:-ms-linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));background:linear-gradient(bottom,#efefef,rgba(239,239,239,.9) 75%,rgba(239,239,239,0));margin:0;padding:1.5em 3em}.ltr .mainbar .form .buttons{left:33.33%;right:0}.rtl .mainbar .form .buttons{right:33.33%;left:0}.mainbar .form .buttons .text span{position:relative;padding:.35rem 2rem;background:#8dae28;display:inline-block;margin:0 auto 1.5rem;color:#fff;font-weight:600;box-shadow:rgba(0,0,0,.05) 0 2px 10px}.mainbar .form .buttons .text span:after{position:absolute;content:"";border-top:5px solid #8dae28;border-left:5px solid transparent;border-right:5px solid transparent;bottom:-5px;left:50%;margin-left:-5px}}.dropdown,.modal-content{box-shadow:rgba(0,0,0,.2) 0 2px 10px}.gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0)}.dropzone{position:relative;border:1px dashed #ddd;border-radius:.2em}.grey .dropzone{border-color:#bbb}.dropzone-text{line-height:1.5em}.dropzone-text .marginalia{font-size:.9em;font-style:italic}.icon,.message-toggle i,.tag .tag-x{font-style:normal}.dropzone-progress{position:absolute;top:50%;left:0;right:0;margin-top:-5px;height:10px;border-radius:2em;background:#ddd;display:none}.dropzone-progress span{display:block;border-radius:2em;width:0;height:100%;background:#8dae28}.dropzone-is-loading{border-color:transparent!important}.dropzone-is-loading .dropzone-progress{display:block}.dropzone-is-loading .dropzone-text{opacity:.05}.dropzone.dropzone-input{border-width:2px;transition:border-color .3s;cursor:pointer}.dropzone.dropzone-input:hover{border-color:#bbb}.dropzone.dropzone-input:focus{border-color:#8dae28;outline:0}.grey .dropzone.dropzone-input:hover{border-color:#aaa}.dropload{position:relative;border:2px dashed rgba(0,0,0,.15);border-radius:.2em;transition:border-color .3s;margin-bottom:1.5em}.dropload.over,.dropload:focus,.dropload:hover{border-color:#8dae28}.dropload-text{display:none}.dropload-text strong{display:block}.dropload.is-active [type=file]{position:absolute;top:0;left:0;right:0;bottom:0;width:100%;opacity:0;cursor:pointer}.dropload.is-active [type=submit]{display:none}.dropload.is-active .dropload-text,.tag{display:block}.message{position:relative;color:#fff;line-height:1em;cursor:pointer}.message-is-notice .message-content{background:#8dae28}.message-is-alert .message-content{background:#b3000a}.message-is-alert a{color:#fff;border-bottom:2px solid rgba(255,255,255,.5)}.message-content{display:block;line-height:1.5em;background:#000}body.ltr .message-content{padding:.75em 4.5em .75em 1.5em}body.rtl .message-content{padding:.75em 1.5em .75em 4.5em}.message-toggle{position:absolute;top:50%;margin-top:-.75em;border:2px solid rgba(255,255,255,.5);border-radius:4em;width:1.5em;height:1.5em;transition:border-color .3s;cursor:pointer}body.ltr .message-toggle{right:1.5em}body.rtl .message-toggle{left:1.5em}.message-toggle:hover{border-color:#fff}.message-toggle i{position:absolute;top:50%;left:50%;font-size:1.25em;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.message-is-notice .message-toggle{color:#fff}body>.message{position:absolute;top:0;height:3em;z-index:1;background:#000}body.ltr body>.message{right:0;left:4em;margin-left:-1px}body.rtl body>.message{left:0;right:4em;margin-right:-1px}body>.message .message-content{position:relative}body.ltr body>.message .message-content{margin-right:4.5em;padding-right:1.5em}body.rtl body>.message .message-content{margin-left:4.5em;padding-left:1.5em}body>.message .message-content:after{position:absolute;top:50%;margin-top:-.5em;border-top:.5em solid transparent;border-bottom:.5em solid transparent}body.ltr body>.message .message-content:after{right:-.5em;border-left:.5em solid #8dae28}body.rtl body>.message .message-content:after{left:-.5em;border-right:.5em solid #8dae28}body.ltr body>.message-is-alert .message-content:after{border-left:.5em solid #b3000a}body.rtl body>.message-is-alert .message-content:after{border-right:.5em solid #b3000a}.modal-content .message{margin:-1.5em -1.5em 1.5em}@keyframes showTopbarMessage{0%{-webkit-transform:translateX(6rem);-moz-transform:translateX(6rem);-ms-transform:translateX(6rem);transform:translateX(6rem);opacity:0}100%{-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0);opacity:1}}.file-preview img,.file-preview object,.fileview-image-link{-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}.topbar .message{position:absolute;top:0;z-index:100;background:#000}.ltr .topbar .message{right:0}.rtl .topbar .message{left:0}.topbar .message-content:after{position:absolute;top:50%;margin-top:-7px;border-top:7px solid transparent;border-bottom:5px solid transparent}body.ltr .topbar .message-content:after{right:-7px;border-left:7px solid #8dae28}body.rtl .topbar .message-content:after{left:-7px;border-right:7px solid #8dae28}body.ltr .topbar .message-is-alert .message-content:after{border-left:7px solid #b3000a}body.rtl .topbar .message-is-alert .message-content:after{border-right:7px solid #b3000a}body.ltr .topbar .message-toggle{right:1.25em}.modal,body.rtl .dropdown-left{right:0}body.rtl .topbar .message-toggle{left:1.25em}.modal,body.ltr .dropdown-left{left:0}.topbar .message-content{position:relative;animation-name:showTopbarMessage;animation-iteration-count:1;animation-timing-function:ease-out;animation-duration:.3s;min-width:4.5em;text-align:center}body.ltr .topbar .message-content{margin-right:4rem;padding-right:1.5rem}body.rtl .topbar .message-content{margin-left:4rem;padding-left:1.5em}.tag{margin:3px;position:relative;cursor:pointer;background:#000;border-radius:3px}a.tag{line-height:1.25em}@media screen and (min-width:30em){.tag{display:inline-block}}.tag .tag-label{display:block;width:100%;color:#fff;z-index:1;padding:.25em 1em;text-align:left;border-radius:3px}.tag .tag-label:focus,.tag-is-focused .tag-label{outline:0;background:#8dae28}.tag button{border:0;background:0 0;cursor:pointer}.tag button::-moz-focus-inner{border:0;padding:0}body.ltr .tag button.tag-label{padding-right:3.5em}body.rtl .tag button.tag-label{padding-left:3.5em}.tag .tag-x{position:absolute;top:0;line-height:1em;padding:.4em 1em;z-index:1;color:rgba(255,255,255,.7)}body.ltr .tag .tag-x{right:0;border-left:1px solid rgba(255,255,255,.3)}body.rtl .tag .tag-x{left:0;border-right:1px solid rgba(255,255,255,.3)}.tag .tag-x:focus,.tag .tag-x:hover{color:#fff}.modal{position:fixed;top:0;bottom:0;height:100%;background:rgba(30,30,30,.8);z-index:1000;overflow-y:scroll;-webkit-overflow-scrolling:touch;padding-top:3rem}.modal-content{position:relative;background:#fff;overflow:auto;z-index:2;border-radius:3px;margin:0 1.5em 3em}.modal-content .form{padding:1.5em}@media screen and (min-width:30em){.modal-content{width:22em;margin-left:auto;margin-right:auto}.modal-content-large{width:90%;max-width:40em}.modal-content-medium{width:70%;max-width:30em}}.instruction{text-align:center;padding:6em 0}.instruction-content{display:inline-block;padding:0 1.5em 1.5em;text-align:center}.instruction-text{line-height:1.5em;margin-bottom:1.5em}.dashboard-section{position:relative;display:inline-block;margin-bottom:1.5em;width:100%;padding:1em 1.5em}.dashboard-section ul,.dashboard-section:last-child{margin-bottom:0}.dashboard-box,.dashboard-section .field:last-child{margin-bottom:.5em}.dashboard-box{position:relative;display:block;border:2px solid #ddd;overflow:hidden;background:#efefef}.dashboard-item-icon,.dashboard-item-text{display:inline-block;vertical-align:middle}.dashboard-box .text{padding:.75em 1em}.dashboard-box .text :last-child{margin-bottom:0}.dashboard-items .dashboard-item{border-bottom:1px solid #ddd}.dashboard-items .dashboard-item:last-child{border-bottom:0}.dashboard-item-icon{height:2.5em;width:2.5em;text-align:center}.dashboard-item-icon-with-border{border-right:1px solid #ddd}.dashboard-item-icon i{position:static;padding-top:.8em}.dashboard-item-text{overflow:hidden;padding:0 .75em;width:calc(100% - 2.5em)}.dropdown{position:relative;background:#fff;display:none}.dropdown:after{position:absolute;content:"";top:-.5em;border-bottom:.5em solid #fff;border-left:.5em solid transparent;border-right:.5em solid transparent}.hgroup,.hgroup a,.icon{position:relative}body.ltr .dropdown:after{margin-left:-.1em}body.rtl .dropdown:after{margin-right:-.1em}body.ltr .dropdown-left:after{left:1.5em}body.rtl .dropdown-left:after{right:1.5em}body.ltr .dropdown-right{right:0}body.rtl .dropdown-right{left:0}body.ltr .dropdown-right:after{right:1.5em}body.rtl .dropdown-right:after{left:1.5em}body.ltr .hgroup a:after,body.rtl .hgroup-single-line .hgroup-options{left:0}.dropdown-list>li>a{padding:.75em 1.5em .75em 1em;overflow:hidden;border-bottom:1px solid #efefef}body.ltr .icon-left,body.rtl .icon-right{padding-right:.5em}.dropdown-list>li>a:hover{color:#777}.dropdown-list>li>a:hover i{color:#000}.dropdown-list>li i{display:inline-block;width:2em;text-align:center}.dropdown-dark{background:#000;border-radius:4px}.dropdown-dark:after{display:none}.dropdown-dark .dropdown-list a{color:#fff;border-bottom:1px solid #222}.dropdown-dark .dropdown-list a:hover,.dropdown-dark .dropdown-list a:hover i{color:#999}.dropdown-dark .dropdown-list li:last-child a{border-bottom:0}.icon{font-size:14px;top:-1px;display:inline-block;font-weight:700;text-align:center}body.ltr .icon-right,body.rtl .icon-left{padding-left:.5em}.avatar{display:block;line-height:0}.avatar img{width:4.5em;height:4.5em;padding:2px;border:2px solid #ddd}.hgroup,.hgroup-title{border-bottom:2px solid #ddd;padding-bottom:.5em}.avatar-large img{width:9em;height:9em}.avatar-centered{text-align:center}.avatar-full img{width:100%;height:auto;max-width:100%}.hgroup{line-height:1.5em;margin-bottom:1.5em}.hgroup a{display:inline-block}.hgroup a:after{position:absolute;bottom:-.5em;margin-bottom:-2px;height:2px;width:100%;background:#000}body.rtl .hgroup a:after{right:0}.hgroup-title{display:block;margin-bottom:.5em;font-weight:600;font-size:1em}.hgroup-options{display:block;font-size:1em;font-weight:400;color:#777}.hgroup-options a{display:inline-block}.hgroup-options a:hover{color:#000}body.ltr .hgroup-option-left{float:left}body.ltr .hgroup-option-right,body.rtl .hgroup-option-left{float:right}body.rtl .hgroup-option-right{float:left}body.ltr .hgroup-option-right a{margin-left:1em}body.rtl .hgroup-option-right a{margin-right:1em}@media screen and (max-width:70em){body.ltr .hgroup-option-right>a .icon-left{padding-right:0}body.rtl .hgroup-option-right>a .icon-left{padding-left:0}.hgroup-option-right>a span{display:none}}.file,.file-meta,.file-name,.file-preview{display:block}.hgroup-options .icon{top:0;color:#000}.hgroup-single-line{border:0;padding-bottom:0}.hgroup-single-line .hgroup-title{margin-bottom:0}.hgroup-single-line .hgroup-options{position:absolute;top:0}body.ltr .hgroup-single-line .hgroup-options{right:0}.hgroup-compressed{margin-bottom:.5em}.hgroup .dropdown{position:absolute;z-index:1;margin-top:-2px}.hgroup .dropdown a{font-weight:400}.file{background:#fff}.file-preview{position:relative;padding-bottom:66.66%;line-height:0;background:#000;overflow:hidden}.file-preview-is-image{background:url(../images/pattern.png)}.item,.item-options{background:#fff}.file-preview img,.file-preview object{position:absolute;top:50%;left:50%;max-width:100%;max-height:100%;transform:translate(-50%,-50%)}.file-preview span{position:absolute;top:50%;left:0;right:0;text-align:center;text-transform:uppercase;color:#fff}.item,.item-content{position:relative;overflow:hidden}.file-info{padding:1em 1.5em;line-height:1.5em;border-bottom:1px solid #ddd}.file-name{color:#000}.file-options .btn{width:50%;border-bottom:0;padding:.75em 1.5em}body.ltr .file-options .btn{float:left;text-align:center}body.rtl .file-options .btn{float:right;text-align:center}.file-options .btn:first-child{border-right:1px solid #ddd}.file-options .btn span{display:none}.file-options .btn:last-child{border-bottom:0}@media screen and (min-width:120em){.file-options .btn{padding:1em 1.5em}.file-options .btn span{display:inline}}.items .item,.items.users{margin-bottom:1.5em}.items-with-borders{border:1px solid #ddd}.items .item:last-child{margin-bottom:0}@media screen and (min-width:50em){.items .item{margin-bottom:1px}}.item{line-height:1em}.item-content{border-bottom:1px solid #efefef}.item-info{padding:1em;height:4.25em;overflow:hidden}.item-meta,.item-title{padding:0 .5em;display:block;white-space:nowrap}.item-title{margin-bottom:.25em}.item-options li{width:50%}body.ltr .item-options li{border-left:1px solid #efefef}body.rtl .item-options li{border-right:1px solid #efefef}.item-options-three li{width:33.33%}body.ltr .item-options li:first-child{border-left:0}body.rtl .item-options li:first-child{border-right:0}.item-options .btn{padding:1em 1.5em}@media screen and (min-width:50em){.item-content{border:0;line-height:1em}.item-info{height:3em}.item-title{margin-bottom:0}body.ltr .item-title{float:left}body.rtl .item-title{float:right}body.ltr .item-meta{float:left}body.rtl .item-meta{float:right}.item-options{position:absolute;top:0}body.ltr .item-options{right:0;border-left:1px solid #efefef}body.rtl .item-options{left:0;border-left:1px solid #efefef}.item-options li{width:auto}}.item-image{position:absolute;width:4.25em;height:4.25em}.item-image img{height:100%;width:100%}body.ltr .item-with-image .item-info{margin-left:4.25em}body.rtl .item-with-image .item-info{margin-right:4.25em}@media screen and (min-width:50em){.item-image{width:3em;height:3em}body.ltr .item-with-image .item-info{margin-left:3em}body.rtl .item-with-image .item-info{margin-right:3em}}.item-condensed .item-info{padding:.75em .5em;height:auto}.item-condensed .item-options .btn{padding:1em}@media screen and (min-width:50em){.item-condensed .item-info{height:auto}.item-condensed .item-options .btn{padding:.75em 1.25em}.item-condensed .item-options a span{display:none}body.ltr .item-condensed .item-options i{padding-right:0}body.rtl .item-condensed .item-options i{padding-left:0}}.item-condensed.item-with-image .item-image{position:absolute;width:3em;height:3em}body.ltr .item-condensed.item-with-image .item-info{margin-left:3.75em}body.rtl .item-condensed.item-with-image .item-info{margin-right:3.75em}@media screen and (min-width:50em){.item-condensed.item-with-image .item-image{width:2.5em;height:2.5em}body.ltr .item-condensed.item-with-image .item-info{margin-left:2.5em}body.rtl .item-condensed.item-with-image .item-info{margin-right:2.5em}}.item-options .icon.marginalia{min-width:1em}.breadcrumb{position:relative;background:#000}.breadcrumb-list{height:3em;overflow:hidden;display:none}.breadcrumb-list:after{position:absolute;top:0;bottom:0;width:2em}body.ltr .breadcrumb-list:after{right:0;background:-webkit-linear-gradient(left,transparent,#000);background:-moz-linear-gradient(left,transparent,#000);background:-ms-linear-gradient(left,transparent,#000);background:linear-gradient(left,transparent,#000)}body.rtl .breadcrumb-list:after{left:0;background:-webkit-linear-gradient(right,transparent,#000);background:-moz-linear-gradient(right,transparent,#000);background:-ms-linear-gradient(right,transparent,#000);background:linear-gradient(right,transparent,#000)}.breadcrumb-list li:last-child .breadcrumb-label{max-width:none}.breadcrumb-link{position:relative;line-height:1em;color:#fff;font-weight:400}body.ltr .breadcrumb-link{padding-left:1.25em}body.rtl .breadcrumb-link{padding-right:1.25em}.breadcrumb-link:focus{outline:0}.breadcrumb-label{display:block;white-space:nowrap;overflow:hidden;max-width:6em}body.ltr .breadcrumb-label{padding:1em .75em 1em 0}body.rtl .breadcrumb-label{padding:1em 0 1em .75em}@media screen and (min-width:50em){.breadcrumb .nav-icon,.breadcrumb-dropdown{display:none}.breadcrumb ul{display:block}}.breadcrumb-label:after{position:absolute;top:0;bottom:0;width:.75em}body.ltr .breadcrumb-label:after{right:0;background:-webkit-linear-gradient(left,transparent,#000);background:-moz-linear-gradient(left,transparent,#000);background:-ms-linear-gradient(left,transparent,#000);background:linear-gradient(left,transparent,#000)}body.rtl .breadcrumb-label:after{left:0;background:-webkit-linear-gradient(right,transparent,#000);background:-moz-linear-gradient(right,transparent,#000);background:-ms-linear-gradient(right,transparent,#000);background:linear-gradient(right,transparent,#000)}.breadcrumb-link:hover{color:#999}.breadcrumb-link:after,.breadcrumb-link:before{position:absolute;height:1.6em;width:1px;z-index:1}body.ltr .breadcrumb-link:after,body.ltr .breadcrumb-link:before{right:0}body.rtl .breadcrumb-link:after,body.rtl .breadcrumb-link:before{left:0}.breadcrumb-link:after{top:0;background:-webkit-linear-gradient(bottom,#555,#555 50%,#000);background:-moz-linear-gradient(bottom,#555,#555 50%,#000);background:-ms-linear-gradient(bottom,#555,#555 50%,#000);background:linear-gradient(bottom,#555,#555 50%,#000)}body.ltr .breadcrumb-link:after{-webkit-transform:rotate(-33.75deg);-moz-transform:rotate(-33.75deg);-ms-transform:rotate(-33.75deg);transform:rotate(-33.75deg)}body.rtl .breadcrumb-link:after{-webkit-transform:rotate(33.75deg);-moz-transform:rotate(33.75deg);-ms-transform:rotate(33.75deg);transform:rotate(33.75deg)}.breadcrumb-link:before{bottom:0;background:-webkit-linear-gradient(top,#555,#555 50%,#000);background:-moz-linear-gradient(top,#555,#555 50%,#000);background:-ms-linear-gradient(top,#555,#555 50%,#000);background:linear-gradient(top,#555,#555 50%,#000)}.languages,.topbar{z-index:100;background:#000}body.ltr .breadcrumb-link:before{-webkit-transform:rotate(33.75deg);-moz-transform:rotate(33.75deg);-ms-transform:rotate(33.75deg);transform:rotate(33.75deg)}body.rtl .breadcrumb-link:before{-webkit-transform:rotate(-33.75deg);-moz-transform:rotate(-33.75deg);-ms-transform:rotate(-33.75deg);transform:rotate(-33.75deg)}.languages{position:absolute;top:0;width:4.5em;bottom:0;text-align:center}.grid-item,.languages .dropdown{width:100%}body.ltr .languages{right:4em;border-left:1px solid #555}body.rtl .languages{left:4em;border-right:1px solid #555}.languages-toggle{display:block;color:#fff;line-height:3em;text-transform:uppercase}.languages-toggle span{position:relative;display:block}body.ltr .languages-toggle span{padding-right:1.25em}body.rtl .languages-toggle span{padding-left:1.25em}.languages-toggle span:after{position:absolute;top:50%;border-top:.3em solid #fff;border-left:.3em solid transparent;border-right:.3em solid transparent}body.ltr .languages-toggle span:after{right:1em}body.rtl .languages-toggle span:after{left:1em}body.ltr .languages .dropdown:after{left:50%;margin-left:-.5em}body.rtl .languages .dropdown:after{right:50%;margin-right:-.5em}.pagination{position:relative;margin-bottom:2.5em}.pagination a,.pagination span{padding:.5em 0;display:block}body.ltr .pagination .pagination-prev{float:left;padding-right:1em}body.ltr .pagination .pagination-next,body.rtl .pagination .pagination-prev{float:right;padding-left:1em}.pagination-index{position:absolute;left:2em;right:2em;text-align:center;color:#777}.topbar,body.ltr .topbar .nav-icon-right,body.rtl .topbar .nav-icon-left{right:0}.pagination-index select{position:absolute;top:0;bottom:0;left:50%;cursor:pointer;-webkit-transform:translate(-50%,0);-moz-transform:translate(-50%,0);-ms-transform:translate(-50%,0);transform:translate(-50%,0);min-width:6em;opacity:0}body.ltr .topbar .nav-icon-left,body.rtl .topbar .nav-icon-right{left:0}body.rtl .pagination .pagination-next{float:left;padding-right:1em}.pagination .pagination-inactive{color:#ddd;pointer-events:none}body.ltr .grid{margin-left:-1.5em}body.rtl .grid{margin-right:-1.5em}.grid-item{display:inline-block;vertical-align:top}body.ltr .grid-item{padding-left:1.5em}body.rtl .grid-item{padding-right:1.5em}body.ltr .grid-full{margin-left:0}body.rtl .grid-full{margin-right:0}body.ltr .grid-full .grid-item{padding-left:0}body.rtl .grid-full .grid-item{padding-right:0}.topbar{position:absolute;top:0;left:0;height:3em}.topbar .nav-icon{position:absolute;top:0}body.ltr .topbar .breadcrumb{margin-left:4em}body.rtl .topbar .breadcrumb{margin-right:4em}.topbar .dropdown{position:absolute;z-index:1000;top:3em}.sidebar{padding:1.5em}.sidebar-toggle{position:relative;font-weight:400;display:block;padding:1em 1.5em;color:#777;background:#efefef;border:2px solid #ddd;border-radius:3px}.sidebar-toggle:hover{color:#000}.sidebar-toggle:after{position:absolute;top:50%;margin-top:-.15em;border-top:.4em solid #000;border-left:.4em solid transparent;border-right:.4em solid transparent}body.ltr .sidebar-toggle:after{right:1.5em}body.rtl .sidebar-toggle:after{left:1.5em}.ltr .sidebar-list>li>.option,body.ltr .sidebar-list .icon{left:0}.sidebar-expanded .sidebar-toggle:before{content:attr(data-hide)}.main .loader:after,.subpages-help:after{content:""}.sidebar-expanded .sidebar-toggle{background:#fff;border-color:#fff;border-bottom:1px dashed #ddd}.sidebar-expanded .sidebar-toggle span{display:none}.sidebar-expanded .sidebar-toggle:after{border-top:0;border-bottom:.4em solid #000}.sidebar-content{display:none;background:#fff}.sidebar-expanded .sidebar-content{display:block}.sidebar-content>.marginalia{margin-bottom:1.5em;line-height:1.5em}@media screen and (min-width:50em){.mainbar,.sidebar{overflow-y:scroll;-webkit-overflow-scrolling:touch}.bars-with-sidebar-left:before,.sidebar{position:absolute;top:0;bottom:0;height:100%}body.ltr .bars-with-sidebar-left:before,body.ltr .sidebar,body.rtl .mainbar{left:0}body.ltr .mainbar,body.rtl .bars-with-sidebar-left:before,body.rtl .sidebar{right:0}.sidebar{width:33.33%;padding:0}.sidebar-toggle{display:none!important}.sidebar-content{display:block!important}.mainbar{position:absolute;top:0;bottom:0;width:66.66%}.bars-with-sidebar-left:before{content:"";width:33.33%;background:#fff;z-index:-1}}.sidebar-list{margin-bottom:1.5em}.sidebar-list>li{position:relative;white-space:nowrap;overflow:hidden}body.ltr .sidebar-list>li>a{padding:.25em 0 .25em 1.75em}body.rtl .sidebar-list>li>a{padding:.25em 1.75em .25em 0}.sidebar-list>li>.option{position:absolute;top:-1px;z-index:1;text-align:right;padding:0!important;background:#fff;display:none;font-size:18px}.rtl .sidebar-list>li>.option{right:0}.sidebar-list>li:hover .option{display:block}.sidebar-list>li:hover .draggable .icon{display:none}.sidebar-list .icon{position:absolute;top:.4em}body.rtl .sidebar-list .icon{right:0}.sidebar-list .marginalia{position:absolute}body.ltr .sidebar-list .marginalia{right:0;padding-left:.5em}body.rtl .sidebar-list .marginalia{left:0;padding-right:.5em}.sidebar-search{padding:.5em 0;margin:0 -.25em}.sidebar .pagination{border-top:2px solid #ddd;margin-top:-1em}.files .grid-item{margin-bottom:1.5em}@media screen and (min-width:40em){.dashboard{-webkit-column-count:2;-webkit-column-gap:1.5em;-moz-column-count:2;-moz-column-gap:1.5em;column-count:2;column-gap:1.5em}.files .grid-item{width:50%}}@media screen and (min-width:50em){.files .grid-item{width:33.33%}}@media screen and (min-width:60em){.dashboard{-webkit-column-count:3;-moz-column-count:3;column-count:3}.breadcrumb-label{max-width:9em}.files .grid-item{width:25%}}@media screen and (min-width:70em){.breadcrumb-label{max-width:12em}.files .grid-item{width:20%}}@media screen and (min-width:80em){.files .grid-item{width:16.66%}}.subpages{padding:.25em;margin-bottom:1em}body.ltr .subpages-help-left,body.rtl .subpages-help-right{padding-left:3em}body.ltr .subpages-help-right,body.rtl .subpages-help-left{padding-right:3em}.subpages .item{margin-bottom:.25em}.dropzone.subpages .items{min-height:2.5em}.subpages .items.sortable .item-info{cursor:move}.subpages-grid .grid-item{margin-bottom:1.5em}.subpages-grid h3{margin-bottom:1em;font-weight:400;color:#777}@media screen and (min-width:50em){.subpages .item{margin-bottom:1px}.subpages-grid .grid-item{width:50%}}.subpages-help{position:relative;font-style:italic}.subpages-help:after{position:absolute;top:-1.5em;height:6em;width:3em}.fileview-preview-link,.main{height:100%}body.ltr .subpages-help-right:after{right:0;background:url(../images/hint.arrows.png) top right no-repeat}body.ltr .subpages-help-left:after,body.rtl .subpages-help-right:after{left:0;background:url(../images/hint.arrows.png) top left no-repeat}body.rtl .subpages-help-left:after{right:0;background:url(../images/hint.arrows.png) top right no-repeat}.fileview{background:#fff}.fileview-image{position:relative;padding-bottom:66.66%;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOTdEOEI3OUE3MDMxMUUzOEIxNEZERTM0N0EzRjlGMSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOTdEOEI3QUE3MDMxMUUzOEIxNEZERTM0N0EzRjlGMSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI5N0Q4Qjc3QTcwMzExRTM4QjE0RkRFMzQ3QTNGOUYxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI5N0Q4Qjc4QTcwMzExRTM4QjE0RkRFMzQ3QTNGOUYxIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+jGcG/wAAAAlQTFRF////zMzMzc3NCvMx6wAAACJJREFUeNpiYGRkYgQBBhgYIAGEBCO6SpIFmKCADDMAAgwATVgAkU8MrdIAAAAASUVORK5CYII=);text-align:center;overflow:hidden}.fileview-image-link{position:absolute;top:50%;max-width:75%;max-height:75%;width:100%;left:50%;transform:translate(-50%,-50%)}.fileview-image-link span{background:#000;display:inline-block;line-height:1.5em;padding:1em 1.5em;border-radius:3px;color:#999;font-weight:300;overflow:hidden}.fileview-image-link strong{color:#fff;font-weight:400;display:block}.fileview-preview-link img,.fileview-preview-link object,.fileview-preview-link span{position:absolute;top:50%;left:50%;max-width:100%;max-height:100%;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.fileview-nav{position:absolute;top:50%;left:0;right:0;margin-top:-1.5em}.fileview-nav a{display:block;padding:1em}.fileview-nav-prev{float:left}.fileview-nav-next{float:right}.fileview-options{margin-top:6em;border-top:2px solid #ddd}.fileview-options li{width:100%}.fileview-options .btn{padding:.75em 0;border-bottom:1px solid #ddd}@media screen and (min-width:50em){.fileview,.fileview-image,.fileview-sidebar{position:absolute;bottom:0}.fileview{top:3em;left:0;right:0;overflow:hidden}.fileview-image{top:0;padding:0}.ltr .fileview-image{left:0;right:50%}.rtl .fileview-image{left:50%;right:0}.fileview-sidebar{top:0;width:50%;overflow:auto}.ltr .fileview-sidebar{right:0}.rtl .fileview-sidebar{left:0}.fileview-options{margin-bottom:6em}.fileview-options li{width:33.33%}.fileview-options .btn{padding:1em;text-align:center;border-bottom:none}}@media screen and (min-width:65em){.fileview-nav{left:1.5em;right:1.5em}.ltr .fileview-image{right:33.33%}.rtl .fileview-image{left:33.33%}.fileview-sidebar{width:33.33%}}.search{width:20em}.search:after{border-bottom-color:#8dae28}.search-results{background:#000}.search-input{width:100%;padding:.75em 1em;border:0;background:#8dae28}.search-input::-webkit-input-placeholder{color:#404f12}.search-input:-moz-placeholder{color:#404f12}.search-input::-moz-placeholder{color:#404f12}.search-input:-ms-input-placeholder{color:#404f12}.search-input:focus{outline:0}.search-section{padding:.5em;border-bottom:1px solid rgba(255,255,255,.2)}.search-section:last-child{border-bottom:0}.search-section li>a{position:relative;padding:.5em 1em;line-height:1em;color:#fff;display:block;white-space:nowrap}.search-section a small,.search-section a strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-section .icon,.search-section a span{display:inline-block;vertical-align:middle}.search-section a strong{padding-bottom:.125em;font-weight:400}.search-section a small{font-size:.7em;color:#999;padding-top:.125em}.search-section li.active a,.search-section li:hover a{color:#8dae28}.search-section li.active a small,.search-section li:hover a small{color:#fff}.search-section .icon{width:2em;text-align:left}.file-selector{border:1px solid #efefef;max-height:15.4em;overflow:auto}.file-selector label{cursor:pointer}.file-selector input{position:absolute;top:1.1em}body.ltr .file-selector input{right:1.5em}body.rtl .file-selector input{left:1.5em}.main .loader{display:none}.main .loader:after{position:fixed;top:50%;width:3em;height:3em;margin-top:-1.5em;background:url(../images/loader.black.gif) center center no-repeat #fff;border-radius:50%;z-index:10000000}.autocomplete:after,.pika-single:before{top:-.5em;content:""}body.ltr .main .loader:after{left:50%;margin-left:-1.5em}body.rtl .main .loader:after{right:50%;margin-right:-1.5em}.autocomplete{position:absolute;margin-top:.5em;width:auto;z-index:1000}.autocomplete:after{position:absolute;border-left:.5em solid transparent;border-right:.5em solid transparent;border-bottom:.5em solid #000}body.ltr .autocomplete:after{left:1em}body.rtl .autocomplete:after{right:1em}.autocomplete button{display:block;width:100%;border:0;padding:.5em 1em;border-bottom:1px solid rgba(255,255,255,.2);color:#fff;background:#000;cursor:pointer}body.ltr .autocomplete button{text-align:left}body.rtl .autocomplete button{text-align:right}.fa-fw,.fa-li,.pika-button,.pika-table th,.pika-title,body.over:after{text-align:center}.autocomplete button:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.autocomplete button:last-child{border:none;border-bottom-left-radius:3px;border-bottom-right-radius:3px;box-shadow:rgba(0,0,0,.1) 0 5px 10px}.autocomplete button:focus{outline:0;background:#8dae28}.autocomplete button::-moz-focus-inner{border:0;padding:0}.autocomplete button strong{color:#8dae28;font-weight:400}.autocomplete button:focus strong{color:#000}.pika-single{display:block;position:relative;color:#fff;background:#000;margin-top:1em;margin-left:-15px;border:3px solid #151515;border-radius:3px;box-shadow:rgba(0,0,0,.1) 0 5px 10px;z-index:1000}.pika-single:before{position:absolute;left:50%;margin-left:-.5em;border-left:.5em solid transparent;border-right:.5em solid transparent;border-bottom:.5em solid #151515}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-title{position:relative}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:.6em .3em .7em;font-weight:400;color:#eee}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;filter:alpha(opacity=0);opacity:0}.pika-next,.pika-prev{position:absolute;display:block;cursor:pointer;outline:0;border:0;padding:0 1em;white-space:nowrap;overflow:hidden;background:0 0;font-size:2em;line-height:1;font-weight:400;top:0;color:#fff}.pika-next:hover,.pika-prev:hover{color:#fff}.is-rtl .pika-next,.pika-prev{left:.1em}.is-rtl .pika-prev,.pika-next{right:.1em}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%}.pika-table th{color:#999;font-weight:400;letter-spacing:1px;padding:.6em .3em;font-size:.9em;border-top:1px solid #333;font-style:none;border-bottom:1px solid #333}.pika-table th abbr{border:0}.pika-button{display:block;outline:0;border:0;width:2em;height:2em;color:#eee;font-size:1em;background:0 0;cursor:pointer;border-radius:50%;margin:.25em}.counter,.fa{display:inline-block}.is-today .pika-button{color:#8dae28}.is-selected .pika-button,.is-selected .pika-button:hover{color:#000;background:#8dae28}.is-disabled .pika-button{cursor:default;color:#fff;opacity:.3}.pika-button:hover{color:#000;background:#fff}.counter{padding:0 .5em;font-weight:400;color:#777}body.loading:before,body.over:before{position:fixed;content:"";top:0;right:0;bottom:0;left:0;background:rgba(30,30,30,.8);z-index:10000000}body.loading:after,body.over:after{position:fixed;top:50%;left:50%;z-index:10000000}body.loading:after{content:"";width:2em;height:2em;margin-left:-1em;margin-top:-1em;border-radius:50%;background:url(data:image/gif;base64,R0lGODlhEAAQAPMAAOfn51JSUpSUlCcnJ0JCQtvb27OzszU1NXp6es3NzaWlpfj4+GBgYIiIiGxsbP///yH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAETBDISau9dYXBh2zCYmmddAxEkRElYLCEOAnoNi2sQG2GRhmDAIWDIU6MGSSAR0G4ghRa7IjIUXAog6QzpRRYhy0nILsKGuJxGcNuTyIAIfkECQoADAAsAgAAAAwAEAAABFWQyXKUvJiREVJmi3AMZIEVGzkgIABuhDIcwBIQ4YAzgfAOgsDAICEWOaQPSDVYKElC48Jo4Ah0zuBPsCCwACMFzOliIJgE06WgOnA/CSFBeVEc1IwIACH5BAkKAA0ALAIAAAAMABAAAARTsMnZAKO4qWHoEsEwHOMiFYSoikKzpESXiEcnDITZ3IgedhJBwWW4DTKSFXLBMpgKLYlhEJAsEIPoK9swkAaJhQFmMq5wQxfpGxDopAMFcsIAICMAIfkECQoAAAAsAgABAA0ADQAABEYQyEnBWnKJMEYQV5AABdGdA8Eli0kYkmF2iZBiGUc3BzzptIpQkigWcRUUgmI4NACn5aQ1EEA7r4vsBnXMUISCZKnRfZARACH5BAkKAAwALAAAAgAQAAwAAARUkEm2RBgjiDU7IFg4EEU3IYTBGSDBTSXVLaDAlMmAACajDAfCgXEBbiSGIkbxCwUME8EBE2AEp4oeADFI3BiC0Uuy4MTILdWCFRjDQKIRr1cpatwRACH5BAUKAA0ALAAAAgAQAAwAAARTsMkpxghizbmEaoZlEcVmHMOxaCAxECwjDsi2uIIEKKhVTqHAxpIQ5IYDJEtiTFg2gYFhUxAdFADma9lAzBiS28ugWaEO00nBJZoaMpuwIDo4biIAOw==) center center no-repeat #fff}body.over:after{content:"\f0ee";font-family:FontAwesome;color:#fff;font-size:3em;width:3em;height:1em;margin-left:-1.5em;margin-top:-.5em}/*! + * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(../fonts/fontawesome-webfont.woff2?v=4.5.0) format("woff2"),url(../fonts/fontawesome-webfont.woff?v=4.5.0) format("woff");font-weight:400;font-style:normal}.fa{font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em}.fa-li.fa-lg{left:-1.85714em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""} \ No newline at end of file diff --git a/panel/assets/fonts/fontawesome-webfont.woff b/panel/assets/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..dc35ce3 Binary files /dev/null and b/panel/assets/fonts/fontawesome-webfont.woff differ diff --git a/panel/assets/fonts/fontawesome-webfont.woff2 b/panel/assets/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..500e517 Binary files /dev/null and b/panel/assets/fonts/fontawesome-webfont.woff2 differ diff --git a/panel/assets/fonts/sourcesanspro-400-italic.woff b/panel/assets/fonts/sourcesanspro-400-italic.woff new file mode 100644 index 0000000..525588f Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-400-italic.woff differ diff --git a/panel/assets/fonts/sourcesanspro-400-italic.woff2 b/panel/assets/fonts/sourcesanspro-400-italic.woff2 new file mode 100755 index 0000000..a008526 Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-400-italic.woff2 differ diff --git a/panel/assets/fonts/sourcesanspro-400.woff b/panel/assets/fonts/sourcesanspro-400.woff new file mode 100644 index 0000000..00b3703 Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-400.woff differ diff --git a/panel/assets/fonts/sourcesanspro-400.woff2 b/panel/assets/fonts/sourcesanspro-400.woff2 new file mode 100755 index 0000000..0dd3464 Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-400.woff2 differ diff --git a/panel/assets/fonts/sourcesanspro-600.woff b/panel/assets/fonts/sourcesanspro-600.woff new file mode 100644 index 0000000..8a165ec Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-600.woff differ diff --git a/panel/assets/fonts/sourcesanspro-600.woff2 b/panel/assets/fonts/sourcesanspro-600.woff2 new file mode 100755 index 0000000..2526d2e Binary files /dev/null and b/panel/assets/fonts/sourcesanspro-600.woff2 differ diff --git a/panel/assets/images/avatar.png b/panel/assets/images/avatar.png new file mode 100644 index 0000000..e200295 Binary files /dev/null and b/panel/assets/images/avatar.png differ diff --git a/panel/assets/images/hint.arrows.png b/panel/assets/images/hint.arrows.png new file mode 100644 index 0000000..8314e23 Binary files /dev/null and b/panel/assets/images/hint.arrows.png differ diff --git a/panel/assets/images/loader.black.gif b/panel/assets/images/loader.black.gif new file mode 100644 index 0000000..ea331c3 Binary files /dev/null and b/panel/assets/images/loader.black.gif differ diff --git a/panel/assets/images/loader.white.gif b/panel/assets/images/loader.white.gif new file mode 100644 index 0000000..609cac7 Binary files /dev/null and b/panel/assets/images/loader.white.gif differ diff --git a/panel/assets/images/pattern.png b/panel/assets/images/pattern.png new file mode 100644 index 0000000..4bf0644 Binary files /dev/null and b/panel/assets/images/pattern.png differ diff --git a/panel/assets/images/placeholder.png b/panel/assets/images/placeholder.png new file mode 100644 index 0000000..c82dbb5 Binary files /dev/null and b/panel/assets/images/placeholder.png differ diff --git a/panel/assets/js/dist/app.min.js b/panel/assets/js/dist/app.min.js new file mode 100644 index 0000000..7577c14 --- /dev/null +++ b/panel/assets/js/dist/app.min.js @@ -0,0 +1 @@ +var app={setup:function(){NProgress.configure({showSpinner:!1}),app.delay=Delay(),app.content=Content(app),app.content.setup(),app.modal=Modal(app),app.modal.setup(),new Context,new Search,$.ajaxPrefilter(function(t,a,e){a.type&&"post"==a.type.toLowerCase()&&(t.data=$.param($.extend(a.data,{csrf:$("body").attr("data-csrf")})))}),$(document).on("click","a",function(t){var a=$(this),e=a.attr("href")||"";return a.is("[data-dropdown]")||e.match(/^#/)?!0:a.is("[data-modal]")?(app.modal.open(a.attr("href")),!1):a.is("[target]")?!0:(app.content.open(e),!1)}),$(document).on("keydown",function(t){switch(t.keyCode){case 83:case 13:return t.metaKey||t.ctrlKey?(app.hasModal()||app.content.form().trigger("submit"),!1):!0;case 27:return app.modal.close(),!1}}),$(document).dropdown()},hasModal:function(){return $(".modal-content").length>0},load:function(t,a,e){if(app.isLoading(!0),"modal"==a)var o={modal:!0};else var o=!1;$.ajax({url:t,method:"GET",headers:o}).success(function(a,o,n){if(app.isLoading(!1),"object"!==$.type(a)||!a.user||!a.direction)return window.location.href=t;document.title=a.title;var r=$("body");r.hasClass(a.direction)||("ltr"==a.direction?r.removeClass("rtl").addClass("ltr"):r.removeClass("ltr").addClass("rtl"));try{e(a)}catch(d){window.location.href=t}}).error(function(){window.location.href=t})},csrf:function(){return $("body").attr("data-csrf")},isLoading:function(t){t?app.delay.start("loader",function(){NProgress.start()},250):(app.delay.stop("loader"),NProgress.done())}};$(function(){app.setup()}); \ No newline at end of file diff --git a/panel/assets/js/dist/form.min.js b/panel/assets/js/dist/form.min.js new file mode 100644 index 0000000..778ae13 --- /dev/null +++ b/panel/assets/js/dist/form.min.js @@ -0,0 +1 @@ +!function(t){t.fn.date=function(){return this.each(function(){if(t(this).data("pikaday"))return t(this);var e=t(this).attr("type","text"),a=e.next(),n=e.data("format"),r=e.val(),i=r?moment(r).format(n):null;if(e.attr("placeholder",n),e.val(i),e.is("[readonly]"))return!1;e.on("change",function(){var t=e.val();t?a.val(moment(t,n).format("YYYY-MM-DD")):a.val("")});var o=new Pikaday({field:this,firstDay:1,format:n,i18n:e.data("i18n"),onSelect:function(t){a.val(moment(t).format("YYYY-MM-DD"))}});t(this).data("pikaday",o)})}}(jQuery),function(t){t.fn.imagefield=function(){return this.each(function(){var e=t(this);if(e.data("imagefield"))return!0;e.data("imagefield",!0);var a=e.find("select"),n=e.find(".input-preview figure"),r=n.parent("a");a.on("keydown change",function(){var t=a.find("option:selected"),e=t.data("url"),i=t.data("thumb");""===t.val()&&(e="#"),i?n.attr("style","background-image: url("+i+")"):n.attr("style","background-image: none"),r.attr("href",e)}).trigger("change"),e.find(".input-preview").on("click",function(){return"#"==t(this).attr("href")?!1:void 0}),e.find(".input").droppable({hoverClass:"over",accept:t(".sidebar .draggable-file"),drop:function(e,a){t(this).find("select").val(a.draggable.data("helper")).trigger("change")}})})}}(jQuery),function(t){var e=function(e){var a=t(e),n=a.data("style"),r=a.data("api"),i=a.data("sortable"),o="table"==n?a.find(".structure-table tbody"):a.find(".structure-entries");return i===!1?!1:void o.sortable({helper:function(e,a){return a.children().each(function(){t(this).width(t(this).width())}),a.addClass("structure-sortable-helper")},update:function(){var e=[];t.each(t(this).sortable("toArray"),function(t,a){e.push(a.replace("structure-entry-",""))}),t.post(r,{ids:e},function(){app.content.reload()})}})};t.fn.structure=function(){return this.each(function(){if(t(this).data("structure"))return t(this);var a=new e(this);return t(this).data("structure",a),t(this)})}}(jQuery),function(t){t.fn.counter=function(){return this.each(function(){var e=t(this);if(e.data("counter"))return e;var a=e.parent(".field").find(".input"),n=t.trim(a.val()).length,r=a.data("max"),i=a.data("min");a.keyup(function(){n=t.trim(a.val()).length,e.text(n+(r?"/"+r:"")),r&&n>r||i&&i>n?e.addClass("outside-range"):e.removeClass("outside-range")}).trigger("keyup"),e.data("counter",!0)})}}(jQuery),function(t){t.fn.editor=function(){return this.each(function(){if(t(this).data("editor"))return t(this);var e=t(this),a=e.parent().find(".field-buttons");e.autosize(),a.find(".btn").on("click.editorButton",function(a){e.focus();var n=t(this);if(n.data("action"))app.modal.open(n.data("action"),window.location.href);else{var r=e.getSelection(),i=n.data("tpl"),o=n.data("text");r.length>0&&(o=r);var u=i.replace("{text}",o);e.insertAtCursor(u),e.trigger("autosize.resize")}return!1}),a.find("[data-editor-shortcut]").each(function(a,n){var r=t(this).data("editor-shortcut"),i=function(e){return t(n).trigger("click"),!1};e.bind("keydown",r,i),r.match(/meta\+/)&&e.bind("keydown",r.replace("meta+","ctrl+"),i)}),e.data("editor",!0)})}}(jQuery),function(t){t.fn.urlfield=function(){return this.each(function(){var e=t(this);if(!e.data("urlfield")){e.data("urlfield",!0);var a=e.next(".field-icon");a.css({cursor:"pointer","pointer-events":"auto"}),a.on("click",function(){var a=t.trim(e.val());""!==a&&e.is(":valid")?window.open(a):e.focus()})}})}}(jQuery); \ No newline at end of file diff --git a/panel/assets/js/dist/panel.min.js b/panel/assets/js/dist/panel.min.js new file mode 100644 index 0000000..a21b1cc --- /dev/null +++ b/panel/assets/js/dist/panel.min.js @@ -0,0 +1,8 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.moment=e()}(this,function(){"use strict";function t(){return Hn.apply(null,arguments)}function e(t){Hn=t}function n(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,i=[];for(n=0;n0)for(n in Yn)i=Yn[n],o=e[i],"undefined"!=typeof o&&(t[i]=o);return t}function f(e){d(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),Ln===!1&&(Ln=!0,t.updateOffset(this),Ln=!1)}function p(t){return t instanceof f||null!=t&&null!=t._isAMomentObject}function m(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=m(e)),n}function v(t,e,n){var i,o=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),s=0;for(i=0;o>i;i++)(n&&t[i]!==e[i]||!n&&g(t[i])!==g(e[i]))&&s++;return s+r}function y(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function _(t){for(var e,n,i,o,r=0;r0;){if(i=w(o.slice(0,e).join("-")))return i;if(n&&n.length>=e&&v(o,n,!0)>=e-1)break;e--}r++}return null}function w(t){var e=null;if(!jn[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=An._abbr,require("./locale/"+t),x(e)}catch(n){}return jn[t]}function x(t,e){var n;return t&&(n="undefined"==typeof e?D(t):k(t,e),n&&(An=n)),An._abbr}function k(t,e){return null!==e?(e.abbr=t,jn[t]=jn[t]||new y,jn[t].set(e),x(t),jn[t]):(delete jn[t],null)}function D(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return An;if(!n(t)){if(e=w(t))return e;t=[t]}return _(t)}function C(t,e){var n=t.toLowerCase();Wn[n]=Wn[n+"s"]=Wn[e]=t}function T(t){return"string"==typeof t?Wn[t]||Wn[t.toLowerCase()]:void 0}function S(t){var e,n,i={};for(n in t)r(t,n)&&(e=T(n),e&&(i[e]=t[n]));return i}function P(e,n){return function(i){return null!=i?(N(this,e,i),t.updateOffset(this,n),this):M(this,e)}}function M(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function N(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function E(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=T(t),"function"==typeof this[t])return this[t](e);return this}function O(t,e,n){var i=""+Math.abs(t),o=e-i.length,r=t>=0;return(r?n?"+":"":"-")+Math.pow(10,Math.max(0,o)).toString().substr(1)+i}function I(t,e,n,i){var o=i;"string"==typeof i&&(o=function(){return this[i]()}),t&&(qn[t]=o),e&&(qn[e[0]]=function(){return O(o.apply(this,arguments),e[1],e[2])}),n&&(qn[n]=function(){return this.localeData().ordinal(o.apply(this,arguments),t)})}function H(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,n,i=t.match(Fn);for(e=0,n=i.length;n>e;e++)qn[i[e]]?i[e]=qn[i[e]]:i[e]=H(i[e]);return function(o){var r="";for(e=0;n>e;e++)r+=i[e]instanceof Function?i[e].call(o,t):i[e];return r}}function Y(t,e){return t.isValid()?(e=L(e,t.localeData()),zn[e]=zn[e]||A(e),zn[e](t)):t.localeData().invalidDate()}function L(t,e){function n(t){return e.longDateFormat(t)||t}var i=5;for(Rn.lastIndex=0;i>=0&&Rn.test(t);)t=t.replace(Rn,n),Rn.lastIndex=0,i-=1;return t}function j(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function W(t,e,n){oi[t]=j(e)?e:function(t){return t&&n?n:e}}function F(t,e){return r(oi,t)?oi[t](e._strict,e._locale):new RegExp(R(t))}function R(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,i,o){return e||n||i||o}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function z(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(i=function(t,n){n[e]=g(t)}),n=0;ni;i++){if(o=a([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(o,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(o,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(o,"")+"|^"+this.monthsShort(o,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}}function Q(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),U(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function V(e){return null!=e?(Q(this,e),t.updateOffset(this,!0),this):M(this,"Month")}function Z(){return U(this.year(),this.month())}function J(t){var e,n=t._a;return n&&-2===c(t).overflow&&(e=n[ai]<0||n[ai]>11?ai:n[li]<1||n[li]>U(n[si],n[ai])?li:n[ci]<0||n[ci]>24||24===n[ci]&&(0!==n[ui]||0!==n[hi]||0!==n[di])?ci:n[ui]<0||n[ui]>59?ui:n[hi]<0||n[hi]>59?hi:n[di]<0||n[di]>999?di:-1,c(t)._overflowDayOfYear&&(si>e||e>li)&&(e=li),c(t).overflow=e),t}function K(e){t.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function tt(t,e){var n=!0;return s(function(){return n&&(K(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function et(t,e){mi[t]||(K(e),mi[t]=!0)}function nt(t){var e,n,i=t._i,o=gi.exec(i);if(o){for(c(t).iso=!0,e=0,n=vi.length;n>e;e++)if(vi[e][1].exec(i)){t._f=vi[e][0];break}for(e=0,n=yi.length;n>e;e++)if(yi[e][1].exec(i)){t._f+=(o[6]||" ")+yi[e][0];break}i.match(ei)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(e){var n=bi.exec(e._i);return null!==n?void(e._d=new Date(+n[1])):(nt(e),void(e._isValid===!1&&(delete e._isValid,t.createFromInputFallback(e))))}function ot(t,e,n,i,o,r,s){var a=new Date(t,e,n,i,o,r,s);return 1970>t&&a.setFullYear(t),a}function rt(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function st(t){return at(t)?366:365}function at(t){return t%4===0&&t%100!==0||t%400===0}function lt(){return at(this.year())}function ct(t,e,n){var i,o=n-e,r=n-t.day();return r>o&&(r-=7),o-7>r&&(r+=7),i=Mt(t).add(r,"d"),{week:Math.ceil(i.dayOfYear()/7),year:i.year()}}function ut(t){return ct(t,this._week.dow,this._week.doy).week}function ht(){return this._week.dow}function dt(){return this._week.doy}function ft(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function pt(t){var e=ct(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function mt(t,e,n,i,o){var r,s=6+o-i,a=rt(t,0,1+s),l=a.getUTCDay();return o>l&&(l+=7),n=null!=n?1*n:o,r=1+s+7*(e-1)-l+n,{year:r>0?t:t-1,dayOfYear:r>0?r:st(t-1)+r}}function gt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function vt(t,e,n){return null!=t?t:null!=e?e:n}function yt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,i,o,r=[];if(!t._d){for(i=yt(t),t._w&&null==t._a[li]&&null==t._a[ai]&&_t(t),t._dayOfYear&&(o=vt(t._a[si],i[si]),t._dayOfYear>st(o)&&(c(t)._overflowDayOfYear=!0),n=rt(o,0,t._dayOfYear),t._a[ai]=n.getUTCMonth(),t._a[li]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=r[e]=i[e];for(;7>e;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[ci]&&0===t._a[ui]&&0===t._a[hi]&&0===t._a[di]&&(t._nextDay=!0,t._a[ci]=0),t._d=(t._useUTC?rt:ot).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[ci]=24)}}function _t(t){var e,n,i,o,r,s,a;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(r=1,s=4,n=vt(e.GG,t._a[si],ct(Mt(),1,4).year),i=vt(e.W,1),o=vt(e.E,1)):(r=t._locale._week.dow,s=t._locale._week.doy,n=vt(e.gg,t._a[si],ct(Mt(),r,s).year),i=vt(e.w,1),null!=e.d?(o=e.d,r>o&&++i):o=null!=e.e?e.e+r:r),a=mt(n,i,o,s,r),t._a[si]=a.year,t._dayOfYear=a.dayOfYear}function wt(e){if(e._f===t.ISO_8601)return void nt(e);e._a=[],c(e).empty=!0;var n,i,o,r,s,a=""+e._i,l=a.length,u=0;for(o=L(e._f,e._locale).match(Fn)||[],n=0;n0&&c(e).unusedInput.push(s),a=a.slice(a.indexOf(i)+i.length),u+=i.length),qn[r]?(i?c(e).empty=!1:c(e).unusedTokens.push(r),$(r,i,e)):e._strict&&!i&&c(e).unusedTokens.push(r);c(e).charsLeftOver=l-u,a.length>0&&c(e).unusedInput.push(a),c(e).bigHour===!0&&e._a[ci]<=12&&e._a[ci]>0&&(c(e).bigHour=void 0),e._a[ci]=xt(e._locale,e._a[ci],e._meridiem),bt(e),J(e)}function xt(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(i=t.isPM(n),i&&12>e&&(e+=12),i||12!==e||(e=0),e):e}function kt(t){var e,n,i,o,r;if(0===t._f.length)return c(t).invalidFormat=!0,void(t._d=new Date(NaN));for(o=0;or)&&(i=r,n=e));s(t,n||e)}function Dt(t){if(!t._d){var e=S(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function Ct(t){var e=new f(J(Tt(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Tt(t){var e=t._i,o=t._f;return t._locale=t._locale||D(t._l),null===e||void 0===o&&""===e?h({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),p(e)?new f(J(e)):(n(o)?kt(t):o?wt(t):i(e)?t._d=e:St(t),t))}function St(e){var r=e._i;void 0===r?e._d=new Date:i(r)?e._d=new Date(+r):"string"==typeof r?it(e):n(r)?(e._a=o(r.slice(0),function(t){return parseInt(t,10)}),bt(e)):"object"==typeof r?Dt(e):"number"==typeof r?e._d=new Date(r):t.createFromInputFallback(e)}function Pt(t,e,n,i,o){var r={};return"boolean"==typeof n&&(i=n,n=void 0),r._isAMomentObject=!0,r._useUTC=r._isUTC=o,r._l=n,r._i=t,r._f=e,r._strict=i,Ct(r)}function Mt(t,e,n,i){return Pt(t,e,n,i,!1)}function Nt(t,e){var i,o;if(1===e.length&&n(e[0])&&(e=e[0]),!e.length)return Mt();for(i=e[0],o=1;ot&&(t=-t,n="-"),n+O(~~(t/60),2)+e+O(~~t%60,2)})}function Yt(t){var e=(t||"").match(ei)||[],n=e[e.length-1]||[],i=(n+"").match(Di)||["-",0,0],o=+(60*i[1])+g(i[2]);return"+"===i[0]?o:-o}function Lt(e,n){var o,r;return n._isUTC?(o=n.clone(),r=(p(e)||i(e)?+e:+Mt(e))-+o,o._d.setTime(+o._d+r),t.updateOffset(o,!1),o):Mt(e).local()}function jt(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Wt(e,n){var i,o=this._offset||0;return null!=e?("string"==typeof e&&(e=Yt(e)),Math.abs(e)<16&&(e=60*e),!this._isUTC&&n&&(i=jt(this)),this._offset=e,this._isUTC=!0,null!=i&&this.add(i,"m"),o!==e&&(!n||this._changeInProgress?ee(this,Vt(e-o,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,t.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?o:jt(this)}function Ft(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Rt(t){return this.utcOffset(0,t)}function zt(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(jt(this),"m")),this}function qt(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Yt(this._i)),this}function $t(t){return t=t?Mt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function Ut(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Bt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(d(t,this),t=Tt(t),t._a){var e=t._isUTC?a(t._a):Mt(t._a);this._isDSTShifted=this.isValid()&&v(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Xt(){return!this._isUTC}function Gt(){return this._isUTC}function Qt(){return this._isUTC&&0===this._offset}function Vt(t,e){var n,i,o,s=t,a=null;return Ht(t)?s={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(s={},e?s[e]=t:s.milliseconds=t):(a=Ci.exec(t))?(n="-"===a[1]?-1:1,s={y:0,d:g(a[li])*n,h:g(a[ci])*n,m:g(a[ui])*n,s:g(a[hi])*n,ms:g(a[di])*n}):(a=Ti.exec(t))?(n="-"===a[1]?-1:1,s={y:Zt(a[2],n),M:Zt(a[3],n),d:Zt(a[4],n),h:Zt(a[5],n),m:Zt(a[6],n),s:Zt(a[7],n),w:Zt(a[8],n)}):null==s?s={}:"object"==typeof s&&("from"in s||"to"in s)&&(o=Kt(Mt(s.from),Mt(s.to)),s={},s.ms=o.milliseconds,s.M=o.months),i=new It(s),Ht(t)&&r(t,"_locale")&&(i._locale=t._locale),i}function Zt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Jt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function Kt(t,e){var n;return e=Lt(e,t),t.isBefore(e)?n=Jt(t,e):(n=Jt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function te(t,e){return function(n,i){var o,r;return null===i||isNaN(+i)||(et(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),r=n,n=i,i=r),n="string"==typeof n?+n:n,o=Vt(n,i),ee(this,o,t),this}}function ee(e,n,i,o){var r=n._milliseconds,s=n._days,a=n._months;o=null==o?!0:o,r&&e._d.setTime(+e._d+r*i),s&&N(e,"Date",M(e,"Date")+s*i),a&&Q(e,M(e,"Month")+a*i),o&&t.updateOffset(e,s||a)}function ne(t,e){var n=t||Mt(),i=Lt(n,this).startOf("day"),o=this.diff(i,"days",!0),r=-6>o?"sameElse":-1>o?"lastWeek":0>o?"lastDay":1>o?"sameDay":2>o?"nextDay":7>o?"nextWeek":"sameElse";return this.format(e&&e[r]||this.localeData().calendar(r,this,Mt(n)))}function ie(){return new f(this)}function oe(t,e){var n;return e=T("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=p(t)?t:Mt(t),+this>+t):(n=p(t)?+t:+Mt(t),n<+this.clone().startOf(e))}function re(t,e){var n;return e=T("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=p(t)?t:Mt(t),+t>+this):(n=p(t)?+t:+Mt(t),+this.clone().endOf(e)e-r?(n=t.clone().add(o-1,"months"),i=(e-r)/(r-n)):(n=t.clone().add(o+1,"months"),i=(e-r)/(n-r)),-(o+i)}function ue(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function he(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=Mt([2e3,1]).day(e),i="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(i.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Re(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=Ye(t,this.localeData()),this.add(t-e,"d")):e}function ze(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function qe(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function $e(t,e){I(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function Ue(t,e){return e._meridiemParse}function Be(t){return"p"===(t+"").toLowerCase().charAt(0)}function Xe(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Ge(t,e){e[di]=g(1e3*("0."+t))}function Qe(){return this._isUTC?"UTC":""}function Ve(){return this._isUTC?"Coordinated Universal Time":""}function Ze(t){return Mt(1e3*t)}function Je(){return Mt.apply(null,arguments).parseZone()}function Ke(t,e,n){var i=this._calendar[t];return"function"==typeof i?i.call(e,n):i}function tn(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function en(){return this._invalidDate}function nn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function rn(t,e,n,i){var o=this._relativeTime[n];return"function"==typeof o?o(t,e,n,i):o.replace(/%d/i,t)}function sn(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function an(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function ln(t,e,n,i){var o=D(),r=a().set(i,e);return o[n](r,t)}function cn(t,e,n,i,o){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return ln(t,e,n,o);var r,s=[];for(r=0;i>r;r++)s[r]=ln(t,r,n,o);return s}function un(t,e){return cn(t,e,"months",12,"month")}function hn(t,e){return cn(t,e,"monthsShort",12,"month")}function dn(t,e){return cn(t,e,"weekdays",7,"day")}function fn(t,e){return cn(t,e,"weekdaysShort",7,"day")}function pn(t,e){return cn(t,e,"weekdaysMin",7,"day")}function mn(){var t=this._data;return this._milliseconds=Qi(this._milliseconds),this._days=Qi(this._days),this._months=Qi(this._months),t.milliseconds=Qi(t.milliseconds),t.seconds=Qi(t.seconds),t.minutes=Qi(t.minutes),t.hours=Qi(t.hours),t.months=Qi(t.months),t.years=Qi(t.years),this}function gn(t,e,n,i){var o=Vt(e,n);return t._milliseconds+=i*o._milliseconds,t._days+=i*o._days,t._months+=i*o._months,t._bubble()}function vn(t,e){return gn(this,t,e,1)}function yn(t,e){return gn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function _n(){var t,e,n,i,o,r=this._milliseconds,s=this._days,a=this._months,l=this._data;return r>=0&&s>=0&&a>=0||0>=r&&0>=s&&0>=a||(r+=864e5*bn(xn(a)+s),s=0,a=0),l.milliseconds=r%1e3,t=m(r/1e3),l.seconds=t%60,e=m(t/60),l.minutes=e%60,n=m(e/60),l.hours=n%24,s+=m(n/24),o=m(wn(s)),a+=o,s-=bn(xn(o)),i=m(a/12),a%=12,l.days=s,l.months=a,l.years=i,this}function wn(t){return 4800*t/146097}function xn(t){return 146097*t/4800}function kn(t){var e,n,i=this._milliseconds;if(t=T(t),"month"===t||"year"===t)return e=this._days+i/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(xn(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}}function Dn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*g(this._months/12)}function Cn(t){return function(){return this.as(t)}}function Tn(t){return t=T(t),this[t+"s"]()}function Sn(t){return function(){return this._data[t]}}function Pn(){return m(this.days()/7)}function Mn(t,e,n,i,o){return o.relativeTime(e||1,!!n,t,i)}function Nn(t,e,n){var i=Vt(t).abs(),o=ho(i.as("s")),r=ho(i.as("m")),s=ho(i.as("h")),a=ho(i.as("d")),l=ho(i.as("M")),c=ho(i.as("y")),u=o0,u[4]=n,Mn.apply(null,u)}function En(t,e){return void 0===fo[t]?!1:void 0===e?fo[t]:(fo[t]=e,!0)}function On(t){var e=this.localeData(),n=Nn(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function In(){var t,e,n,i=po(this._milliseconds)/1e3,o=po(this._days),r=po(this._months);t=m(i/60),e=m(t/60),i%=60,t%=60,n=m(r/12),r%=12;var s=n,a=r,l=o,c=e,u=t,h=i,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(s?s+"Y":"")+(a?a+"M":"")+(l?l+"D":"")+(c||u||h?"T":"")+(c?c+"H":"")+(u?u+"M":"")+(h?h+"S":""):"P0D"}var Hn,An,Yn=t.momentProperties=[],Ln=!1,jn={},Wn={},Fn=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Rn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,zn={},qn={},$n=/\d/,Un=/\d\d/,Bn=/\d{3}/,Xn=/\d{4}/,Gn=/[+-]?\d{6}/,Qn=/\d\d?/,Vn=/\d{1,3}/,Zn=/\d{1,4}/,Jn=/[+-]?\d{1,6}/,Kn=/\d+/,ti=/[+-]?\d+/,ei=/Z|[+-]\d\d:?\d\d/gi,ni=/[+-]?\d+(\.\d{1,3})?/,ii=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,oi={},ri={},si=0,ai=1,li=2,ci=3,ui=4,hi=5,di=6;I("M",["MM",2],"Mo",function(){return this.month()+1}),I("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),I("MMMM",0,0,function(t){return this.localeData().months(this,t)}),C("month","M"),W("M",Qn),W("MM",Qn,Un),W("MMM",ii),W("MMMM",ii),z(["M","MM"],function(t,e){e[ai]=g(t)-1}),z(["MMM","MMMM"],function(t,e,n,i){var o=n._locale.monthsParse(t,i,n._strict);null!=o?e[ai]=o:c(n).invalidMonth=t});var fi="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),pi="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),mi={};t.suppressDeprecationWarnings=!1;var gi=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,vi=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],yi=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],bi=/^\/?Date\((\-?\d+)/i;t.createFromInputFallback=tt("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),I(0,["YY",2],0,function(){return this.year()%100}),I(0,["YYYY",4],0,"year"),I(0,["YYYYY",5],0,"year"),I(0,["YYYYYY",6,!0],0,"year"),C("year","y"),W("Y",ti),W("YY",Qn,Un),W("YYYY",Zn,Xn),W("YYYYY",Jn,Gn),W("YYYYYY",Jn,Gn),z(["YYYYY","YYYYYY"],si),z("YYYY",function(e,n){n[si]=2===e.length?t.parseTwoDigitYear(e):g(e)}),z("YY",function(e,n){n[si]=t.parseTwoDigitYear(e)}),t.parseTwoDigitYear=function(t){return g(t)+(g(t)>68?1900:2e3)};var _i=P("FullYear",!1);I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),W("w",Qn),W("ww",Qn,Un),W("W",Qn),W("WW",Qn,Un),q(["w","ww","W","WW"],function(t,e,n,i){e[i.substr(0,1)]=g(t)});var wi={dow:0,doy:6};I("DDD",["DDDD",3],"DDDo","dayOfYear"),C("dayOfYear","DDD"),W("DDD",Vn),W("DDDD",Bn),z(["DDD","DDDD"],function(t,e,n){n._dayOfYear=g(t)}),t.ISO_8601=function(){};var xi=tt("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=Mt.apply(null,arguments);return this>t?this:t}),ki=tt("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=Mt.apply(null,arguments);return t>this?this:t});At("Z",":"),At("ZZ",""),W("Z",ei),W("ZZ",ei),z(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=Yt(t)});var Di=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var Ci=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Ti=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Vt.fn=It.prototype;var Si=te(1,"add"),Pi=te(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var Mi=tt("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});I(0,["gg",2],0,function(){return this.weekYear()%100}),I(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Me("gggg","weekYear"),Me("ggggg","weekYear"),Me("GGGG","isoWeekYear"),Me("GGGGG","isoWeekYear"),C("weekYear","gg"),C("isoWeekYear","GG"),W("G",ti),W("g",ti),W("GG",Qn,Un),W("gg",Qn,Un),W("GGGG",Zn,Xn),W("gggg",Zn,Xn),W("GGGGG",Jn,Gn),W("ggggg",Jn,Gn),q(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,i){e[i.substr(0,2)]=g(t)}),q(["gg","GG"],function(e,n,i,o){n[o]=t.parseTwoDigitYear(e)}),I("Q",0,0,"quarter"),C("quarter","Q"),W("Q",$n),z("Q",function(t,e){e[ai]=3*(g(t)-1)}),I("D",["DD",2],"Do","date"),C("date","D"),W("D",Qn),W("DD",Qn,Un),W("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),z(["D","DD"],li),z("Do",function(t,e){e[li]=g(t.match(Qn)[0],10)});var Ni=P("Date",!0);I("d",0,"do","day"),I("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),I("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),I("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),W("d",Qn),W("e",Qn),W("E",Qn),W("dd",ii),W("ddd",ii),W("dddd",ii),q(["dd","ddd","dddd"],function(t,e,n){var i=n._locale.weekdaysParse(t);null!=i?e.d=i:c(n).invalidWeekday=t}),q(["d","e","E"],function(t,e,n,i){e[i]=g(t)});var Ei="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Oi="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ii="Su_Mo_Tu_We_Th_Fr_Sa".split("_");I("H",["HH",2],0,"hour"),I("h",["hh",2],0,function(){return this.hours()%12||12}),$e("a",!0),$e("A",!1),C("hour","h"),W("a",Ue),W("A",Ue),W("H",Qn),W("h",Qn),W("HH",Qn,Un),W("hh",Qn,Un),z(["H","HH"],ci),z(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),z(["h","hh"],function(t,e,n){e[ci]=g(t),c(n).bigHour=!0});var Hi=/[ap]\.?m?\.?/i,Ai=P("Hours",!0);I("m",["mm",2],0,"minute"),C("minute","m"),W("m",Qn),W("mm",Qn,Un),z(["m","mm"],ui);var Yi=P("Minutes",!1);I("s",["ss",2],0,"second"),C("second","s"),W("s",Qn),W("ss",Qn,Un),z(["s","ss"],hi);var Li=P("Seconds",!1);I("S",0,0,function(){return~~(this.millisecond()/100)}),I(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),I(0,["SSS",3],0,"millisecond"),I(0,["SSSS",4],0,function(){return 10*this.millisecond()}),I(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),I(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),I(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),I(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),I(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),C("millisecond","ms"),W("S",Vn,$n),W("SS",Vn,Un),W("SSS",Vn,Bn);var ji;for(ji="SSSS";ji.length<=9;ji+="S")W(ji,Kn);for(ji="S";ji.length<=9;ji+="S")z(ji,Ge);var Wi=P("Milliseconds",!1);I("z",0,0,"zoneAbbr"),I("zz",0,0,"zoneName");var Fi=f.prototype;Fi.add=Si,Fi.calendar=ne,Fi.clone=ie,Fi.diff=le,Fi.endOf=_e,Fi.format=de,Fi.from=fe,Fi.fromNow=pe,Fi.to=me,Fi.toNow=ge,Fi.get=E,Fi.invalidAt=Pe,Fi.isAfter=oe,Fi.isBefore=re,Fi.isBetween=se,Fi.isSame=ae,Fi.isValid=Te,Fi.lang=Mi,Fi.locale=ve,Fi.localeData=ye,Fi.max=ki,Fi.min=xi,Fi.parsingFlags=Se,Fi.set=E,Fi.startOf=be,Fi.subtract=Pi,Fi.toArray=De,Fi.toObject=Ce,Fi.toDate=ke,Fi.toISOString=he,Fi.toJSON=he,Fi.toString=ue,Fi.unix=xe,Fi.valueOf=we,Fi.year=_i,Fi.isLeapYear=lt,Fi.weekYear=Ee,Fi.isoWeekYear=Oe,Fi.quarter=Fi.quarters=Ae,Fi.month=V,Fi.daysInMonth=Z,Fi.week=Fi.weeks=ft,Fi.isoWeek=Fi.isoWeeks=pt,Fi.weeksInYear=He,Fi.isoWeeksInYear=Ie,Fi.date=Ni,Fi.day=Fi.days=Re,Fi.weekday=ze,Fi.isoWeekday=qe,Fi.dayOfYear=gt,Fi.hour=Fi.hours=Ai,Fi.minute=Fi.minutes=Yi,Fi.second=Fi.seconds=Li, +Fi.millisecond=Fi.milliseconds=Wi,Fi.utcOffset=Wt,Fi.utc=Rt,Fi.local=zt,Fi.parseZone=qt,Fi.hasAlignedHourOffset=$t,Fi.isDST=Ut,Fi.isDSTShifted=Bt,Fi.isLocal=Xt,Fi.isUtcOffset=Gt,Fi.isUtc=Qt,Fi.isUTC=Qt,Fi.zoneAbbr=Qe,Fi.zoneName=Ve,Fi.dates=tt("dates accessor is deprecated. Use date instead.",Ni),Fi.months=tt("months accessor is deprecated. Use month instead",V),Fi.years=tt("years accessor is deprecated. Use year instead",_i),Fi.zone=tt("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ft);var Ri=Fi,zi={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},qi={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},$i="Invalid date",Ui="%d",Bi=/\d{1,2}/,Xi={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Gi=y.prototype;Gi._calendar=zi,Gi.calendar=Ke,Gi._longDateFormat=qi,Gi.longDateFormat=tn,Gi._invalidDate=$i,Gi.invalidDate=en,Gi._ordinal=Ui,Gi.ordinal=nn,Gi._ordinalParse=Bi,Gi.preparse=on,Gi.postformat=on,Gi._relativeTime=Xi,Gi.relativeTime=rn,Gi.pastFuture=sn,Gi.set=an,Gi.months=B,Gi._months=fi,Gi.monthsShort=X,Gi._monthsShort=pi,Gi.monthsParse=G,Gi.week=ut,Gi._week=wi,Gi.firstDayOfYear=dt,Gi.firstDayOfWeek=ht,Gi.weekdays=Le,Gi._weekdays=Ei,Gi.weekdaysMin=We,Gi._weekdaysMin=Ii,Gi.weekdaysShort=je,Gi._weekdaysShort=Oi,Gi.weekdaysParse=Fe,Gi.isPM=Be,Gi._meridiemParse=Hi,Gi.meridiem=Xe,x("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===g(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),t.lang=tt("moment.lang is deprecated. Use moment.locale instead.",x),t.langData=tt("moment.langData is deprecated. Use moment.localeData instead.",D);var Qi=Math.abs,Vi=Cn("ms"),Zi=Cn("s"),Ji=Cn("m"),Ki=Cn("h"),to=Cn("d"),eo=Cn("w"),no=Cn("M"),io=Cn("y"),oo=Sn("milliseconds"),ro=Sn("seconds"),so=Sn("minutes"),ao=Sn("hours"),lo=Sn("days"),co=Sn("months"),uo=Sn("years"),ho=Math.round,fo={s:45,m:45,h:22,d:26,M:11},po=Math.abs,mo=It.prototype;mo.abs=mn,mo.add=vn,mo.subtract=yn,mo.as=kn,mo.asMilliseconds=Vi,mo.asSeconds=Zi,mo.asMinutes=Ji,mo.asHours=Ki,mo.asDays=to,mo.asWeeks=eo,mo.asMonths=no,mo.asYears=io,mo.valueOf=Dn,mo._bubble=_n,mo.get=Tn,mo.milliseconds=oo,mo.seconds=ro,mo.minutes=so,mo.hours=ao,mo.days=lo,mo.weeks=Pn,mo.months=co,mo.years=uo,mo.humanize=On,mo.toISOString=In,mo.toString=In,mo.toJSON=In,mo.locale=ve,mo.localeData=ye,mo.toIsoString=tt("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",In),mo.lang=Mi,I("X",0,0,"unix"),I("x",0,0,"valueOf"),W("x",ti),W("X",ni),z("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),z("x",function(t,e,n){n._d=new Date(g(t))}),t.version="2.10.6",e(Mt),t.fn=Ri,t.min=Et,t.max=Ot,t.utc=a,t.unix=Ze,t.months=un,t.isDate=i,t.locale=x,t.invalid=h,t.duration=Vt,t.isMoment=p,t.weekdays=dn,t.parseZone=Je,t.localeData=D,t.isDuration=Ht,t.monthsShort=hn,t.weekdaysMin=pn,t.defineLocale=k,t.weekdaysShort=fn,t.normalizeUnits=T,t.relativeTimeThreshold=En;var go=t;return go}),function(t,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():t.NProgress=e()}(this,function(){function t(t,e,n){return e>t?e:t>n?n:t}function e(t){return 100*(-1+t)}function n(t,n,i){var o;return o="translate3d"===c.positionUsing?{transform:"translate3d("+e(t)+"%,0,0)"}:"translate"===c.positionUsing?{transform:"translate("+e(t)+"%,0)"}:{"margin-left":e(t)+"%"},o.transition="all "+n+"ms "+i,o}function i(t,e){var n="string"==typeof t?t:s(t);return n.indexOf(" "+e+" ")>=0}function o(t,e){var n=s(t),o=n+e;i(n,e)||(t.className=o.substring(1))}function r(t,e){var n,o=s(t);i(t,e)&&(n=o.replace(" "+e+" "," "),t.className=n.substring(1,n.length-1))}function s(t){return(" "+(t.className||"")+" ").replace(/\s+/gi," ")}function a(t){t&&t.parentNode&&t.parentNode.removeChild(t)}var l={};l.version="0.2.0";var c=l.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
    '};l.configure=function(t){var e,n;for(e in t)n=t[e],void 0!==n&&t.hasOwnProperty(e)&&(c[e]=n);return this},l.status=null,l.set=function(e){var i=l.isStarted();e=t(e,c.minimum,1),l.status=1===e?null:e;var o=l.render(!i),r=o.querySelector(c.barSelector),s=c.speed,a=c.easing;return o.offsetWidth,u(function(t){""===c.positionUsing&&(c.positionUsing=l.getPositioningCSS()),h(r,n(e,s,a)),1===e?(h(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout(function(){h(o,{transition:"all "+s+"ms linear",opacity:0}),setTimeout(function(){l.remove(),t()},s)},s)):setTimeout(t,s)}),this},l.isStarted=function(){return"number"==typeof l.status},l.start=function(){l.status||l.set(0);var t=function(){setTimeout(function(){l.status&&(l.trickle(),t())},c.trickleSpeed)};return c.trickle&&t(),this},l.done=function(t){return t||l.status?l.inc(.3+.5*Math.random()).set(1):this},l.inc=function(e){var n=l.status;return n?("number"!=typeof e&&(e=(1-n)*t(Math.random()*n,.1,.95)),n=t(n+e,0,.994),l.set(n)):l.start()},l.trickle=function(){return l.inc(Math.random()*c.trickleRate)},function(){var t=0,e=0;l.promise=function(n){return n&&"resolved"!==n.state()?(0===e&&l.start(),t++,e++,n.always(function(){e--,0===e?(t=0,l.done()):l.set((t-e)/t)}),this):this}}(),l.render=function(t){if(l.isRendered())return document.getElementById("nprogress");o(document.documentElement,"nprogress-busy");var n=document.createElement("div");n.id="nprogress",n.innerHTML=c.template;var i,r=n.querySelector(c.barSelector),s=t?"-100":e(l.status||0),u=document.querySelector(c.parent);return h(r,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),c.showSpinner||(i=n.querySelector(c.spinnerSelector),i&&a(i)),u!=document.body&&o(u,"nprogress-custom-parent"),u.appendChild(n),n},l.remove=function(){r(document.documentElement,"nprogress-busy"),r(document.querySelector(c.parent),"nprogress-custom-parent");var t=document.getElementById("nprogress");t&&a(t)},l.isRendered=function(){return!!document.getElementById("nprogress")},l.getPositioningCSS=function(){var t=document.body.style,e="WebkitTransform"in t?"Webkit":"MozTransform"in t?"Moz":"msTransform"in t?"ms":"OTransform"in t?"O":"";return e+"Perspective"in t?"translate3d":e+"Transform"in t?"translate":"margin"};var u=function(){function t(){var n=e.shift();n&&n(t)}var e=[];return function(n){e.push(n),1==e.length&&t()}}(),h=function(){function t(t){return t.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(t,e){return e.toUpperCase()})}function e(t){var e=document.body.style;if(t in e)return t;for(var n,i=o.length,r=t.charAt(0).toUpperCase()+t.slice(1);i--;)if(n=o[i]+r,n in e)return n;return t}function n(n){return n=t(n),r[n]||(r[n]=e(n))}function i(t,e,i){e=n(e),t.style[e]=i}var o=["Webkit","O","Moz","ms"],r={};return function(t,e){var n,o,r=arguments;if(2==r.length)for(n in e)o=e[n],void 0!==o&&e.hasOwnProperty(n)&&i(t,n,o);else i(t,r[1],r[2])}}();return l}),function(t,e){"use strict";var n;if("object"==typeof exports){try{n=require("moment")}catch(i){}module.exports=e(n)}else"function"==typeof define&&define.amd?define(function(t){var i="moment";try{n=t(i)}catch(o){}return e(n)}):t.Pikaday=e(t.moment)}(this,function(t){"use strict";var e="function"==typeof t,n=!!window.addEventListener,i=window.document,o=window.setTimeout,r=function(t,e,i,o){n?t.addEventListener(e,i,!!o):t.attachEvent("on"+e,i)},s=function(t,e,i,o){n?t.removeEventListener(e,i,!!o):t.detachEvent("on"+e,i)},a=function(t,e,n){var o;i.createEvent?(o=i.createEvent("HTMLEvents"),o.initEvent(e,!0,!1),o=b(o,n),t.dispatchEvent(o)):i.createEventObject&&(o=i.createEventObject(),o=b(o,n),t.fireEvent("on"+e,o))},l=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")},c=function(t,e){return-1!==(" "+t.className+" ").indexOf(" "+e+" ")},u=function(t,e){c(t,e)||(t.className=""===t.className?e:t.className+" "+e)},h=function(t,e){t.className=l((" "+t.className+" ").replace(" "+e+" "," "))},d=function(t){return/Array/.test(Object.prototype.toString.call(t))},f=function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())},p=function(t){var e=t.getDay();return 0===e||6===e},m=function(t){return t%4===0&&t%100!==0||t%400===0},g=function(t,e){return[31,m(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]},v=function(t){f(t)&&t.setHours(0,0,0,0)},y=function(t,e){return t.getTime()===e.getTime()},b=function(t,e,n){var i,o;for(i in e)o=void 0!==t[i],o&&"object"==typeof e[i]&&null!==e[i]&&void 0===e[i].nodeName?f(e[i])?n&&(t[i]=new Date(e[i].getTime())):d(e[i])?n&&(t[i]=e[i].slice(0)):t[i]=b({},e[i],n):!n&&o||(t[i]=e[i]);return t},_=function(t){return t.month<0&&(t.year-=Math.ceil(Math.abs(t.month)/12),t.month+=12),t.month>11&&(t.year+=Math.floor(Math.abs(t.month)/12),t.month-=12),t},w={field:null,bound:void 0,position:"bottom left",reposition:!0,format:"YYYY-MM-DD",defaultDate:null,setDefaultDate:!1,firstDay:0,minDate:null,maxDate:null,yearRange:10,showWeekNumber:!1,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,yearSuffix:"",showMonthAfterYear:!1,numberOfMonths:1,mainCalendar:"left",container:void 0,i18n:{previousMonth:"Previous Month",nextMonth:"Next Month",months:["January","February","March","April","May","June","July","August","September","October","November","December"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},theme:null,onSelect:null,onOpen:null,onClose:null,onDraw:null},x=function(t,e,n){for(e+=t.firstDay;e>=7;)e-=7;return n?t.i18n.weekdaysShort[e]:t.i18n.weekdays[e]},k=function(t){if(t.isEmpty)return'';var e=[];return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&e.push("is-selected"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"},D=function(t,e,n){var i=new Date(n,0,1),o=Math.ceil(((new Date(n,e,t)-i)/864e5+i.getDay()+1)/7);return''+o+""},C=function(t,e){return""+(e?t.reverse():t).join("")+""},T=function(t){return""+t.join("")+""},S=function(t){var e,n=[];for(t.showWeekNumber&&n.push(""),e=0;7>e;e++)n.push(''+x(t,e,!0)+"");return""+(t.isRTL?n.reverse():n).join("")+""},P=function(t,e,n,i,o){var r,s,a,l,c,u=t._o,h=n===u.minYear,f=n===u.maxYear,p='
    ',m=!0,g=!0;for(a=[],r=0;12>r;r++)a.push('");for(l='
    '+u.i18n.months[i]+'
    ",d(u.yearRange)?(r=u.yearRange[0],s=u.yearRange[1]+1):(r=n-u.yearRange,s=1+n+u.yearRange),a=[];s>r&&r<=u.maxYear;r++)r>=u.minYear&&a.push('");return c='
    '+n+u.yearSuffix+'
    ",p+=u.showMonthAfterYear?c+l:l+c,h&&(0===i||u.minMonth>=i)&&(m=!1),f&&(11===i||u.maxMonth<=i)&&(g=!1),0===e&&(p+='"),e===t._o.numberOfMonths-1&&(p+='"),p+="
    "},M=function(t,e){return''+S(t)+T(e)+"
    "},N=function(s){var a=this,l=a.config(s);a._onMouseDown=function(t){if(a._v){t=t||window.event;var e=t.target||t.srcElement;if(e)if(c(e.parentNode,"is-disabled")||(c(e,"pika-button")&&!c(e,"is-empty")?(a.setDate(new Date(e.getAttribute("data-pika-year"),e.getAttribute("data-pika-month"),e.getAttribute("data-pika-day"))),l.bound&&o(function(){a.hide(),l.field&&l.field.blur()},100)):c(e,"pika-prev")?a.prevMonth():c(e,"pika-next")&&a.nextMonth()),c(e,"pika-select"))a._c=!0;else{if(!t.preventDefault)return t.returnValue=!1,!1;t.preventDefault()}}},a._onChange=function(t){t=t||window.event;var e=t.target||t.srcElement;e&&(c(e,"pika-select-month")?a.gotoMonth(e.value):c(e,"pika-select-year")&&a.gotoYear(e.value))},a._onInputChange=function(n){var i;n.firedBy!==a&&(e?(i=t(l.field.value,l.format),i=i&&i.isValid()?i.toDate():null):i=new Date(Date.parse(l.field.value)),f(i)&&a.setDate(i),a._v||a.show())},a._onInputFocus=function(){a.show()},a._onInputClick=function(){a.show()},a._onInputBlur=function(){var t=i.activeElement;do if(c(t,"pika-single"))return;while(t=t.parentNode);a._c||(a._b=o(function(){a.hide()},50)),a._c=!1},a._onClick=function(t){t=t||window.event;var e=t.target||t.srcElement,i=e;if(e){!n&&c(e,"pika-select")&&(e.onchange||(e.setAttribute("onchange","return;"),r(e,"change",a._onChange)));do if(c(i,"pika-single")||i===l.trigger)return;while(i=i.parentNode);a._v&&e!==l.trigger&&i!==l.trigger&&a.hide()}},a.el=i.createElement("div"),a.el.className="pika-single"+(l.isRTL?" is-rtl":"")+(l.theme?" "+l.theme:""),r(a.el,"mousedown",a._onMouseDown,!0),r(a.el,"touchend",a._onMouseDown,!0),r(a.el,"change",a._onChange),l.field&&(l.container?l.container.appendChild(a.el):l.bound?i.body.appendChild(a.el):l.field.parentNode.insertBefore(a.el,l.field.nextSibling),r(l.field,"change",a._onInputChange),l.defaultDate||(e&&l.field.value?l.defaultDate=t(l.field.value,l.format).toDate():l.defaultDate=new Date(Date.parse(l.field.value)),l.setDefaultDate=!0));var u=l.defaultDate;f(u)?l.setDefaultDate?a.setDate(u,!0):a.gotoDate(u):a.gotoDate(new Date),l.bound?(this.hide(),a.el.className+=" is-bound",r(l.trigger,"click",a._onInputClick),r(l.trigger,"focus",a._onInputFocus),r(l.trigger,"blur",a._onInputBlur)):this.show()};return N.prototype={config:function(t){this._o||(this._o=b({},w,!0));var e=b(this._o,t,!0);e.isRTL=!!e.isRTL,e.field=e.field&&e.field.nodeName?e.field:null,e.theme="string"==typeof e.theme&&e.theme?e.theme:null,e.bound=!!(void 0!==e.bound?e.field&&e.bound:e.field),e.trigger=e.trigger&&e.trigger.nodeName?e.trigger:e.field,e.disableWeekends=!!e.disableWeekends,e.disableDayFn="function"==typeof e.disableDayFn?e.disableDayFn:null;var n=parseInt(e.numberOfMonths,10)||1;if(e.numberOfMonths=n>4?4:n,f(e.minDate)||(e.minDate=!1),f(e.maxDate)||(e.maxDate=!1),e.minDate&&e.maxDate&&e.maxDate100&&(e.yearRange=100);return e},toString:function(n){return f(this._d)?e?t(this._d).format(n||this._o.format):this._d.toDateString():""},getMoment:function(){return e?t(this._d):null},setMoment:function(n,i){e&&t.isMoment(n)&&this.setDate(n.toDate(),i)},getDate:function(){return f(this._d)?new Date(this._d.getTime()):null},setDate:function(t,e){if(!t)return this._d=null,this._o.field&&(this._o.field.value="",a(this._o.field,"change",{firedBy:this})),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),f(t)){var n=this._o.minDate,i=this._o.maxDate;f(n)&&n>t?t=n:f(i)&&t>i&&(t=i),this._d=new Date(t.getTime()),v(this._d),this.gotoDate(this._d),this._o.field&&(this._o.field.value=this.toString(),a(this._o.field,"change",{firedBy:this})),e||"function"!=typeof this._o.onSelect||this._o.onSelect.call(this,this.getDate())}},gotoDate:function(t){var e=!0;if(f(t)){if(this.calendars){var n=new Date(this.calendars[0].year,this.calendars[0].month,1),i=new Date(this.calendars[this.calendars.length-1].year,this.calendars[this.calendars.length-1].month,1),o=t.getTime();i.setMonth(i.getMonth()+1),i.setDate(i.getDate()-1),e=o=i&&(this._y=i,!isNaN(s)&&this._m>s&&(this._m=s));for(var l=0;l'+P(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year)+this.render(this.calendars[l].year,this.calendars[l].month)+"
    ";if(this.el.innerHTML=a,e.bound&&"hidden"!==e.field.type&&o(function(){e.trigger.focus()},1),"function"==typeof this._o.onDraw){var c=this;o(function(){c._o.onDraw.call(c)},0)}}},adjustPosition:function(){var t,e,n,o,r,s,a,l,c,u;if(!this._o.container){if(this.el.style.position="absolute",t=this._o.trigger,e=t,n=this.el.offsetWidth,o=this.el.offsetHeight,r=window.innerWidth||i.documentElement.clientWidth,s=window.innerHeight||i.documentElement.clientHeight,a=window.pageYOffset||i.body.scrollTop||i.documentElement.scrollTop,"function"==typeof t.getBoundingClientRect)u=t.getBoundingClientRect(),l=u.left+window.pageXOffset,c=u.bottom+window.pageYOffset;else for(l=e.offsetLeft,c=e.offsetTop+e.offsetHeight;e=e.offsetParent;)l+=e.offsetLeft,c+=e.offsetTop;(this._o.reposition&&l+n>r||this._o.position.indexOf("right")>-1&&l-n+t.offsetWidth>0)&&(l=l-n+t.offsetWidth),(this._o.reposition&&c+o>s+a||this._o.position.indexOf("top")>-1&&c-o-t.offsetHeight>0)&&(c=c-o-t.offsetHeight),this.el.style.left=l+"px",this.el.style.top=c+"px"}},render:function(t,e){var n=this._o,i=new Date,o=g(t,e),r=new Date(t,e,1).getDay(),s=[],a=[];v(i),n.firstDay>0&&(r-=n.firstDay,0>r&&(r+=7));for(var l=o+r,c=l;c>7;)c-=7;l+=7-c;for(var u=0,h=0;l>u;u++){var d=new Date(t,e,1+(u-r)),m=f(this._d)?y(d,this._d):!1,b=y(d,i),_=r>u||u>=o+r,w=n.startRange&&y(n.startRange,d),x=n.endRange&&y(n.endRange,d),T=n.startRange&&n.endRange&&n.startRangen.maxDate||n.disableWeekends&&p(d)||n.disableDayFn&&n.disableDayFn(d),P={day:1+(u-r),month:e,year:t,isSelected:m,isToday:b,isDisabled:S,isEmpty:_,isStartRange:w,isEndRange:x,isInRange:T};a.push(k(P)),7===++h&&(n.showWeekNumber&&a.unshift(D(u-r,e,t)),s.push(C(a,n.isRTL)),a=[],h=0)}return M(n,s)},isVisible:function(){return this._v},show:function(){this._v||(h(this.el,"is-hidden"),this._v=!0,this.draw(),this._o.bound&&(r(i,"click",this._onClick),this.adjustPosition()),"function"==typeof this._o.onOpen&&this._o.onOpen.call(this))},hide:function(){var t=this._v;t!==!1&&(this._o.bound&&s(i,"click",this._onClick),this.el.style.position="static",this.el.style.left="auto",this.el.style.top="auto",u(this.el,"is-hidden"),this._v=!1,void 0!==t&&"function"==typeof this._o.onClose&&this._o.onClose.call(this))},destroy:function(){this.hide(),s(this.el,"mousedown",this._onMouseDown,!0),s(this.el,"touchend",this._onMouseDown,!0),s(this.el,"change",this._onChange),this._o.field&&(s(this._o.field,"change",this._onInputChange),this._o.bound&&(s(this._o.trigger,"click",this._onInputClick),s(this._o.trigger,"focus",this._onInputFocus),s(this._o.trigger,"blur",this._onInputBlur))),this.el.parentNode&&this.el.parentNode.removeChild(this.el)}},N}),!function(t,e){"object"==typeof module&&"object"==typeof module.exports?module.exports=t.document?e(t,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return e(t)}:e(t)}("undefined"!=typeof window?window:this,function(t,e){function n(t){var e="length"in t&&t.length,n=K.type(t);return"function"===n||K.isWindow(t)?!1:1===t.nodeType&&e?!0:"array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t}function i(t,e,n){if(K.isFunction(e))return K.grep(t,function(t,i){return!!e.call(t,i,t)!==n});if(e.nodeType)return K.grep(t,function(t){return t===e!==n});if("string"==typeof e){if(at.test(e))return K.filter(e,t,n);e=K.filter(e,t)}return K.grep(t,function(t){return B.call(e,t)>=0!==n})}function o(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}function r(t){var e=pt[t]={};return K.each(t.match(ft)||[],function(t,n){e[n]=!0}),e}function s(){Z.removeEventListener("DOMContentLoaded",s,!1),t.removeEventListener("load",s,!1),K.ready()}function a(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=K.expando+a.uid++}function l(t,e,n){var i;if(void 0===n&&1===t.nodeType)if(i="data-"+e.replace(_t,"-$1").toLowerCase(),n=t.getAttribute(i),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:bt.test(n)?K.parseJSON(n):n}catch(o){}yt.set(t,e,n)}else n=void 0;return n}function c(){return!0}function u(){return!1}function h(){try{return Z.activeElement}catch(t){}}function d(t,e){return K.nodeName(t,"table")&&K.nodeName(11!==e.nodeType?e:e.firstChild,"tr")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}function f(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function p(t){var e=Yt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function m(t,e){for(var n=0,i=t.length;i>n;n++)vt.set(t[n],"globalEval",!e||vt.get(e[n],"globalEval"))}function g(t,e){var n,i,o,r,s,a,l,c;if(1===e.nodeType){if(vt.hasData(t)&&(r=vt.access(t),s=vt.set(e,r),c=r.events)){delete s.handle,s.events={};for(o in c)for(n=0,i=c[o].length;i>n;n++)K.event.add(e,o,c[o][n])}yt.hasData(t)&&(a=yt.access(t),l=K.extend({},a),yt.set(e,l))}}function v(t,e){var n=t.getElementsByTagName?t.getElementsByTagName(e||"*"):t.querySelectorAll?t.querySelectorAll(e||"*"):[];return void 0===e||e&&K.nodeName(t,e)?K.merge([t],n):n}function y(t,e){var n=e.nodeName.toLowerCase();"input"===n&&Dt.test(t.type)?e.checked=t.checked:("input"===n||"textarea"===n)&&(e.defaultValue=t.defaultValue)}function b(e,n){var i,o=K(n.createElement(e)).appendTo(n.body),r=t.getDefaultComputedStyle&&(i=t.getDefaultComputedStyle(o[0]))?i.display:K.css(o[0],"display");return o.detach(),r}function _(t){var e=Z,n=Ft[t];return n||(n=b(t,e),"none"!==n&&n||(Wt=(Wt||K("