diff --git a/app/config.ini b/app/config.ini index 9ab16fb..b3e1e44 100644 --- a/app/config.ini +++ b/app/config.ini @@ -1,25 +1,32 @@ [globals] -; prefix all your db table names to avoid conflict -; with other apps that are using the same database -db_table_prefix = fblg_ - -; use 'md5' for PHP < 5.3.7 and 'bcrypt' for best security -password_hash_engine = md5 -password_md5_salt = jK$N!Lx5 - -; 'summernote' -text_editor = sommernote - ;########################## -; additional system config - +; general settings AUTOLOAD = app/;app/inc/ UI = ui/ BACKEND_UI = app/ui/ UPLOADS = res/ +LOCALES = app/dict/ +# overwrite auto-detection of language +;LANGUAGE=de-DE +DEV = true DEBUG = 2 CACHE = false TZ = Europe/Berlin -; CORTEX.smartLoading = false \ No newline at end of file + +ONERROR = \Error->render + +;########################## +; additional app config + +; prefix all your db table names to avoid conflict +; with other apps that are using the same database +db_table_prefix = fblg_ + +; use 'md5' for PHP < 5.3.7 and 'bcrypt' for best security +password_hash_engine = md5 +password_md5_salt = jK$N!Lx5 + +; 'summernote' +text_editor = sommernote \ No newline at end of file diff --git a/app/controller/auth.php b/app/controller/auth.php index a9f7b65..3979949 100644 --- a/app/controller/auth.php +++ b/app/controller/auth.php @@ -8,6 +8,13 @@ class Auth extends Base { protected $response; + /** + * init the View + */ + public function beforeroute() { + $this->response = new \View\Backend(); + } + /** * check login state * @return bool @@ -51,7 +58,7 @@ public function login($f3,$params) { else $f3->reroute('/admin'); } } - \FlashMessage::instance()->addMessage('Wrong Username/Password', 'danger'); + \Flash::instance()->addMessage('Wrong Username/Password', 'danger'); } $this->response->setTemplate('templates/login.html'); } diff --git a/app/controller/backend.php b/app/controller/backend.php index fde9fbb..d40b797 100644 --- a/app/controller/backend.php +++ b/app/controller/backend.php @@ -5,6 +5,25 @@ class Backend extends Base { + /** @var \Controller\Base */ + protected $module; + + /** + * init the backend view, so the module controller can care about it + */ + public function beforeroute() { + $module_name = \Base::instance()->get('PARAMS.module'); + $this->response = new \View\Backend(); + $this->response->data['LAYOUT'] = $module_name.'_layout.html'; + $this->module = $this->loadModule($module_name); + $this->module->setView($this->response); + } + + /** + * load module controller class + * @param $name + * @return bool + */ protected function loadModule($name) { $class = '\Controller\\'.ucfirst($name); if(!class_exists($class)) { @@ -16,26 +35,20 @@ protected function loadModule($name) { } /** - * create a response that displays a list of module records + * pass method calls to module + * @param $name + * @param $args + * @return mixed */ - public function getList($f3,$params) { - $module = $this->loadModule($params['module']); - $module->setView($this->response); - $module->getList($f3,$params); - $this->response->data['SUBPART'] = $params['module'].'_list.html'; - $this->response->data['LAYOUT'] = $params['module'].'_layout.html'; + public function __call($name,$args) { + return call_user_func_array(array($this->module,$name),$args); } /** - * return an create/edit form for a given module + * give the module control about the view */ - public function getSingle($f3,$params) { - $module = $this->loadModule($params['module']); - $module->setView($this->response); - $module->getSingle($f3, $params); - $this->response->data['SUBPART'] = $params['module'].'_edit.html'; - $this->response->data['LAYOUT'] = $params['module'].'_layout.html'; + public function afterroute() { + $this->module->afterroute(); } - } \ No newline at end of file diff --git a/app/controller/base.php b/app/controller/base.php index 5845fd2..cfac070 100644 --- a/app/controller/base.php +++ b/app/controller/base.php @@ -19,12 +19,15 @@ public function setView(\View\Base $view) { * init the View */ public function beforeroute() { - $this->response = \View\Backend::instance(); + $this->response = new \View\Frontend(); } /** - * kick start the View, which finally creates the response - * based on our previously set content data + * kick start the View, which creates the response + * based on our previously set content data. + * finally echo the response or overwrite this method + * and do something else with it. + * @return string */ public function afterroute() { if (!$this->response) diff --git a/app/controller/comment.php b/app/controller/comment.php index 17d2283..60ed93a 100644 --- a/app/controller/comment.php +++ b/app/controller/comment.php @@ -4,50 +4,66 @@ class Comment extends Resource { - public function __construct() - { + public function __construct() { $mapper = new \Model\Comment(); parent::__construct($mapper); } - public function approve($f3,$params) - { + public function beforeroute() { + $this->response = new \View\Backend(); + $this->response->data['LAYOUT'] = 'comment_layout.html'; + } + + /** + * @param \Base $f3 + * @param $params + */ + public function approve(\Base $f3, $params) { if($this->resource->updateProperty(array('_id = ?', $params['id']),'approved',1)) { - \FlashMessage::instance()->addMessage('Comment approved', 'success'); + \Flash::instance()->addMessage('Comment approved', 'success'); } else { - \FlashMessage::instance()->addMessage('Unknown Comment ID', 'danger'); + \Flash::instance()->addMessage('Unknown Comment ID', 'danger'); } $f3->reroute($f3->get('SESSION.LastPageURL')); } - public function reject($f3,$params) - { + /** + * @param \Base $f3 + * @param $params + */ + public function reject(\Base $f3, $params) { if ($this->resource->updateProperty(array('_id = ?', $params['id']), 'approved', 2)) { - \FlashMessage::instance()->addMessage('Comment rejected', 'success'); + \Flash::instance()->addMessage('Comment rejected', 'success'); } else { - \FlashMessage::instance()->addMessage('Unknown Comment ID', 'danger'); + \Flash::instance()->addMessage('Unknown Comment ID', 'danger'); } $f3->reroute($f3->get('SESSION.LastPageURL')); } - public function getSingle($f3,$params) - { + /** + * @param \Base $f3 + * @param array $params + * @return bool + */ + public function getSingle(\Base $f3,$params) { + $this->response->data['SUBPART'] = 'comment_edit.html'; + if (isset($params['id'])) { - $this->response->data['content'] = $this->resource->load(array('_id = ?',$params['id'])); + $this->response->data['comment'] = $this->resource->load(array('_id = ?',$params['id'])); if(!$this->resource->dry()) return true; } - \FlashMessage::instance()->addMessage('Unknown Comment ID', 'danger'); + \Flash::instance()->addMessage('Unknown Comment ID', 'danger'); $f3->reroute($f3->get('SESSION.LastPageURL')); } /** * display list of comments + * @param \Base $f3 + * @param array $params */ - public function getList($f3, $params) - { + public function getList(\Base $f3, $params) { $this->response->data['SUBPART'] = 'comment_list.html'; - $this->response->data['LAYOUT'] = 'comment_layout.html'; $filter = array('approved = ?',0); // new if (isset($params['viewtype'])) { if ($params['viewtype'] == 'published') @@ -61,8 +77,8 @@ public function getList($f3, $params) } $page = \Pagination::findCurrentPage(); - $limit = 3; - $this->response->data['content'] = + $limit = 10; + $this->response->data['comments'] = $this->resource->paginate($page-1,$limit,$filter, array('order' => 'datetime desc')); } diff --git a/app/controller/dashboard.php b/app/controller/dashboard.php index fde9c71..fd57f50 100644 --- a/app/controller/dashboard.php +++ b/app/controller/dashboard.php @@ -8,6 +8,13 @@ class Dashboard extends Base { protected $response; + /** + * init the View + */ + public function beforeroute() { + $this->response = new \View\Backend(); + } + /** * fetch data for an overview page */ diff --git a/app/controller/post.php b/app/controller/post.php index e6cd368..fa5c849 100644 --- a/app/controller/post.php +++ b/app/controller/post.php @@ -7,11 +7,10 @@ compliance with the license. Any of the license terms and conditions can be waived if you get permission from the copyright holder. - Copyright (c) 2013 ~ ikkez + Copyright (c) 2015 ~ ikkez Christian Knuth - @version 0.1.0 - @date: 03.02.14 + @version 0.2.0 **/ namespace Controller; @@ -19,26 +18,31 @@ class Post extends Resource { - public function __construct() - { - $mapper = new \Model\Post(); - parent::__construct($mapper); + public function __construct() { + parent::__construct(new \Model\Post()); + // setup delete cascade + $this->resource->onerase(function($self){ + // erase comment references + $comments = new \Model\Comment(); + $comments->erase(array('post = ?',$self->_id)); + return true; + }); } /** * display a list of post entries + * @param \Base $f3 + * @param array $params */ - public function getList($f3, $params) - { + public function getList(\Base $f3, $params) { $this->response->data['SUBPART'] = 'post_list.html'; $page = \Pagination::findCurrentPage(); if ($this->response instanceof \View\Backend) { // backend view - $records = $this->resource->paginate($page-1,25,null, + $records = $this->resource->paginate($page-1,5,null, array('order'=>'publish_date desc')); } else { // frontend view - $tags = new Tag(); $f3->set('tag_cloud',$tags->tagCloud()); @@ -51,30 +55,32 @@ public function getList($f3, $params) $this->response->data['content'] = $records; } + /** + * display a list of post entries by a specific tag they are attached to + * @param \Base $f3 + * @param $params + */ + public function getListByTag(\Base $f3, $params) { + $this->response->data['headline'] = 'All Post by Tag: '.$params['slug']; + // set the tag condition + $this->resource->has('tags',array('slug = ?',$params['slug'])); + $this->getList($f3,$params); + } + /** * display a single post + * @param \Base $f3 + * @param array $params */ - public function getSingle($f3, $params) - { - $this->response->data = array('SUBPART' => 'post_single.html'); + public function getSingle(\Base $f3, $params) { + $this->response->data['SUBPART'] = 'post_single.html'; $addQuery = ''; - // only show published posts, except in backend - if (!$this->response instanceof \View\Backend) - $addQuery = ' and publish_date <= ? and published = ?'; - else { - $ui = $f3->get('BACKEND_UI'); - if ($f3->get('text_editor') == 'sommernote') { - $f3->set('ASSETS.JS.summernote', $ui.'js/summernote.js'); - $f3->set('ASSETS.CSS.fontawesome', - '//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css'); - $f3->set('ASSETS.CSS.summernote', $ui.'css/summernote.css'); - $f3->set('ASSETS.CSS.summernote-bs3', $ui.'css/summernote-bs3.css'); - } - $f3->set('ASSETS.JS.jqueryui', $ui.'js/vendor/jquery.ui.widget.js'); - $f3->set('ASSETS.JS.jq-iframe-transport', $ui.'js/jquery.iframe-transport.js'); - $f3->set('ASSETS.JS.fileupload', $ui.'js/jquery.fileupload.js'); - $f3->set('ASSETS.CSS.fileupload', $ui.'css/jquery.fileupload.css'); + if ($this->response instanceof \View\Backend) + $this->initBackend(); + else { + // only show published posts on the frontend + $addQuery = ' and publish_date <= ? and published = ?'; } // show only approved comments in the next query @@ -88,28 +94,67 @@ public function getSingle($f3, $params) elseif (isset($params['slug'])) { $this->resource->load(array('slug = ?'.$addQuery, $params['slug'], date('Y-m-d'), true)); } - $this->response->data['content'] = $this->resource; - if ($this->resource->dry() && !$this->response instanceof \View\Backend) - $f3->error(404, 'Post not found'); + if ($this->response instanceof \View\Backend) { + $data=$this->resource->cast(null, 0); + if (!$this->resource->dry()) { + $tags=$this->resource->tags; + if ($tags) + $tags=implode(',', $tags->getAll('title')); + $data['tags']=$tags; + } + $data['publish_date'] = $f3->format('{0,date}', !empty($data['publish_date']) + ? strtotime($data['publish_date']) : time()); + // the model object itself, for getting relations etc. + $this->response->data['post']=$this->resource; + // the prepared form data, processed by FooForms + $this->response->data['POST']=$data; + + } else { + if ($this->resource->dry()) + $f3->error(404, 'Post not found'); + // copy whole post model, to be able to fetch relations + // on the fly from within the template, if we need them + $this->response->data['post']=$this->resource; + } } + public function initBackend() { + $f3 = \Base::instance(); + $this->response->data['SUBPART'] = 'post_edit.html'; + + $ui = $f3->get('UI'); + if ($f3->get('text_editor') == 'sommernote') { + $f3->set('ASSETS.JS.summernote', $ui.'js/summernote.js'); + $f3->set('ASSETS.CSS.fontawesome', '//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css'); + $f3->set('ASSETS.CSS.summernote', $ui.'css/summernote.css'); + $f3->set('ASSETS.CSS.summernote-bs3', $ui.'css/summernote-bs3.css'); + } + + $f3->set('ASSETS.JS.jqueryui', $ui.'js/vendor/jquery.ui.widget.js'); + $f3->set('ASSETS.JS.jq-iframe-transport', $ui.'js/jquery.iframe-transport.js'); + $f3->set('ASSETS.JS.fileupload', $ui.'js/jquery.fileupload.js'); + $f3->set('ASSETS.CSS.fileupload', $ui.'css/jquery.fileupload.css'); + + $f3->set('DP_FORMAT', $f3->get('LANGUAGE') == 'de-DE' ? 'dd.mm.yyyy' : 'mm/dd/yy'); + } + /** - * remove a post entry + * update/create post + * @param \Base $f3 + * @param array $params */ - public function delete($f3, $params) - { - // TODO: erase comments and tag references - parent::delete($f3,$params); + public function post(\Base $f3,$params) { + parent::post($f3,$params); + if ($this->response instanceof \View\Backend) + $this->initBackend(); } - /** * add a comment from POST data to current blog post */ - public function addComment(\Base $f3, $params) - { + public function addComment(\Base $f3, $params) { if (isset($params['slug'])) { // you may only comment published posts $this->resource->load(array('slug = ? and publish_date <= ? and published = ?', @@ -124,47 +169,49 @@ public function addComment(\Base $f3, $params) return false; } $comment = new \Model\Comment(); - if ($comment->addToPost($this->resource->_id)) { + $comment->copyfrom('POST','author_name, author_email, message'); + $comment->post = $this->resource->_id; + $comment->approved = \Config::instance()->get('auto_approve_comments') ? 1 : 0; + $comment->save(); + + if ($f3->get('ERROR')) { + // if posting failed, return to comment form + $this->getSingle($f3, $params); + } else { // if posting was successful, reroute to the post view if (\Config::instance()->get('auto_approve_comments')) - \FlashMessage::instance()->addMessage('Your comment has been added.', - 'success'); + \Flash::instance()->addMessage('Your comment has been added.', 'success'); else - \FlashMessage::instance()->addMessage('Your comment has been added, but must be approved first before it becomes public.', - 'success'); + \Flash::instance()->addMessage('Your comment has been added, but must be approved first before it becomes public.', 'success'); $f3->reroute('/'.$params['slug']); - } else - // if posting failed, return to comment form - $this->getSingle($f3, $params); + } } else { // invalid URL, no post id given - \FlashMessage::instance()->addMessage('No Post ID specified.', 'danger'); + \Flash::instance()->addMessage('No Post ID specified.', 'danger'); $f3->reroute('/'); } } - public function publish($f3, $params) - { + + public function publish($f3, $params) { if ($this->resource->updateProperty(array('_id = ?', $params['id']), 'published', true)) { - \FlashMessage::instance()->addMessage('Your post was published. Hurray!', 'success'); + \Flash::instance()->addMessage('Your post was published. Hurray!', 'success'); } else { - \FlashMessage::instance()->addMessage('This Post ID was not found', 'danger'); + \Flash::instance()->addMessage('This Post ID was not found', 'danger'); } $f3->reroute('/admin/post'); } - public function hide($f3, $params) - { + public function hide($f3, $params) { if ($this->resource->updateProperty(array('_id = ?', $params['id']), 'published', false)) { - \FlashMessage::instance()->addMessage('Your post is now hidden.', 'success'); + \Flash::instance()->addMessage('Your post is now hidden.', 'success'); } else { - \FlashMessage::instance()->addMessage('This Post ID was not found', 'danger'); + \Flash::instance()->addMessage('This Post ID was not found', 'danger'); } $f3->reroute('/admin/post'); } - public function beforeroute() - { - $this->response = \View\Frontend::instance(); + public function beforeroute() { + $this->response = new \View\Frontend(); } } \ No newline at end of file diff --git a/app/controller/resource.php b/app/controller/resource.php index 8eb73af..fd6f453 100644 --- a/app/controller/resource.php +++ b/app/controller/resource.php @@ -10,70 +10,80 @@ abstract class Resource extends Base { /** @var \View\Base */ protected $response; + /** + * @param \Model\Base $model + */ public function __construct(\Model\Base $model) { $this->resource = $model; } - public function getSingle($f3,$param) { + /** + * get single record + * @param \Base $f3 + * @param array $params + */ + public function getSingle(\Base $f3,$params) { $f3->error(403); } - public function getList($f3,$param) { + public function edit(\Base $f3,$params) { + if ($f3->get('VERB') == 'POST') + $this->post($f3,$params); + elseif ($f3->get('VERB') == 'GET') + $this->getSingle($f3,$params); + } + + /** + * get collection of records + * @param \Base $f3 + * @param array $params + */ + public function getList(\Base $f3,$params) { $f3->error(403); } - public function post($f3, $params) { - $msg = \FlashMessage::instance(); + /** + * create / update a record + * @param \Base $f3 + * @param array $params + */ + public function post(\Base $f3, $params) { + $flash = \Flash::instance(); $this->resource->reset(); if (isset($params['id'])) { // update existing $this->resource->load(array('_id = ?', $params['id'])); if ($this->resource->dry()) { - $msg->addMessage("No record found with this ID.",'danger'); + $flash->addMessage('No record found with this ID.','danger'); $f3->reroute('/admin/'.$params['module']); return; } } $fields = $this->resource->getFieldConfiguration(); $this->resource->copyfrom('POST', array_keys($fields)); - $requiredError = false; - foreach ($fields as $name => $conf) { - if (isset($conf['required']) && $conf['required'] == TRUE - && empty($this->resource->{$name}) - ) { - $requiredError = true; - } - } - if($requiredError || $msg->hasMessages()) { - if($requiredError) - $msg->addMessage("Please fill in all required fields"); - $f3->copy('POST', 'form'); - $backend = new Backend(); - $backend->setView($this->response); - $backend->getSingle($f3, $params); - return; - } - - $out = $this->resource->save(); - if ($out) { + $this->resource->save(); + if (!$f3->get('ERROR')) { // display the list again after saving - $msg->addMessage("Successfully saved.", 'success'); + $flash->addMessage('Successfully saved.', 'success'); $f3->reroute('/admin/'.$params['module']); - } else { - $msg->addMessage("Operation failed.", 'danger'); } } - public function delete($f3, $params) { + /** + * delete a record + * @param \Base $f3 + * @param array $params + */ + public function delete(\Base $f3, $params) { $this->resource->reset(); - $msg = \FlashMessage::instance(); + $flash = \Flash::instance(); if (isset($params['id'])) { $this->resource->load(array('_id = ?', $params['id'])); if ($this->resource->dry()) { - $msg->addMessage("No record found with this ID.", 'danger'); + $flash->addMessage('No record found with this ID.', 'danger'); } else { $this->resource->erase(); - $msg->addMessage("Record deleted.", 'success'); + $flash->addMessage("Record deleted.", 'success'); } } $f3->reroute($f3->get('SESSION.LastPageURL')); diff --git a/app/controller/settings.php b/app/controller/settings.php index 9e72f8c..c0091a2 100644 --- a/app/controller/settings.php +++ b/app/controller/settings.php @@ -7,56 +7,56 @@ class Settings extends Base { public function beforeroute() { - parent::beforeroute(); + $this->response = new \View\Backend(); $this->response->data['LAYOUT'] = 'settings_layout.html'; } public function general( \Base $f3 ) { - $this->response->data['SUBPART'] = 'settings_general.html'; + $cfg = \Config::instance(); if($f3->get('VERB') == 'POST') { - $cfg = \Config::instance(); $error = false; - if($f3->devoid('POST.title')) { + if($f3->devoid('POST.blog_title')) { $error=true; - \FlashMessage::instance()->addMessage('Please enter a Blog Title','warning'); + \Flash::instance()->addMessage('Please enter a Blog Title','warning'); } else { - $cfg->set('blog_title',$f3->get('POST.title')); + $cfg->set('blog_title',$f3->get('POST.blog_title')); } - $cfg->set('ssl_backend',$f3->get('POST.ssl_backend')==1); - - $cfg->set('auto_approve_comments',$f3->get('POST.auto_approve_comments')==1); + $cfg->set('ssl_backend',$f3->get('POST.ssl_backend')=='1'); + $cfg->set('auto_approve_comments',$f3->get('POST.auto_approve_comments')=='1'); if(!$error) { - \FlashMessage::instance()->addMessage('Config saved','success'); + \Flash::instance()->addMessage('Config saved','success'); $cfg->save(); } } + $cfg->copyto('POST'); } public function database( \Base $f3 ) { - $this->response->data['SUBPART'] = 'settings_database.html'; + $cfg = \Config::instance(); if ($f3->get('VERB') == 'POST' && $f3->exists('POST.active_db')) { - $cfg = \Config::instance(); $type = $f3->get('POST.active_db'); - $cfg->{'DB_'.$type} = $f3->get('POST.'.$type); + $cfg->{'DB_'.$type} = $f3->get('POST.DB_'.$type); $cfg->ACTIVE_DB = $type; $cfg->save(); - \FlashMessage::instance()->addMessage('Config saved','success'); + \Flash::instance()->addMessage('Config saved','success'); $setup = new \Setup(); $setup->install($type); - + // logout $f3->clear('SESSION.user_id'); } + $cfg->copyto('POST'); + $f3->set('JIG_format', array('JSON','Serialized')); } } \ No newline at end of file diff --git a/app/controller/tag.php b/app/controller/tag.php index d078356..26b026c 100644 --- a/app/controller/tag.php +++ b/app/controller/tag.php @@ -4,53 +4,32 @@ class Tag extends Resource { - public function __construct() - { + public function __construct() { $mapper = new \Model\Tag(); parent::__construct($mapper); } /** * get a list of tags + * @param \Base $f3 + * @param array $params */ - public function getList($f3, $params) - { - if (isset($params['slug'])) { - $this->response = new \View\Frontend(); - - $f3->set('tag_cloud',$this->tagCloud()); - - $post = new \Model\Post(); - $post->filter('comments',array('approved = ?',1)); - $post->has('tags',array('slug = ?',$params['slug'])); - $post->countRel('comments'); - $page = \Pagination::findCurrentPage(); - $posts = $post->paginate($page - 1, 10, - array('publish_date <= ? and published = ?', date('Y-m-d'), true), - array('order'=>'publish_date desc')); - $this->response->data = array( - 'content' => $posts, - 'headline' => 'All Post by Tag: '.$params['slug'], - 'SUBPART' => 'post_list.html', - ); + public function getList(\Base $f3, $params) { + $this->response = new \View\JSON(); + $tags = $this->resource->find(); + if (!$tags) { + echo null; + return; } - elseif ($f3->get('AJAX')) { - $tags = $this->resource->find(); - if (!$tags) { - echo null; - return; - } - $return = array(); - foreach ($tags as $tag) { - $return[] = array( - 'value' => $tag->title, - 'tokens' => array($tag->title), - 'id' => $tag->_id, - ); - } - header('Content-Type: application/json'); - die(json_encode($return)); + $return = array(); + foreach ($tags as $tag) { + $return[] = array( + 'value' => $tag->title, + 'tokens' => array($tag->title), + 'id' => $tag->_id, + ); } + $this->response->data = $return; } public function tagCloud() { diff --git a/app/controller/user.php b/app/controller/user.php index 6da56d1..32fa3b5 100644 --- a/app/controller/user.php +++ b/app/controller/user.php @@ -5,31 +5,34 @@ class User extends Resource { - public function __construct() - { + public function __construct() { $mapper = new \Model\User(); parent::__construct($mapper); } - public function getSingle($f3, $params) - { - if (isset($params['id'])) - $this->response->data['content'] = $this->resource->load(array('_id = ?', $params['id'])); - if ($this->resource->dry() && !$this->response instanceof \View\Backend) - $f3->error(404, 'User not found'); + public function getSingle(\Base $f3, $params) { + $this->response->data['SUBPART'] = 'user_edit.html'; + if (isset($params['id'])) { + $this->resource->load(array('_id = ?', $params['id'])); + if ($this->resource->dry()) + $f3->error(404, 'User not found'); + $this->response->data['POST'] = $this->resource; + } } - public function getList($f3,$param) { - $this->response->data = array( - 'content' => $this->resource->find(), - ); + public function getList(\Base $f3,$param) { + $this->response->data['SUBPART'] = 'user_list.html'; + $this->response->data['content'] = $this->resource->find(); } - public function post($f3, $params) { - $msg = \FlashMessage::instance(); + + public function post(\Base $f3, $params) { + $this->response->data['SUBPART'] = 'user_edit.html'; + $msg = \Flash::instance(); if (isset($params['id'])) { // update existing $this->resource->load(array('_id = ?', $params['id'])); - if (!$this->resource->dry() && $this->resource->username == 'admin') { + if ($f3->get('HOST') == 'ikkez.de' + && !$this->resource->dry() && $this->resource->username == 'admin') { $msg->addMessage("You are not allowed to change the demo-admin",'danger'); $f3->reroute('/admin/'.$params['module']); return; @@ -38,16 +41,18 @@ public function post($f3, $params) { parent::post($f3,$params); } - public function delete($f3, $params) { + public function delete(\Base $f3, $params) { $this->resource->reset(); - $msg = \FlashMessage::instance(); + $msg = \Flash::instance(); if (isset($params['id'])) { $this->resource->load(array('_id = ?', $params['id'])); - if (!$this->resource->dry() && $this->resource->username == 'admin') { + if ($f3->get('HOST') == 'ikkez.de' + && !$this->resource->dry() && $this->resource->username == 'admin') { $msg->addMessage("You are not allowed to delete the demo-admin",'danger'); $f3->reroute('/admin/'.$params['module']); return; } + parent::delete($f3,$params); } $f3->reroute($f3->get('SESSION.LastPageURL')); } diff --git a/app/dict/de-DE.ini b/app/dict/de-DE.ini new file mode 100644 index 0000000..4d0ba37 --- /dev/null +++ b/app/dict/de-DE.ini @@ -0,0 +1,25 @@ +; general validation error messages +[error.validation] +email.required = Please enter an e-mail address. +email.invalid = This is no valid e-mail address. +email.host = Mail MX host not found. Please enter an existing e-mail address. + +; model specific error messages +[error.model.post] +title.required = Please give your post a title. +slug.required = No slug title was defined. +text.required = Your post needs a body text. + +[error.model.comment] +author_name.required = Please enter your name. +author_email.required = Please enter your e-mail address, so we can answer to you. +author_email.invalid = This is no valid e-mail address. +author_email.host = Nice try. That mail MX host was not found. Please enter an existing e-mail address. +message.required = Please enter your comment text. + +[error.model.user] +username.required = Please choose a username. +username.unique = This username is already taken. Please choose another one. +name.required = Please enter a name for this user. +password.required = Please enter a password. +mail.unique = This e-mail is already in use by another user. \ No newline at end of file diff --git a/app/dict/en.ini b/app/dict/en.ini new file mode 100644 index 0000000..4d0ba37 --- /dev/null +++ b/app/dict/en.ini @@ -0,0 +1,25 @@ +; general validation error messages +[error.validation] +email.required = Please enter an e-mail address. +email.invalid = This is no valid e-mail address. +email.host = Mail MX host not found. Please enter an existing e-mail address. + +; model specific error messages +[error.model.post] +title.required = Please give your post a title. +slug.required = No slug title was defined. +text.required = Your post needs a body text. + +[error.model.comment] +author_name.required = Please enter your name. +author_email.required = Please enter your e-mail address, so we can answer to you. +author_email.invalid = This is no valid e-mail address. +author_email.host = Nice try. That mail MX host was not found. Please enter an existing e-mail address. +message.required = Please enter your comment text. + +[error.model.user] +username.required = Please choose a username. +username.unique = This username is already taken. Please choose another one. +name.required = Please enter a name for this user. +password.required = Please enter a password. +mail.unique = This e-mail is already in use by another user. \ No newline at end of file diff --git a/app/dict/en.php b/app/dict/en.php deleted file mode 100644 index d7ec267..0000000 --- a/app/dict/en.php +++ /dev/null @@ -1,10 +0,0 @@ -'I love', - 'today'=>'Today is {0,date,long}', - 'tqbf'=>'The quick brown fox jumps over the lazy dog.', - 'undefined'=>'Undefined', - 'pi'=>'{0,number}', - 'money'=>'{0,number,currency}' -); diff --git a/app/dict/en_us.php b/app/dict/en_us.php deleted file mode 100644 index 846ca13..0000000 --- a/app/dict/en_us.php +++ /dev/null @@ -1,5 +0,0 @@ -'{0,number,percent}' -); diff --git a/app/dict/es.php b/app/dict/es.php deleted file mode 100644 index ffa20a0..0000000 --- a/app/dict/es.php +++ /dev/null @@ -1,7 +0,0 @@ -'Me encanta', - 'today'=>'Hoy es {0,date,long}', - 'tqbf'=>'El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.' -); diff --git a/app/dict/fr.ini b/app/dict/fr.ini deleted file mode 100644 index 2045daa..0000000 --- a/app/dict/fr.ini +++ /dev/null @@ -1,6 +0,0 @@ -; F3 recognizes .ini-style dictionaries too! -love=J'aime -today="Aujourd 'hui, c'est {0,date,long} -tqbf=Les naïfs ægithales hâtifs pondant \ -à Noël où il gèle sont sûrs d'être \ -déçus et de voir leurs drôles d'œufs abîmés." diff --git a/app/inc/error.php b/app/inc/error.php new file mode 100644 index 0000000..787ab28 --- /dev/null +++ b/app/inc/error.php @@ -0,0 +1,44 @@ +set('headline', 'Error '.$f3->get('ERROR.code')); + $f3->set('text', $f3->get('ERROR.text')); + $f3->set('ESCAPE', false); + + if ($f3->get('AJAX')) { + die(json_encode(array('error'=>$f3->get('ERROR.text')))); + } + + if ($f3->get('ERROR.code') == 400) { + \Flash::instance()->addMessage($f3->get('ERROR.text'),'warning'); + $f3->set('HALT',false); + return; + } + elseif ($f3->get('ERROR.code') == 404) { + $f3->set('headline', 'Page not found'); + } + elseif ($f3->get('ERROR.code') == 405) { + $f3->set('headline', 'This action is not allowed'); + } + elseif ($f3->get('ERROR.code') == 500) { + $f3->set('headline', 'Internal Server Error'); + if ($f3->get('DEV')) + $f3->set('trace',$f3->get('ERROR.trace')); + + @mail('ikkez0n3@gmail.com','Fabulog Error',$f3->get('ERROR.text')."\n\n".$f3->get('ERROR.trace')); + } + $f3->set('LAYOUT', 'error.html'); + $f3->set('HALT', true); + echo \Template::instance()->render('templates/layout.html'); + + } + +} \ No newline at end of file diff --git a/app/inc/flash.php b/app/inc/flash.php new file mode 100644 index 0000000..8b14bd7 --- /dev/null +++ b/app/inc/flash.php @@ -0,0 +1,68 @@ + + + @version 0.2.0 + @date: 21.01.2015 + **/ + +class Flash extends Prefab { + + /** @var \Base */ + protected $f3; + + /** @var array */ + protected $msg; + + /** @var array */ + protected $key; + + public function __construct($key = 'flash') { + $this->f3 = \Base::instance(); + $this->msg = &$this->f3->ref('SESSION.'.$key.'.msg'); + $this->key = &$this->f3->ref('SESSION.'.$key.'.key'); + } + + public function addMessage($text,$status = 'info') { + $this->msg[] = array('text'=>$text,'status'=>$status); + } + + public function getMessages() { + $out = $this->msg; + $this->clearMessages(); + return $out; + } + + public function clearMessages() { + $this->msg = array(); + } + + public function hasMessages() { + return !empty($this->msg); + } + + public function setKey($key,$val=TRUE) { + $this->key[$key] = $val; + } + + public function getKey($key) { + $out = NULL; + if ($this->key && array_key_exists($key,$this->key)) { + $out = $this->key[$key]; + unset($this->key[$key]); + } + return $out; + } + + public function hasKey($key) { + return ($this->key && array_key_exists($key,$this->key)); + } +} diff --git a/app/inc/flashmessage.php b/app/inc/flashmessage.php deleted file mode 100644 index 04e5952..0000000 --- a/app/inc/flashmessage.php +++ /dev/null @@ -1,60 +0,0 @@ - - - @version 0.1.0 - @date: 11.10.13 - **/ - -class FlashMessage extends Prefab { - - /** @var Base */ - protected $f3; - - public function __construct() { - $this->f3 = \Base::instance(); - if(!$this->f3->exists('SESSION.flash.msg')) - $this->f3->set('SESSION.flash.msg',array()); - } - - public function addMessage($text,$status = 'info') { - $msg = array('text'=>$text,'status'=>$status); - $this->f3->push('SESSION.flash.msg', $msg); - } - - public function getMessages() { - $out = $this->f3->get('SESSION.flash.msg'); - $this->clearMessages(); - return $out; - } - - public function clearMessages() { - $this->f3->clear('SESSION.flash.msg'); - } - - public function hasMessages() { - $val = $this->f3->get('SESSION.flash.msg'); - return !empty($val); - } - - public function setKey($key,$val=null) { - $this->f3->set('SESSION.flash.key.'.$key,$val); - } - - public function getKey($key) { - if(!$this->f3->exists('SESSION.flash.key.'.$key)) - return ''; - $out = $this->f3->get('SESSION.flash.key.'.$key); - $this->f3->clear('SESSION.flash.key.'.$key); - return $out; - } - -} \ No newline at end of file diff --git a/app/inc/setup.php b/app/inc/setup.php index bfc1dcd..5f3cac0 100644 --- a/app/inc/setup.php +++ b/app/inc/setup.php @@ -37,10 +37,10 @@ public function install($db_type) { $user->name = 'Administrator'; $user->password = 'fabulog'; $user->save(); - \FlashMessage::instance()->addMessage('Admin User created,' + \Flash::instance()->addMessage('Admin User created,' .' username: admin, password: fabulog','success'); } - \FlashMessage::instance()->addMessage('Setup complete','success'); + \Flash::instance()->addMessage('Setup complete','success'); } public function uninstall() diff --git a/app/inc/validation.php b/app/inc/validation.php new file mode 100644 index 0000000..9232a18 --- /dev/null +++ b/app/inc/validation.php @@ -0,0 +1,89 @@ +f3 = \Base::instance(); + } + + /** + * check if a model field is empty, but required + * @param mixed $val + * @param string $field + * @param string $context + * @return bool + */ + function required($val, $field, $context=null) { + if (!$this->f3->exists($context.'.required',$errText)) + $errText = $field.' field is required'; + if(empty($val) && $val!==0) { + $this->f3->error(400, $errText); + \Flash::instance()->setKey($context,'has-error'); + return false; + } + return true; + } + + /** + * check if a model field value is unique + * @param \DB\Cortex $model + * @param mixed $val + * @param string $field + * @param string $context + * @return bool + */ + function unique($model, $val, $field, $context=null) { + $valid = true; + if (empty($val)) + return $valid; + if (!$this->f3->exists($context.'.unique',$errText)) + $errText = 'This '.$field.' is already taken'; + $filter = $model->dry() + // new record + ? array($field.' = ?',$val) + // change field of existing record, excludes itself + : array($field.' = ? and _id != ?',$val,$model->_id); + if ($model->findone($filter)) { + $this->f3->error(400, $errText); + \Flash::instance()->setKey($context,'has-error'); + $valid = false; + } + return $valid; + } + + /** + * validate email address + * @param string $val + * @param string $context + * @param bool $mx + * @return bool + */ + function email($val, $context=null, $mx=true) { + $valid = true; + if (!$context) + $context = 'error.validation.email'; + if (!empty($val)) { + if (!\Audit::instance()->email($val,false)) { + $val = NULL; + if (!$this->f3->exists($context.'.invalid',$errText)) + $errText = 'e-mail is not valid'; + $this->f3->error(400, $errText); + $valid = false; + } + elseif ($mx && !\Audit::instance()->email($val,true)) { + $val = NULL; + if (!$this->f3->exists($context.'.host',$errText)) + $errText = 'unknown mail mx.host'; + $this->f3->error(400, $errText); + $valid = false; + } + } + if (!$valid) + \Flash::instance()->setKey($context,'has-error'); + return $valid; + } + +} \ No newline at end of file diff --git a/app/model/base.php b/app/model/base.php index 9d85d53..1dfdc63 100644 --- a/app/model/base.php +++ b/app/model/base.php @@ -7,16 +7,43 @@ class Base extends \DB\Cortex { // persistence settings protected $table, $db, $fieldConf; - public function __construct() - { + /** + * init the model + */ + public function __construct() { $f3 = \Base::instance(); $this->table = $f3->get('db_table_prefix').$this->table; $this->db = 'DB'; parent::__construct(); + // validation & error handler + $class = get_called_class(); // PHP 5.3 bug + $saveHandler = function(\DB\Cortex $self) use($class) { + $valid = true; + foreach($self->getFieldConfiguration() as $field=>$conf) { + if (isset($conf['type'])) { + $val = $self->get($field); + $model = strtolower(str_replace('\\','.',$class)); + // check required fields + if (isset($conf['required'])) + $valid = \Validation::instance()->required($val,$field,'error.'.$model.'.'.$field); + // check unique + if (isset($conf['unique'])) + $valid = \Validation::instance()->unique($self,$val,$field,'error.'.$model.'.'.$field); + } + } + return $valid; + }; + $this->beforesave($saveHandler); } - public function updateProperty($filter, $key, $value) - { + /** + * just a little mass update shortcut + * @param $filter + * @param $key + * @param $value + * @return bool + */ + public function updateProperty($filter, $key, $value) { $this->load($filter); if ($this->dry()) { return false; diff --git a/app/model/comment.php b/app/model/comment.php index 8164c79..021c270 100644 --- a/app/model/comment.php +++ b/app/model/comment.php @@ -35,30 +35,13 @@ class Comment extends Base { $db = 'DB'; /** - * save new comment - * @param $postID - * @return bool|mixed + * validate email address + * @param $val + * @return null */ - public function addToPost($postID) - { - $f3 = \Base::instance(); - foreach($this->fieldConf as $key => $conf) { - // check requirements - if (isset($this->fieldConf[$key]['required']) - && $this->fieldConf[$key]['required'] == TRUE - && (!$f3->exists('POST.'.$key) || empty($_POST[$key]) ) - ) { - \FlashMessage::instance()->addMessage('Please fill out all required fields', - 'warning'); - return false; - } - } - $this->set('author_name',$f3->get('POST.author_name')); - $this->set('author_email',$f3->get('POST.author_email')); - $this->set('message',$f3->get('POST.message')); - $this->set('post', $postID); - $this->set('approved', \Config::instance()->get('auto_approve_comments') ? 1 : 0 ); - return $this->save(); + public function set_author_email($val) { + return \Validation::instance()->email($val,'error.model.comment.author_email') + ? $val : null; } static public function countNew() { diff --git a/app/model/post.php b/app/model/post.php index f7e974e..22d85c1 100644 --- a/app/model/post.php +++ b/app/model/post.php @@ -70,13 +70,14 @@ public function set_title($val) { /** * set and add new tags to the post entity - * @param $val + * @param string $val * @return array */ public function set_tags($val) { if (!empty($val)) { $tagsArr = \Base::instance()->split($val); $tag_res = new Tag(); + $tags = array(); // find IDs of known Tags $known_tags = $tag_res->find(array('title IN ?', $tagsArr)); if ($known_tags) { @@ -99,8 +100,7 @@ public function set_tags($val) { } - public function save() - { + public function save() { /** @var Base $f3 */ $f3 = \Base::instance(); if(!$this->author) @@ -108,5 +108,4 @@ public function save() return parent::save(); } - } \ No newline at end of file diff --git a/app/model/user.php b/app/model/user.php index 82c59a9..876c05d 100644 --- a/app/model/user.php +++ b/app/model/user.php @@ -10,6 +10,7 @@ class User extends Base { 'type' => \DB\SQL\Schema::DT_VARCHAR128, 'nullable'=>false, 'required'=>true, + 'unique'=>true, ), 'name' => array( 'type' => \DB\SQL\Schema::DT_VARCHAR128, @@ -21,7 +22,8 @@ class User extends Base { 'required'=>true, ), 'mail' => array( - 'type' => \DB\SQL\Schema::DT_VARCHAR256 + 'type' => \DB\SQL\Schema::DT_VARCHAR256, + 'unique'=>true, ), 'news' => array( 'has-many' => array('\Model\Post','author'), @@ -30,24 +32,6 @@ class User extends Base { $table = 'user', $db = 'DB'; - /** - * check if username already exists - * @param $val - * @return null - */ - public function set_username($val) { - if($this->dry()) - // new - $user = $this->findone(array('username = ?',$val)); - else // existing - $user = $this->findone(array('username = ? and _id != ?',$val,$this->_id)); - if($user) { - $val = NULL; - \FlashMessage::instance()->addMessage('This username already exists. Please select a unique username.','warning'); - \FlashMessage::instance()->setKey('form.username','has-error'); - } - return $val; - } /** * crypt password @@ -77,12 +61,8 @@ public function set_password($val) { * @return null */ public function set_mail($val) { - if(!empty($val) && !\Audit::instance()->email($val,false)) { - $val = NULL; - \FlashMessage::instance()->addMessage('The entered email address is not valid.', 'warning'); - \FlashMessage::instance()->setKey('form.mail', 'has-error'); - } - return $val; + return \Validation::instance()->email($val) + ? $val : null; } } diff --git a/app/ui/css/backend.css b/app/ui/css/backend.css index 2d04f6d..038643f 100644 --- a/app/ui/css/backend.css +++ b/app/ui/css/backend.css @@ -148,4 +148,16 @@ body { /*db-select > div { float: left; margin-right: 10px; -}*/ \ No newline at end of file +}*/ + +.error-trace { + background-color: #191b1d; +} +.error-trace code { + background-color: transparent; + white-space: pre-wrap; /* CSS 3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} \ No newline at end of file diff --git a/app/ui/css/code.css b/app/ui/css/code.css new file mode 100644 index 0000000..d4bcb24 --- /dev/null +++ b/app/ui/css/code.css @@ -0,0 +1,58 @@ +pre { + background-color: #353535; +} +pre code { + color: #fff; +} +pre code span { + color: #9f9f9f; +} +code { + color: #DD4814; + word-wrap: break-word; +} +.comment, .doc_comment, .ml_comment { + color: darkgrey; +} +.variable { + color: #9fdfff; +} +.const, .constant_encapsed_string, .class_c, .dir, .file, .func_c, .halt_compiler, .line, .method_c, .ns_c, .ns_separator, .lnumber, .dnumber { + color: darksalmon; +} +.string, .array, .and_equal, .boolean_and, .boolean_or, .concat_equal, .dec, .div_equal, .inc, .is_equal, .is_greater_or_equal, .is_identical, .is_not_equal, .is_not_identical, .is_smaller_or_equal, .logical_and, .logical_or, .logical_xor, .minus_equal, .mod_equal, .mul_equal, .or_equal, .plus_equal, .sl, .sl_equal, .sr, .sr_equal, .xor_equal, .start_heredoc, .end_heredoc, .object_operator, .paamayim_nekudotayim { + color: #bfbfbf; +} +.abstract, .array_cast, .as, .break, .case, .catch, .class, .clone, .continue, .declare, .default, .do, .echo, .else, .elseif, .empty.enddeclare, .endfor, .endforach, .endif, .endswitch, .endwhile, .eval, .exit, .extends, .final, .for, .foreach, .function, .global, .goto, .if, .implements, .include, .include_once, .instanceof, .interface, .isset, .list, .namespace, .new, .print, .private, .public, .protected, .require, .require_once, .return, .static, .switch, .throw, .try, .unset, .use, .var, .while { + color: deeppink; +} +.open_tag, .open_tag_with_echo, .close_tag { + color: lightgrey; +} +.ini_section { + color: chartreuse; +} +.ini_key { + color: #9fdfff; +} +.ini_value { + color: darkseagreen; +} +.xml_tag, .inline_html { + color: deepskyblue; +} +.xml_attr { + color: white; +} +.xml_data { + color: deeppink; +} +.section { + color: skyblue; +} +.directive { + color: cornflowerblue; +} +.data { + color: gray; +} diff --git a/app/ui/js/app.js b/app/ui/js/app.js index e6a2c40..3cf212a 100644 --- a/app/ui/js/app.js +++ b/app/ui/js/app.js @@ -2,17 +2,17 @@ $(function () { // datepicker $('.datepicker').datepicker({ - format: 'dd.mm.yyyy', + format: dp_format, weekStart: 1 }); - // tag multiselect and auto-complete if($("#input-tags").length > 0) { + varTags = $(' +

{{ @msg.text }} diff --git a/app/ui/templates/layout.html b/app/ui/templates/layout.html index c02063f..451dc27 100644 --- a/app/ui/templates/layout.html +++ b/app/ui/templates/layout.html @@ -2,11 +2,11 @@ - + fabulog backend - {{@CONFIG.blog_title}} - + @@ -56,15 +56,11 @@
  • User
  • -
  • logout
  • +
  • - - settings - +
  • - - display blog - +
  • @@ -78,7 +74,7 @@ -
    {{ \Base::instance()->format('Page rendered in {0} msecs / Memory usage {1} KB',round(1e3*(microtime(TRUE)-$TIME),2),round(memory_get_usage(TRUE)/1e3,1)) }}
    +

    {{ \Base::instance()->format('Page rendered by fabulog v{0}, using {1} in {2} msecs / Memory usage {3} KB',@APP_VERSION,@CONFIG->ACTIVE_DB,round(1e3*(microtime(TRUE)-$TIME),2),round(memory_get_usage(TRUE)/1e3,1)) }}

    diff --git a/app/ui/templates/login.html b/app/ui/templates/login.html index 0a1e015..bdefc32 100644 --- a/app/ui/templates/login.html +++ b/app/ui/templates/login.html @@ -19,7 +19,7 @@
    - +
    -

    Hey, {{ @BACKEND_USER.name }}. This is the fabulog admin center, or at least it should become something like that ;)

    -

    fabulog currently has the following features:

    +

    Hey, {{ @BACKEND_USER.name }}. This is fabulog v{{ @APP_VERSION }} ;)

    +

    fabulog is currently just a playground and demo blog application. It has the following features:

      -
    • Jig, SQL and Mongo database support with Cortex ORM
    • -
    • Post module with publishing by state and date
    • -
    • Tag management
    • +
    • It's powered by Fat-Free Framework 3.4!
    • +
    • Cortex ORM with Jig, SQL and Mongo database support
    • +
    • Post module with drafts and publishing by date
    • +
    • add Tags to Posts with auto-completion
    • Comment module with backend moderation
    • simple User management module
    • -
    • Sommernote Bootstrap WYSIWYG editor
    • -
    • powered by fat-free framework
    • +
    • Twitter Bootstrap Backend, with Sommernote WYSIWYG editor
    • +
    • F3 FooForms plugin for handling all form data, yeah!
    diff --git a/app/ui/templates/pagebrowser.html b/app/ui/templates/pagebrowser.html index 2e104da..251239d 100644 --- a/app/ui/templates/pagebrowser.html +++ b/app/ui/templates/pagebrowser.html @@ -1,4 +1,4 @@ -
      +
      • First
      • diff --git a/app/ui/templates/post_edit.html b/app/ui/templates/post_edit.html index e5f6e62..7d101c4 100644 --- a/app/ui/templates/post_edit.html +++ b/app/ui/templates/post_edit.html @@ -8,35 +8,38 @@

        Create new Post

        -
        - -
        written by: {{ @content.author.name }}
        + + +
        written by: {{ @post.author.name }}
        -
        +
        - +
        -
        +
        +
        - +
        -
        - - +
        + +


        Upload an Image

        - - + +
        @@ -45,21 +48,21 @@

        Create new Post

        This is a little introduction text that mark out your article on overview pages or in RSS feed. - +
        -
        +
        - -
        {{isset(@content.text)?@content.text:''}}
        + +
        {{isset(@POST.text)?@POST.text:'' | raw }}
        @@ -71,7 +74,7 @@

        Create new Post

        - +
        @@ -80,13 +83,13 @@

        Create new Post

        @@ -97,20 +100,19 @@

        Create new Post

        - diff --git a/app/ui/templates/post_list.html b/app/ui/templates/post_list.html index b7f9e26..49f51d6 100644 --- a/app/ui/templates/post_list.html +++ b/app/ui/templates/post_list.html @@ -7,12 +7,12 @@

        Post list

        -

        {{date('d.m.Y',strtotime(@lastDate))}}

        +

        {{'{0,date}', strtotime(@lastDate) | format }}

        -

        {{date('d.m.Y',strtotime(@lastDate))}}

        +

        {{'{0,date}', strtotime(@lastDate) | format }}

        diff --git a/app/ui/templates/settings_database.html b/app/ui/templates/settings_database.html index d1a97c3..b14a86c 100644 --- a/app/ui/templates/settings_database.html +++ b/app/ui/templates/settings_database.html @@ -23,17 +23,13 @@

        - + Notice: make sure this folder is writable
        - + Notice: switching the format of existing db files is not supported -

        @@ -60,13 +56,13 @@

        - + Notice: this file will be created, if it's not existing. Make sure its folder is writable.

        - + @@ -76,7 +72,7 @@

        Missing dependency: "pdo_sqlite" module not loaded

        - +
        @@ -97,31 +93,31 @@

        - +
        - +
        - +
        - +
        - +
        - + @@ -131,7 +127,7 @@

        Missing dependency: "pdo_mysql" module not loaded - + @@ -152,26 +148,26 @@

        - +
        - +
        - +
        - +
        - + @@ -181,7 +177,7 @@

        Missing dependency: "pdo_pgsql" module not loaded - + @@ -202,26 +198,26 @@

        - +
        - +
        -
        +
        - +
        - +
        - + @@ -231,7 +227,7 @@

        Missing dependency: "pdo_sqlsrv" module not loaded

        - + @@ -253,21 +249,21 @@

        - +
        - +
        - +
        - + @@ -277,7 +273,7 @@

        Missing dependency: "mongo" module not loaded - + diff --git a/app/ui/templates/settings_general.html b/app/ui/templates/settings_general.html index d622e97..1aece8e 100644 --- a/app/ui/templates/settings_general.html +++ b/app/ui/templates/settings_general.html @@ -6,14 +6,14 @@

        Settings: General

        - +
        @@ -23,7 +23,7 @@

        Settings: General

        diff --git a/app/ui/templates/user_edit.html b/app/ui/templates/user_edit.html index adcf5f0..63007f8 100644 --- a/app/ui/templates/user_edit.html +++ b/app/ui/templates/user_edit.html @@ -8,30 +8,30 @@

        Create new User

        -
        + -
        +
        - +
        -
        +
        - +
        -
        - +
        +
        - +
        -
        +
        - +
        diff --git a/app/view/backend.php b/app/view/backend.php index 7bfcfcc..0c35696 100644 --- a/app/view/backend.php +++ b/app/view/backend.php @@ -10,6 +10,8 @@ class Backend extends Base { public function __construct() { /** @var \Base $f3 */ $f3 = \Base::instance(); + // change UI path to backend layout dir + $f3->copy('BACKEND_UI','UI'); // save last visited URL if ($f3->exists('SESSION.CurrentPageURL')) { if ($f3->get('SESSION.CurrentPageURL') != $f3->get('PARAMS.0')) @@ -24,20 +26,11 @@ public function setTemplate($filepath) { } public function render() { - /** @var \Base $f3 */ - $f3 = \Base::instance(); - if ($f3->get('AJAX')) { - // if this is an ajax request, respond a JSON string - echo json_encode($this->data); - } else { - // add template data to F3 hive - if($this->data) - $f3->mset($this->data); - // change UI path to backend layout dir - $f3->copy('BACKEND_UI','UI'); - // render base layout, the rest happens in template - echo \Template::instance()->render($this->template); - } + // add template data to F3 hive + if($this->data) + \Base::instance()->mset($this->data); + // render base layout, the rest happens in template + return \Template::instance()->render($this->template); } } \ No newline at end of file diff --git a/app/view/base.php b/app/view/base.php index 5869fcc..4125158 100644 --- a/app/view/base.php +++ b/app/view/base.php @@ -2,12 +2,12 @@ namespace View; -abstract class Base extends \Prefab { +abstract class Base { public $data = array(); /** - * create response content + * create and return response content * @return mixed */ abstract public function render(); diff --git a/app/view/frontend.php b/app/view/frontend.php index acf7a4d..c851bc0 100644 --- a/app/view/frontend.php +++ b/app/view/frontend.php @@ -9,7 +9,7 @@ public function render() { $f3 = \Base::instance(); if($this->data) $f3->mset($this->data); - echo \Template::instance()->render('templates/layout.html'); + return \Template::instance()->render('templates/layout.html'); } } \ No newline at end of file diff --git a/app/view/json.php b/app/view/json.php new file mode 100644 index 0000000..efcf22c --- /dev/null +++ b/app/view/json.php @@ -0,0 +1,12 @@ +data); + } + +} \ No newline at end of file diff --git a/index.php b/index.php index fdb895c..4564346 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,7 @@ set('APP_VERSION', '0.2.0'); //ini_set('display_errors', 1); //error_reporting(-1); @@ -33,10 +34,12 @@ } $f3->set('CONFIG', $cfg); -$f3->set('FLASH', FlashMessage::instance()); +$f3->set('FLASH', Flash::instance()); -\Template::instance()->extend('image','\ImageViewHelper::render'); +\Template::instance()->extend('image','\Template\Tags\Image::render'); \Template::instance()->extend('pagebrowser','\Pagination::renderTag'); +// Handles all data +\Template\FooForms::init(); ## POSTS // view list @@ -55,9 +58,10 @@ ## TAGS $f3->route(array( 'GET /tag [ajax]', - 'GET /tag/@slug' ),'Controller\Tag->getList'); +$f3->route('GET /tag/@slug','Controller\Post->getListByTag'); + /////////////// // backend // @@ -65,43 +69,25 @@ if (\Controller\Auth::isLoggedIn()) { - # specific routes + // specific routes // comments $f3->route(array( 'GET /admin/comment/list/@viewtype', 'GET /admin/comment/list/@viewtype/@page', ), 'Controller\Comment->getList'); - $f3->route('GET /admin/comment/approve/@id', 'Controller\Comment->approve'); - $f3->route('GET /admin/comment/reject/@id', 'Controller\Comment->reject'); - // posts - $f3->route('GET /admin/post/publish/@id', 'Controller\Post->publish'); - $f3->route('GET /admin/post/hide/@id', 'Controller\Post->hide'); - - # general CRUD operations - // create record - $f3->route('POST /admin/@module', 'Controller\@module->post'); - // update record - $f3->route('POST /admin/@module/save/@id', 'Controller\@module->post'); - // delete record - $f3->route('GET /admin/@module/delete/@id', 'Controller\@module->delete'); - - # general forms - // view list - $f3->route(array( - 'GET /admin/@module', - 'GET /admin/@module/@page') - , 'Controller\Backend->getList'); - // view create form + + // general CRUD operations + $f3->route('GET|POST /admin/@module', 'Controller\Backend->getList'); + $f3->route('GET|POST /admin/@module/@page', 'Controller\Backend->getList'); +// $f3->route('GET|POST /admin/@module/@action', 'Controller\Backend->@action'); + $f3->route('GET|POST /admin/@module/@action/@id', 'Controller\Backend->@action'); + // some method reroutes $f3->route('GET /admin/@module/create', 'Controller\Backend->getSingle'); - // view edit form + $f3->route('POST /admin/@module/create', 'Controller\Backend->post'); $f3->route('GET /admin/@module/edit/@id', 'Controller\Backend->getSingle'); + $f3->route('POST /admin/@module/edit/@id', 'Controller\Backend->post'); - -// $f3->route('GET /admin/@module', 'Controller\@module->getSingle'); -// $f3->route('GET /admin/@module/@action', 'Controller\@module->@action'); -// $f3->route('GET /admin/@module/@action/@id', 'Controller\@module->@action'); - - // dashboard + // backend home - dashboard $f3->route('GET /admin', 'Controller\Dashboard->main'); // settings panel @@ -109,9 +95,7 @@ $f3->route('GET|POST /admin/settings/@type', 'Controller\Settings->@type'); // no auth again - $f3->route('GET|POST /login', function (Base $f3) { - $f3->reroute('/admin'); - }); + $f3->redirect('GET|POST /login', '/admin', false); // upload file $f3->route('POST /admin/file [ajax]', function ($f3) { @@ -127,10 +111,7 @@ } else { // login - $f3->route(array('GET|POST /admin/*','GET|POST /admin'),function(Base $f3) { - $f3->reroute('/login'); - }); - + $f3->redirect(array('GET|POST /admin/*','GET|POST /admin'), '/login', false); $f3->route('GET|POST /login','Controller\Auth->login'); } diff --git a/lib/changelog.txt b/lib/CHANGELOG similarity index 88% rename from lib/changelog.txt rename to lib/CHANGELOG index b8a0730..6a4ddd2 100644 --- a/lib/changelog.txt +++ b/lib/CHANGELOG @@ -1,5 +1,67 @@ CHANGELOG +3.4.0 (1 January 2015) +* NEW: [redirects] section +* NEW: Custom config sections +* NEW: User-defined AUTOLOAD function +* NEW: ONREROUTE variable +* NEW: Provision for in-memory Jig database (#727) +* Return run() result (#687) +* Pass result of run() to mock() (#687) +* Add port suffix to REALM variable +* New attribute in tag to extend hive +* Adjust unit tests and clean up templates +* Expose header-related methods +* Web->request: allow content array +* Preserve contents of ROUTES (#723) +* Smart detection of PHP functions in template expressions +* Add afterrender() hook to View class +* Implement ArrayAccess and magic properties on hive +* Improvement on mocking of superglobals and request body +* Fix table creation for pgsql handled sessions +* Add QUERY to hive +* Exempt E_NOTICE from default error_reporting() +* Add method to build alias routes from template, fixes #693 +* Fix dangerous caching of cookie values +* Fix multiple encoding in nested templates +* Fix node attribute parsing for empty/zero values +* Apply URL encoding on BASE to emulate v2 behavior (#123) +* Improve Base->map performance (#595) +* Add simple backtrace for fatal errors +* Count Cursor->load() results (#581) +* Add form field name to Web->receive() callback arguments +* Fix missing newlines after template expansion +* Fix overwrite of ENCODING variable +* limit & offset workaround for SQL Server, fixes #671 +* SQL Mapper->find: GROUP BY SQL compliant statement +* Bug fix: Missing abstract method fields() +* Bug fix: Auto escaping does not work with mapper objects (#710) +* Bug fix: 'with' attribute in tag raise error when no token + inside +* View rendering: optional Content-Type header +* Bug fix: Undefined variable: cache (#705) +* Bug fix: Routing does not work if project base path includes valid + special URI character (#704) +* Bug fix: Template hash collision (#702) +* Bug fix: Property visibility is incorrect (#697) +* Bug fix: Missing Allow header on HTTP 405 response +* Bug fix: Double quotes in lexicon files (#681) +* Bug fix: Space should not be mandatory in ICU pluralization format string +* Bug fix: Incorrect log entry when SQL query contains a question mark +* Bug fix: Error stack trace +* Bug fix: Cookie expiration (#665) +* Bug fix: OR operator (||) parsed incorrectly +* Bug fix: Routing treatment of * wildcard character +* Bug fix: Mapper copyfrom() method doesn't allow class/object callbacks + (#590) +* Bug fix: exists() creates elements/properties (#591) +* Bug fix: Wildcard in routing pattern consumes entire query string (#592) +* Bug fix: Workaround bug in latest MongoDB driver +* Bug fix: Default error handler silently fails for AJAX request with + DEBUG>0 (#599) +* Bug fix: Mocked BODY overwritten (#601) +* Bug fix: Undefined pkey (#607) + 3.3.0 (8 August 2014) * NEW: Attribute in tag to extend hive * NEW: Image overlay with transparency and alignment control diff --git a/lib/license.txt b/lib/COPYING similarity index 100% rename from lib/license.txt rename to lib/COPYING diff --git a/lib/auth.php b/lib/auth.php index 0e8036a..5f7e050 100644 --- a/lib/auth.php +++ b/lib/auth.php @@ -129,7 +129,7 @@ protected function _ldap($id,$pw) { @ldap_close($dc)) { return $info[0]['uid'][0]==$id; } - user_error(self::E_LDAP); + user_error(self::E_LDAP,E_USER_ERROR); } /** @@ -176,7 +176,7 @@ protected function _smtp($id,$pw) { fclose($socket); return (bool)preg_match('/^235 /',$reply); } - user_error(self::E_SMTP); + user_error(self::E_SMTP,E_USER_ERROR); } /** diff --git a/lib/base.php b/lib/base.php index 63f60bf..cbf4f6d 100644 --- a/lib/base.php +++ b/lib/base.php @@ -178,12 +178,13 @@ function build($url,$params=array()) { * assemble url from alias name * @return NULL * @param $name string - * @param $params string + * @param $params array|string **/ - function alias($name,$params=null) { - $params=$params?$this->parse($params):array(); + function alias($name,$params=array()) { + if (!is_array($params)) + $params=$this->parse($params); if (empty($this->hive['ALIASES'][$name])) - user_error(sprintf(self::E_Named,$name)); + user_error(sprintf(self::E_Named,$name),E_USER_ERROR); $url=$this->build($this->hive['ALIASES'][$name],$params); return $url; } @@ -248,7 +249,8 @@ function &ref($key,$add=TRUE) { $this->sync('SESSION'); } elseif (!preg_match('/^\w+$/',$parts[0])) - user_error(sprintf(self::E_Hive,$this->stringify($key))); + user_error(sprintf(self::E_Hive,$this->stringify($key)), + E_USER_ERROR); if ($add) $var=&$this->hive; else @@ -524,7 +526,7 @@ function flip($key) { **/ function push($key,$val) { $ref=&$this->ref($key); - array_push($ref,$val); + $ref[] = $val; return $val; } @@ -584,10 +586,11 @@ function fixslashes($str) { * Split comma-, semi-colon, or pipe-separated string * @return array * @param $str string + * @param $noempty bool **/ - function split($str) { + function split($str,$noempty=TRUE) { return array_map('trim', - preg_split('/[,;|]/',$str,0,PREG_SPLIT_NO_EMPTY)); + preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0)); } /** @@ -672,6 +675,24 @@ function sign($num) { return $num?($num/abs($num)):0; } + /** + * Convert class constants to array + * @return array + * @param $class object|string + * @param $prefix string + **/ + function constants($class,$prefix='') { + $ref=new ReflectionClass($class); + $out=array(); + foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants())) + as $val) { + $out[$key=substr($val,strlen($prefix))]= + constant((is_object($class)?get_class($class):$class).'::'.$prefix.$key); + } + unset($ref); + return $out; + } + /** * Generate 64bit/base36 hash * @return string @@ -1020,12 +1041,6 @@ function expire($secs=0) { header('Expires: '.gmdate('r',$time+$secs)); header('Cache-Control: max-age='.$secs); header('Last-Modified: '.gmdate('r')); - $headers=$this->hive['HEADERS']; - if (isset($headers['If-Modified-Since']) && - strtotime($headers['If-Modified-Since'])+$secs>$time) { - $this->status(304); - die; - } } else header('Cache-Control: no-cache, no-store, must-revalidate'); @@ -1070,22 +1085,7 @@ function ip() { $_SERVER['REMOTE_ADDR']:'')); } - /** - * Log error; Execute ONERROR handler if defined, else display - * default error page (HTML for synchronous requests, JSON string - * for AJAX requests) - * @return NULL - * @param $code int - * @param $text string - * @param $trace array - **/ - function error($code,$text='',array $trace=NULL) { - $prior=$this->hive['ERROR']; - $header=$this->status($code); - $req=$this->hive['VERB'].' '.$this->hive['PATH']; - if (!$text) - $text='HTTP '.$code.' ('.$req.')'; - error_log($text); + function trace(array $trace=NULL) { if (!$trace) { $trace=debug_backtrace(FALSE); $frame=$trace[0]; @@ -1103,8 +1103,6 @@ function($frame) use($debug) { '__call|call_user_func)/',$frame['function'])); } ); - $highlight=PHP_SAPI!='cli' && - $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS); $out=''; $eol="\n"; // Analyze stack trace @@ -1117,12 +1115,33 @@ function($frame) use($debug) { ($debug>2 && isset($frame['args'])? $this->csv($frame['args']):'').')'; $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT']. - '/','',$frame['file'])).':'.$frame['line'].' '; - error_log('- '.$src.$line); - $out.='• '.($highlight? - ($this->highlight($src).' '.$this->highlight($line)): - ($src.$line)).$eol; + '/','',$frame['file'])).':'.$frame['line']; + $out.='['.$src.'] '.$line.$eol; } + return $out; + } + + /** + * Log error; Execute ONERROR handler if defined, else display + * default error page (HTML for synchronous requests, JSON string + * for AJAX requests) + * @return NULL + * @param $code int + * @param $text string + * @param $trace array + **/ + function error($code,$text='',array $trace=NULL) { + $prior=$this->hive['ERROR']; + $header=$this->status($code); + $req=$this->hive['VERB'].' '.$this->hive['PATH']; + if (!$text) + $text='HTTP '.$code.' ('.$req.')'; + error_log($text); + $trace=$this->trace($trace); + error_log($trace); + if ($highlight=PHP_SAPI!='cli' && + $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) + $trace=nl2br($this->highlight($trace)); $this->hive['ERROR']=array( 'status'=>$header, 'code'=>$code, @@ -1172,13 +1191,13 @@ function mock($pattern, $verb=strtoupper($parts[1]); if ($parts[2]) { if (empty($this->hive['ALIASES'][$parts[2]])) - user_error(sprintf(self::E_Named,$parts[2])); + user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR); $parts[4]=$this->hive['ALIASES'][$parts[2]]; $parts[4]=$this->build($parts[4], isset($parts[3])?$this->parse($parts[3]):array()); } if (empty($parts[4])) - user_error(sprintf(self::E_Pattern,$pattern)); + user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); $url=parse_url($parts[4]); parse_str(@$url['query'],$GLOBALS['_GET']); if (preg_match('/GET|HEAD/',$verb)) @@ -1221,11 +1240,11 @@ function route($pattern,$handler,$ttl=0,$kbps=0) { $this->hive['ALIASES'][$alias=$parts[2]]=$parts[3]; elseif (!empty($parts[4])) { if (empty($this->hive['ALIASES'][$parts[4]])) - user_error(sprintf(self::E_Named,$parts[4])); + user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR); $parts[3]=$this->hive['ALIASES'][$alias=$parts[4]]; } if (empty($parts[3])) - user_error(sprintf(self::E_Pattern,$pattern)); + user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); $type=empty($parts[5])? self::REQ_SYNC|self::REQ_AJAX: constant('self::REQ_'.strtoupper($parts[5])); @@ -1243,24 +1262,22 @@ function route($pattern,$handler,$ttl=0,$kbps=0) { * @param $url string * @param $permanent bool **/ - function reroute($url,$permanent=FALSE) { + function reroute($url=NULL,$permanent=FALSE) { + if (!$url) + $url=$this->hive['REALM']; + if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*)/',$url,$parts)) { + if (empty($this->hive['ALIASES'][$parts[1]])) + user_error(sprintf(self::E_Named,$parts[1]),E_USER_ERROR); + $url=$this->hive['ALIASES'][$parts[1]]; + } + $url=$this->build($url, + isset($parts[2])?$this->parse($parts[2]):array()); if (($handler=$this->hive['ONREROUTE']) && $this->call($handler,array($url,$permanent))!==FALSE) return; + if ($url[0]=='/') + $url=$this->hive['BASE'].$url; if (PHP_SAPI!='cli') { - if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*|https?:\/\/)/', - $url,$parts)) { - if (isset($parts[1])) { - if (empty($this->hive['ALIASES'][$parts[1]])) - user_error(sprintf(self::E_Named,$parts[1])); - $url=$this->hive['BASE']. - $this->hive['ALIASES'][$parts[1]]; - $url=$this->build($url, - isset($parts[2])?$this->parse($parts[2]):array()); - } - } - else - $url=$this->hive['BASE'].$url; header('Location: '.$url); $this->status($permanent?301:302); die; @@ -1292,15 +1309,16 @@ function map($url,$class,$ttl=0,$kbps=0) { * @return NULL * @param $pattern string|array * @param $url string + * @param $permanent bool */ - function redirect($pattern,$url) { + function redirect($pattern,$url,$permanent=TRUE) { if (is_array($pattern)) { foreach ($pattern as $item) - $this->redirect($item,$url); + $this->redirect($item,$url,$permanent); return; } - $this->route($pattern,function($this) use($url) { - $this->reroute($url); + $this->route($pattern,function($fw) use($url,$permanent) { + $fw->reroute($url,$permanent); }); } @@ -1327,6 +1345,23 @@ function blacklisted($ip) { return FALSE; } + /** + * Applies the specified URL mask and returns parameterized matches + * @return $args array + * @param $pattern string + * @param $url string|NULL + **/ + function mask($pattern,$url=NULL) { + if (!$url) + $url=$this->rel($this->hive['URI']); + $case=$this->hive['CASELESS']?'i':''; + preg_match('/^'. + preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)', + str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). + '\/?(?:\?.*)?$/'.$case.'um',$url,$args); + return $args; + } + /** * Match routes against incoming URI * @return mixed @@ -1337,7 +1372,7 @@ function run() { $this->error(403); if (!$this->hive['ROUTES']) // No routes defined - user_error(self::E_Routes); + user_error(self::E_Routes,E_USER_ERROR); // Match specific routes first $paths=array(); foreach ($keys=array_keys($this->hive['ROUTES']) as $key) @@ -1346,22 +1381,16 @@ function run() { array_multisort($paths,SORT_DESC,$keys,$vals); $this->hive['ROUTES']=array_combine($keys,$vals); // Convert to BASE-relative URL - $req=preg_replace( - '/^'.preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1', - $this->hive['URI'] - ); + $req=$this->rel($this->hive['URI']); $allowed=array(); - $case=$this->hive['CASELESS']?'i':''; - foreach ($this->hive['ROUTES'] as $url=>$routes) { - if (!preg_match('/^'. - preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)', - str_replace('\*','([^\?]*)',preg_quote($url,'/'))). - '\/?(?:\?.*)?$/'.$case.'um',$req,$args)) + foreach ($this->hive['ROUTES'] as $pattern=>$routes) { + if (!$args=$this->mask($pattern,$req)) continue; ksort($args); $route=NULL; - if (isset($routes[$this->hive['AJAX']+1])) - $route=$routes[$this->hive['AJAX']+1]; + if (isset( + $routes[$ptr=$this->hive['AJAX']+1][$this->hive['VERB']])) + $route=$routes[$ptr]; elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX])) $route=$routes[self::REQ_SYNC|self::REQ_AJAX]; if (!$route) @@ -1374,7 +1403,7 @@ function run() { $this->reroute(substr($parts['path'],0,-1). (isset($parts['query'])?('?'.$parts['query']):'')); list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; - if (is_bool(strpos($url,'/*'))) + if (is_bool(strpos($pattern,'/*'))) foreach (array_keys($args) as $key) if (is_numeric($key) && $key) unset($args[$key]); @@ -1382,7 +1411,7 @@ function run() { $this->hive['PARAMS']=$args=array_map('urldecode',$args); // Save matching route $this->hive['ALIAS']=$alias; - $this->hive['PATTERN']=$url; + $this->hive['PATTERN']=$pattern; if (is_string($handler)) { // Replace route pattern tokens in handler if any $handler=preg_replace_callback('/@(\w+\b)/', @@ -1407,6 +1436,12 @@ function($id) use($args) { $hash=$this->hash($this->hive['VERB'].' '. $this->hive['URI']).'.url',$data); if ($cached && $cached[0]+$ttl>$now) { + if (isset($headers['If-Modified-Since']) && + strtotime($headers['If-Modified-Since'])+ + $ttl>$now) { + $this->status(304); + die; + } // Retrieve from cache backend list($headers,$body,$result)=$data; if (PHP_SAPI!='cli') @@ -1427,10 +1462,13 @@ function($id) use($args) { $result=$this->call($handler,array($this,$args), 'beforeroute,afterroute'); $body=ob_get_clean(); - if (isset($cache) && !error_get_last()) + if (isset($cache) && !error_get_last()) { // Save to cache backend - $cache->set($hash, - array(headers_list(),$body,$result),$ttl); + $cache->set($hash,array( + // Remove cookies + preg_grep('/Set-Cookie\:/',headers_list(), + PREG_GREP_INVERT),$body,$result),$ttl); + } } $this->hive['RESPONSE']=$body; if (!$this->hive['QUIET']) { @@ -1465,6 +1503,43 @@ function($id) use($args) { return FALSE; } + /** + * Grab the real route handler behind the string expression + * @return object + * @param $func string + * @param $args mixed + **/ + function grab($func,$args) { + if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { + // Convert string to executable PHP callback + if (!class_exists($parts[1])) + user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR); + if ($this->hive['PSEUDO'] && + $this->hive['VERB']=='POST' && + strtolower($parts[3])=='post' && + preg_match('/!(put|delete)/', + implode(',',array_keys($_GET)),$hook) && + method_exists($parts[1],$parts[3])) { + // ReST implementation for non-capable HTTP clients + $this->hive['BODY']=http_build_query($_POST); + $this->hive['VERB']=$parts[3]; + $parts[3]=$hook[1]; + } + if ($parts[2]=='->') { + if (is_subclass_of($parts[1],'Prefab')) + $parts[1]=call_user_func($parts[1].'::instance'); + else { + $ref=new ReflectionClass($parts[1]); + $parts[1]=method_exists($parts[1],'__construct')? + $ref->newinstanceargs($args): + $ref->newinstance(); + } + } + $func=array($parts[1],$parts[3]); + } + return $func; + } + /** * Execute callback/hooks (supports 'class->method' format) * @return mixed|FALSE @@ -1475,22 +1550,14 @@ function($id) use($args) { function call($func,$args=NULL,$hooks='') { if (!is_array($args)) $args=array($args); + // Grab the real handler behind the string representation + if (is_string($func)) + $func=$this->grab($func,$args); // Execute function; abort if callback/hook returns FALSE - if (is_string($func) && - preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { - // Convert string to executable PHP callback - if (!class_exists($parts[1])) - user_error(sprintf(self::E_Class,$parts[1])); - if ($parts[2]=='->') - $parts[1]=is_subclass_of($parts[1],'Prefab')? - call_user_func($parts[1].'::instance'): - new $parts[1]($this); - $func=array($parts[1],$parts[3]); - } if (!is_callable($func)) // No route handler if ($hooks=='beforeroute,afterroute') { - $allowed=''; + $allowed=array(); if (isset($parts[1])) $allowed=array_intersect( array_map('strtoupper',get_class_methods($parts[1])), @@ -1501,7 +1568,8 @@ function call($func,$args=NULL,$hooks='') { } else user_error(sprintf(self::E_Method, - is_string($func)?$func:$this->stringify($func))); + is_string($func)?$func:$this->stringify($func)), + E_USER_ERROR); $obj=FALSE; if (is_array($func)) { $hooks=$this->split($hooks); @@ -1552,65 +1620,79 @@ function relay($funcs,$args=NULL) { } /** - * Configure framework according to .ini-style file settings - * @return NULL + * Configure framework according to .ini-style file settings; + * If optional 2nd arg is provided, template strings are interpreted + * @return object * @param $file string + * @param $allow bool **/ - function config($file) { + function config($file,$allow=FALSE) { preg_match_all( '/(?<=^|\n)(?:'. '\[(?
        .+?)\]|'. '(?[^\h\r\n;].*?)\h*=\h*'. '(?(?:\\\\\h*\r?\n|.+?)*)'. ')(?=\r?\n|$)/', - $this->read($file),$matches,PREG_SET_ORDER); + $this->read($file), + $matches,PREG_SET_ORDER); if ($matches) { $sec='globals'; foreach ($matches as $match) { if ($match['section']) $sec=$match['section']; - elseif (in_array($sec,array('routes','maps','redirects'))) { - call_user_func_array( - array($this,rtrim($sec,'s')), - array_merge(array($match['lval']), - str_getcsv($match['rval']))); - } else { - $args=array_map( - function($val) { - if (is_numeric($val)) - return $val+0; - $val=ltrim($val); - if (preg_match('/^\w+$/i',$val) && defined($val)) - return constant($val); - return preg_replace( - array('/\\\\"/','/\\\\\h*(\r?\n)/'), - array('"','\1'),$val); - }, - // Mark quoted strings with 0x00 whitespace - str_getcsv(preg_replace('/(?[^:]+)(?:\:(?.+))?/', - $sec,$parts); - $func=isset($parts['func'])?$parts['func']:NULL; - $custom=($parts['section']!='globals'); - if ($func) - $args=array($this->call($func, - count($args)>1?array($args):$args)); - call_user_func_array( - array($this,'set'), - array_merge( - array( - ($custom?($parts['section'].'.'):''). - $match['lval'] - ), - count($args)>1?array($args):$args - ) - ); + if ($allow) { + $match['lval']=Preview::instance()-> + resolve($match['lval']); + $match['rval']=Preview::instance()-> + resolve($match['rval']); + } + if (preg_match('/^(config|route|map|redirect)s\b/i', + $sec,$cmd)) { + call_user_func_array( + array($this,$cmd[1]), + array_merge(array($match['lval']), + str_getcsv($match['rval']))); + } + else { + $args=array_map( + function($val) { + if (is_numeric($val)) + return $val+0; + $val=ltrim($val); + if (preg_match('/^\w+$/i',$val) && + defined($val)) + return constant($val); + return trim(preg_replace( + array('/\\\\"/','/\\\\\h*(\r?\n)/'), + array('"','\1'),$val)); + }, + // Mark quoted strings with 0x00 whitespace + str_getcsv(preg_replace('/(?[^:]+)(?:\:(?.+))?/', + $sec,$parts); + $func=isset($parts['func'])?$parts['func']:NULL; + $custom=(strtolower($parts['section'])!='globals'); + if ($func) + $args=array($this->call($func, + count($args)>1?array($args):$args)); + call_user_func_array( + array($this,'set'), + array_merge( + array( + ($custom?($parts['section'].'.'):''). + $match['lval'] + ), + count($args)>1?array($args):$args + ) + ); + } } } } + return $this; } /** @@ -1697,13 +1779,13 @@ function dump($expr) { } /** - * Return path relative to the base directory + * Return path (and query parameters) relative to the base directory * @return string * @param $url string **/ function rel($url) { - return preg_replace('/(?:https?:\/\/)?'. - preg_quote($this->hive['BASE'],'/').'/','',rtrim($url,'/')); + return preg_replace('/^(?:https?:\/\/)?'. + preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url); } /** @@ -1844,7 +1926,7 @@ function __construct() { @ini_set('magic_quotes_gpc',0); @ini_set('register_globals',0); // Intercept errors/exceptions; PHP5.3-compatible - error_reporting((E_ALL|E_STRICT)&~E_NOTICE); + error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); $fw=$this; set_exception_handler( function($obj) use($fw) { @@ -1854,7 +1936,7 @@ function($obj) use($fw) { ); set_error_handler( function($code,$text) use($fw) { - if (error_reporting()) + if ($code & error_reporting()) $fw->error(500,$text); } ); @@ -1954,6 +2036,7 @@ function($code,$text) use($fw) { 'PLUGINS'=>$this->fixslashes(__DIR__).'/', 'PORT'=>$port, 'PREFIX'=>NULL, + 'PSEUDO'=>FALSE, 'QUERY'=>isset($uri['query'])?$uri['query']:'', 'QUIET'=>FALSE, 'RAW'=>FALSE, @@ -2361,7 +2444,7 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { $cache->set($hash,$data); return $data; } - user_error(sprintf(Base::E_Open,$file)); + user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); } /** @@ -2412,7 +2495,7 @@ function($expr) use($self) { '->'.$func.'('.$str.')'; } return ''. - (isset($expr[3])?$expr[3]:''); + (isset($expr[3])?$expr[3]."\n":''); }, preg_replace_callback( '/\{~(.+?)~\}/s', @@ -2485,7 +2568,7 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { return $data; } } - user_error(sprintf(Base::E_Open,$file)); + user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); } } @@ -2833,29 +2916,12 @@ class ISO extends Prefab { LC_zh='Chinese'; //@} - /** - * Convert class constants to array - * @return array - * @param $prefix string - **/ - protected function constants($prefix) { - $ref=new ReflectionClass($this); - $out=array(); - foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants())) - as $val) { - $out[$key=substr($val,strlen($prefix))]= - constant('static::'.$prefix.$key); - } - unset($ref); - return $out; - } - /** * Return list of languages indexed by ISO 639-1 language code * @return array **/ function languages() { - return $this->constants('LC_'); + return \Base::instance()->constants($this,'LC_'); } /** @@ -2863,7 +2929,7 @@ function languages() { * @return array **/ function countries() { - return $this->constants('CC_'); + return \Base::instance()->constants($this,'CC_'); } } diff --git a/lib/basket.php b/lib/basket.php index 5e1e332..7445e14 100644 --- a/lib/basket.php +++ b/lib/basket.php @@ -65,7 +65,7 @@ function &get($key) { return $this->id; if (array_key_exists($key,$this->item)) return $this->item[$key]; - user_error(sprintf(self::E_Field,$key)); + user_error(sprintf(self::E_Field,$key),E_USER_ERROR); return FALSE; } @@ -189,10 +189,12 @@ function drop() { /** * Hydrate item using hive array variable * @return NULL - * @param $key string + * @param $var array|string **/ - function copyfrom($key) { - foreach (\Base::instance()->get($key) as $key=>$val) + function copyfrom($var) { + if (is_string($var)) + $var=\Base::instance()->get($var); + foreach ($var as $key=>$val) $this->item[$key]=$val; } diff --git a/lib/bcrypt.php b/lib/bcrypt.php index 1bea272..6ecd61e 100644 --- a/lib/bcrypt.php +++ b/lib/bcrypt.php @@ -42,11 +42,11 @@ class Bcrypt extends Prefab { **/ function hash($pw,$salt=NULL,$cost=self::COST) { if ($cost<4 || $cost>31) - user_error(self::E_CostArg); + user_error(self::E_CostArg,E_USER_ERROR); $len=22; if ($salt) { if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt)) - user_error(self::E_SaltArg); + user_error(self::E_SaltArg,E_USER_ERROR); } else { $raw=16; diff --git a/lib/composer.json b/lib/composer.json new file mode 100644 index 0000000..0c4d971 --- /dev/null +++ b/lib/composer.json @@ -0,0 +1,12 @@ +{ + "name": "bcosca/fatfree-core", + "description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", + "homepage": "http://fatfreeframework.com/", + "license": "GPL-3.0", + "require": { + "php": ">=5.3.6" + }, + "autoload": { + "classmap": ["lib/"] + } +} diff --git a/lib/db/cortex.php b/lib/db/cortex.php index f25bd58..54c0b6f 100644 --- a/lib/db/cortex.php +++ b/lib/db/cortex.php @@ -1,27 +1,27 @@ - https://github.com/ikkez/F3-Sugar/ - - @package DB - @version 1.3.0-dev - @since 24.04.2012 - @date 02.04.2014 - **/ + * Cortex - a general purpose mapper for the PHP Fat-Free Framework + * + * The contents of this file are subject to the terms of the GNU General + * Public License Version 3.0. You may not use this file except in + * compliance with the license. Any of the license terms and conditions + * can be waived if you get permission from the copyright holder. + * + * crafted by __ __ __ + * |__| |--.| |--.-----.-----. + * | | < | <| -__|-- __| + * |__|__|__||__|__|_____|_____| + * + * Copyright (c) 2014 by ikkez + * Christian Knuth + * https://github.com/ikkez/F3-Sugar/ + * + * @package DB + * @version 1.3.1-dev + * @since 24.04.2012 + * @date 19.01.2015 + */ namespace DB; use DB\SQL\Schema; @@ -1002,7 +1002,7 @@ protected function getRelFilterOption($key) public function erase($filter = null) { $filter = $this->queryParser->prepareFilter($filter, $this->dbsType); - if ((!$filter && $this->emit('beforeerase')!==false) || $filter) { + if (!$filter && $this->emit('beforeerase')!==false) { if ($this->fieldConf) { foreach($this->fieldConf as $field => $conf) if (isset($conf['has-many']) && @@ -1010,10 +1010,10 @@ public function erase($filter = null) $this->set($field,null); $this->save(); } + $this->mapper->erase(); + $this->emit('aftererase'); + } elseif($filter) $this->mapper->erase($filter); - if (!$filter) - $this->emit('aftererase'); - } } /** @@ -1031,9 +1031,16 @@ function save() return false; $result=$this->update(); } + // update changed collections + $fields = $this->fieldConf; + if ($fields) + foreach($fields as $key=>$conf) + if (!empty($this->fieldsCache[$key]) && $this->fieldsCache[$key] instanceof CortexCollection + && $this->fieldsCache[$key]->hasChanged()) + $this->set($key,$this->fieldsCache[$key]->getAll('_id',true)); + // m:m save cascade if (!empty($this->saveCsd)) { - $fields = $this->fieldConf; foreach($this->saveCsd as $key => $val) { if($fields[$key]['relType'] == 'has-many') { $relConf = $fields[$key]['has-many']; @@ -1065,6 +1072,7 @@ function save() $val->save(); } } + $this->saveCsd = array(); } $this->emit($new?'afterinsert':'afterupdate'); return $result; @@ -1283,9 +1291,11 @@ function set($key, $val) $schema = new Schema($this->db); $table = $schema->alterTable($this->table); // add missing field - if(!in_array($key,$table->getCols())) { + if (!in_array($key,$table->getCols())) { // determine data type - if (is_int($val)) $type = $schema::DT_INT; + if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type'])) + $type = $this->fieldConf[$key]['type']; + elseif (is_int($val)) $type = $schema::DT_INT; elseif (is_double($val)) $type = $schema::DT_DOUBLE; elseif (is_float($val)) $type = $schema::DT_FLOAT; elseif (is_bool($val)) $type = $schema::DT_BOOLEAN; @@ -1459,7 +1469,7 @@ function &get($key,$raw = false) } $result = $cx->getSubset($key, array($this->get($toConf[1]))); $this->fieldsCache[$key] = $result ? (($type == 'has-one') - ? $result[0][0] : $result[0]) : NULL; + ? $result[0][0] : CortexCollection::factory($result[0])) : NULL; } else { $crit = array($fromConf[1].' = ?', $this->get($toConf[1],true)); $crit = $this->mergeWithRelFilter($key, $crit); @@ -1481,6 +1491,10 @@ function &get($key,$raw = false) } else $mmTable = $fromConf['refTable']; // create mm table mapper + if (!$this->get($id,true)) { + $this->fieldsCache[$key] = null; + return $this->fieldsCache[$key]; + } $rel = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable)); if ($this->collectionID && $this->smartLoading) { $cx = CortexCollection::instance($this->collectionID); @@ -1513,7 +1527,8 @@ function &get($key,$raw = false) } // fetch subset from preloaded rels using cached pivot keys $fkeys = $cx->getSubset($key.'_pivot', array($this->get($id))); - $this->fieldsCache[$key] = $fkeys ? $cx->getSubset($key, $fkeys[0]) : NULL; + $this->fieldsCache[$key] = $fkeys ? + CortexCollection::factory($cx->getSubset($key, $fkeys[0])) : NULL; } // no collection else { // find foreign keys @@ -1576,7 +1591,7 @@ function &get($key,$raw = false) $cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL); } // get a subset of the preloaded set - $this->fieldsCache[$key] = $cx->getSubset($key, $fkeys); + $this->fieldsCache[$key] = CortexCollection::factory($cx->getSubset($key, $fkeys)); } else { // load foreign models $filter = array($relConf[1].' IN ?', $fkeys); @@ -1721,17 +1736,10 @@ public function cast($obj = NULL, $rel_depths = 1) // cast relations $val = (($relType == 'belongs-to-one' || $relType == 'belongs-to-many') && !$mp->exists($key)) ? NULL : $mp->get($key); - if (is_array($val) || is_object($val)) { - if ($relType == 'belongs-to-one' || $relType == 'has-one') - // single object - $val = $val->cast(null, $rel_depths); - elseif ($relType == 'belongs-to-many' || $relType == 'has-many') - // multiple objects - foreach ($val as $k => $item) - $val[$k] = is_object($item) ? $item->cast(null, $rel_depths) : null; - } - if ($val instanceof CortexCollection) - $val = $val->expose(); + if ($val instanceof Cortex) + $val = $val->cast(null, $rel_depths); + elseif ($val instanceof CortexCollection) + $val = $val->castAll($rel_depths); } // decode array fields elseif (isset($this->fieldConf[$key]['type'])) { @@ -2318,6 +2326,7 @@ class CortexCollection extends \ArrayIterator { protected $relSets = array(), $pointer = 0, + $changed = false, $cid; const @@ -2342,20 +2351,30 @@ private function __clone() { } * set a collection of models * @param $models */ - function setModels($models) { + function setModels($models,$init=true) { array_map(array($this,'add'),$models); + if ($init) + $this->changed = false; } /** * add single model to collection * @param $model */ - function add(Cortex $model) - { + function add(Cortex $model) { $model->addToCollection($this->cid); $this->append($model); } + public function offsetSet($i, $val) { + $this->changed=true; + parent::offsetSet($i,$val); + } + + public function hasChanged() { + return $this->changed; + } + /** * get a related collection * @param $key @@ -2400,9 +2419,11 @@ public function getSubset($prop,$keys) { trigger_error(sprintf(self::E_SubsetKeysValue,gettype($keys))); if (!$this->hasRelSet($prop) || !($relSet = $this->getRelSet($prop))) return null; - foreach ($keys as &$key) + foreach ($keys as &$key) { if ($key instanceof \MongoId) $key = (string) $key; + unset($key); + } return array_values(array_intersect_key($relSet, array_flip($keys))); } @@ -2496,6 +2517,12 @@ public function slice($offset,$limit=null) { unset($this[$ii]); } + static public function factory($records) { + $cc = new self(); + $cc->setModels($records); + return $cc; + } + /** * @param $cid * @return CortexCollection diff --git a/lib/db/cursor.php b/lib/db/cursor.php index 12c5c04..f8e8d9f 100644 --- a/lib/db/cursor.php +++ b/lib/db/cursor.php @@ -89,10 +89,10 @@ abstract function update(); /** * Hydrate mapper object using hive array variable * @return NULL - * @param $key string + * @param $var array|string * @param $func callback **/ - abstract function copyfrom($key,$func=NULL); + abstract function copyfrom($var,$func=NULL); /** * Populate hive array variable with mapper fields @@ -106,7 +106,7 @@ abstract function copyto($key); * Causes a fatal error in PHP 5.3.5if uncommented * return ArrayIterator **/ - //abstract function getiterator(); +// abstract function getiterator(); /** diff --git a/lib/db/jig/mapper.php b/lib/db/jig/mapper.php index 8e212a3..6c11aea 100644 --- a/lib/db/jig/mapper.php +++ b/lib/db/jig/mapper.php @@ -72,7 +72,7 @@ function &get($key) { return $this->id; if (array_key_exists($key,$this->document)) return $this->document[$key]; - user_error(sprintf(self::E_Field,$key)); + user_error(sprintf(self::E_Field,$key),E_USER_ERROR); } /** @@ -420,11 +420,12 @@ function reset() { /** * Hydrate mapper object using hive array variable * @return NULL - * @param $key string + * @param $var array|string * @param $func callback **/ - function copyfrom($key,$func=NULL) { - $var=\Base::instance()->get($key); + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->get($var); if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) diff --git a/lib/db/mongo.php b/lib/db/mongo.php index 55d35db..ffe772e 100644 --- a/lib/db/mongo.php +++ b/lib/db/mongo.php @@ -81,7 +81,7 @@ function log() { **/ function drop() { $out=$this->db->drop(); - $this->setprofilinglevel(-1); + $this->setprofilinglevel(2); return $out; } @@ -105,7 +105,7 @@ function __construct($dsn,$dbname,array $options=NULL) { $this->uuid=\Base::instance()->hash($this->dsn=$dsn); $class=class_exists('\MongoClient')?'\MongoClient':'\Mongo'; $this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname); - $this->setprofilinglevel(-1); + $this->setprofilinglevel(2); } } diff --git a/lib/db/mongo/mapper.php b/lib/db/mongo/mapper.php index 6285c4f..3287ce5 100644 --- a/lib/db/mongo/mapper.php +++ b/lib/db/mongo/mapper.php @@ -70,7 +70,7 @@ function set($key,$val) { function &get($key) { if ($this->exists($key)) return $this->document[$key]; - user_error(sprintf(self::E_Field,$key)); + user_error(sprintf(self::E_Field,$key),E_USER_ERROR); } /** @@ -297,11 +297,12 @@ function reset() { /** * Hydrate mapper object using hive array variable * @return NULL - * @param $key string + * @param $var array|string * @param $func callback **/ - function copyfrom($key,$func=NULL) { - $var=\Base::instance()->get($key); + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->get($var); if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) diff --git a/lib/db/sql.php b/lib/db/sql.php index 6c02c7d..9301284 100644 --- a/lib/db/sql.php +++ b/lib/db/sql.php @@ -157,6 +157,12 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). '/'; } + if ($log) + $this->log.=date('r').' ('. + sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + '[CACHED] '. + preg_replace($keys,$vals, + str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; } elseif (is_object($query=$this->pdo->prepare($cmd))) { foreach ($arg as $key=>$val) { @@ -174,13 +180,18 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). '/'; } + if ($log) + $this->log.=date('r').' ('. + sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + preg_replace($keys,$vals, + str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; $query->execute(); $error=$query->errorinfo(); if ($error[0]!=\PDO::ERR_NONE) { // Statement-level error occurred if ($this->trans) $this->rollback(); - user_error('PDOStatement: '.$error[2]); + user_error('PDOStatement: '.$error[2],E_USER_ERROR); } if (preg_match('/^\s*'. '(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING|EXEC)\b/is', @@ -210,15 +221,9 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { // PDO-level error occurred if ($this->trans) $this->rollback(); - user_error('PDO: '.$error[2]); + user_error('PDO: '.$error[2],E_USER_ERROR); } } - if ($log) - $this->log.=date('r').' ('. - sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - (empty($cached)?'':'[CACHED] '). - preg_replace($keys,$vals, - str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; } if ($this->trans && $auto) $this->commit(); @@ -316,11 +321,11 @@ function schema($table,$fields=NULL,$ttl=0) { $rows[$row[$val[1]]]=array( 'type'=>$row[$val[2]], 'pdo_type'=> - preg_match('/int\b|int(?=eger)|bool/i', - $row[$val[2]],$parts)? - constant('\PDO::PARAM_'. - strtoupper($parts[0])): - \PDO::PARAM_STR, + preg_match('/int\b|integer/i',$row[$val[2]])? + \PDO::PARAM_INT: + (preg_match('/bool/i',$row[$val[2]])? + \PDO::PARAM_BOOL: + \PDO::PARAM_STR), 'default'=>is_string($row[$val[3]])? preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', $row[$val[3]]):$row[$val[3]], diff --git a/lib/db/sql/mapper.php b/lib/db/sql/mapper.php index a022c75..ca05a10 100644 --- a/lib/db/sql/mapper.php +++ b/lib/db/sql/mapper.php @@ -54,6 +54,14 @@ function dbtype() { return 'SQL'; } + /** + * Return mapped table + * @return string + **/ + function table() { + return $this->source; + } + /** * Return TRUE if field is defined * @return bool @@ -63,6 +71,20 @@ function exists($key) { return array_key_exists($key,$this->fields+$this->adhoc); } + /** + * Return TRUE if any/specified field value has changed + * @return bool + * @param $key string + **/ + function changed($key=NULL) { + if (isset($key)) + return $this->fields[$key]['changed']; + foreach($this->fields as $key=>$field) + if ($field['changed']) + return TRUE; + return FALSE; + } + /** * Assign value to field * @return scalar @@ -95,7 +117,7 @@ function &get($key) { return $this->fields[$key]['value']; elseif (array_key_exists($key,$this->adhoc)) return $this->adhoc[$key]['value']; - user_error(sprintf(self::E_Field,$key)); + user_error(sprintf(self::E_Field,$key),E_USER_ERROR); } /** @@ -197,15 +219,19 @@ function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { } $sql.=' WHERE '.$filter; } - if ($options['group']) + if ($options['group']) { $sql.=' GROUP BY '.implode(',',array_map( function($str) use($db) { - return preg_match('/^(\w+)(?:\h+HAVING|\h*(?:,|$))/i', - $str,$parts)? - ($db->quotekey($parts[1]). - (isset($parts[2])?(' '.$parts[2]):'')):$str; + return preg_replace_callback( + '/\b(\w+)\h*(HAVING.+|$)/i', + function($parts) use($db) { + return $db->quotekey($parts[1]); + }, + $str + ); }, explode(',',$options['group']))); + } if ($options['order']) { $sql.=' ORDER BY '.implode(',',array_map( function($str) use($db) { @@ -348,7 +374,8 @@ function skip($ofs=1) { **/ function insert() { $args=array(); - $ctr=0; + $actr=0; + $nctr=0; $fields=''; $values=''; $filter=''; @@ -369,13 +396,14 @@ function insert() { empty($field['value']) && !$field['nullable']) $inc=$key; $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $nkeys[$ctr+1]=array($field['value'],$field['pdo_type']); + $nkeys[$nctr+1]=array($field['value'],$field['pdo_type']); + $nctr++; } if ($field['changed'] && $key!=$inc) { - $fields.=($ctr?',':'').$this->db->quotekey($key); - $values.=($ctr?',':'').'?'; - $args[$ctr+1]=array($field['value'],$field['pdo_type']); - $ctr++; + $fields.=($actr?',':'').$this->db->quotekey($key); + $values.=($actr?',':'').'?'; + $args[$actr+1]=array($field['value'],$field['pdo_type']); + $actr++; $ckeys[]=$key; } $field['changed']=FALSE; @@ -521,11 +549,12 @@ function reset() { /** * Hydrate mapper object using hive array variable * @return NULL - * @param $key string + * @param $var array|string * @param $func callback **/ - function copyfrom($key,$func=NULL) { - $var=\Base::instance()->get($key); + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->get($var); if ($func) $var=call_user_func($func,$var); foreach ($var as $key=>$val) diff --git a/lib/db/sql/schema.php b/lib/db/sql/schema.php index f7f1e7e..eae536b 100644 --- a/lib/db/sql/schema.php +++ b/lib/db/sql/schema.php @@ -1,24 +1,24 @@ - https://github.com/ikkez/F3-Sugar/ - - @package DB - @version 2.1.1 + * SQL Table Schema Builder extension for the PHP Fat-Free Framework + * + * The contents of this file are subject to the terms of the GNU General + * Public License Version 3.0. You may not use this file except in + * compliance with the license. Any of the license terms and conditions + * can be waived if you get permission from the copyright holder. + * + * crafted by __ __ __ + * |__| |--.| |--.-----.-----. + * | | < | <| -__|-- __| + * |__|__|__||__|__|_____|_____| + * + * Copyright (c) 2014 by ikkez + * Christian Knuth + * https://github.com/ikkez/F3-Sugar/ + * + * @package DB + * @version 2.1.1 **/ diff --git a/lib/image.php b/lib/image.php index 3132f55..55a29ba 100644 --- a/lib/image.php +++ b/lib/image.php @@ -26,6 +26,7 @@ class Image { //@{ Messages const E_Color='Invalid color specified: %s', + E_File='File not found', E_Font='CAPTCHA font not found', E_Length='Invalid CAPTCHA length: %s'; //@} @@ -58,7 +59,7 @@ class Image { function rgb($color) { $hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT); if (($len=strlen($hex))>6) - user_error(sprintf(self::E_Color,'0x'.$hex)); + user_error(sprintf(self::E_Color,'0x'.$hex),E_USER_ERROR); $color=str_split($hex,$len/3); foreach ($color as &$hue) { $hue=hexdec(str_repeat($hue,6/$len)); @@ -227,11 +228,12 @@ function crop($x1,$y1,$x2,$y2) { function resize($width,$height,$crop=TRUE,$enlarge=TRUE) { // Adjust dimensions; retain aspect ratio $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); - if (!$crop) + if (!$crop) { if ($width/$ratio<=$height) $height=$width/$ratio; else $width=$height*$ratio; + } if (!$enlarge) { $width=min($origw,$width); $height=min($origh,$height); @@ -393,7 +395,7 @@ function identicon($str,$size=64,$blocks=4) { function captcha($font,$size=24,$len=5, $key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) { if ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) { - user_error(sprintf(self::E_Length,$len)); + user_error(sprintf(self::E_Length,$len),E_USER_ERROR); return FALSE; } $fw=Base::instance(); @@ -439,7 +441,7 @@ function captcha($font,$size=24,$len=5, $fw->set($key,$seed); return $this->save(); } - user_error(self::E_Font); + user_error(self::E_Font,E_USER_ERROR); return FALSE; } @@ -559,15 +561,18 @@ function load($str) { * @param $flag bool * @param $path string **/ - function __construct($file=NULL,$flag=FALSE,$path='') { + function __construct($file=NULL,$flag=FALSE,$path=NULL) { $this->flag=$flag; if ($file) { $fw=Base::instance(); // Create image from file $this->file=$file; - foreach ($fw->split($path?:$fw->get('UI').';./') as $dir) + if (!isset($path)) + $path=$fw->get('UI').';./'; + foreach ($fw->split($path,FALSE) as $dir) if (is_file($dir.$file)) return $this->load($fw->read($dir.$file)); + user_error(self::E_File,E_USER_ERROR); } } diff --git a/lib/imageviewhelper.php b/lib/imageviewhelper.php deleted file mode 100644 index d1af80d..0000000 --- a/lib/imageviewhelper.php +++ /dev/null @@ -1,59 +0,0 @@ - - - @version 0.1.0 - @date: 22.08.13 - **/ - -class ImageViewHelper { - - static public function resize($path,$width,$height,$crop=false,$quality=75) { - $f3 = \Base::instance(); - $new_file_name = $f3->hash($path.$width.$height.$crop.$quality).'.jpg'; - $dst_path = $f3->get('UPLOADS').'cache/'; - list($path, $file) = explode('/', $path); - - if(!is_dir($dst_path)) - mkdir($dst_path); - elseif(!file_exists($dst_path.$new_file_name)) { - $imgObj = new \Image($file,false, $path.'/'); - $imgObj->resize($width, $height, $crop); - $file_data = $imgObj->dump('jpeg',null, $quality); - $f3->write($dst_path.$new_file_name, $file_data); - } - return $dst_path.$new_file_name; - } - - static public function render($args) { - $options = array( - 'src','width', 'height', 'crop', 'quality' - ); - $attr = $args['@attrib']; - $tmp = \Template::instance(); - foreach ($attr as $key=>&$att) { - if (preg_match('/{{(.+?)}}/s', $att)) { - if(in_array($key,$options)) - $att = $tmp->token($att); - else - $att = $tmp->build($att); - } - unset($att); - } - $tag_attr = ''; - foreach ($attr as $key => $val) - if (!in_array($key, $options)) - $tag_attr .= ' '.$key.'="'.$tmp->build($val).'"'; - $crop = isset($attr['crop']) && $attr['crop'] == "true" ? 'true':'false'; - - return ''; - } -} \ No newline at end of file diff --git a/lib/smtp.php b/lib/smtp.php index b70f56e..92cb776 100644 --- a/lib/smtp.php +++ b/lib/smtp.php @@ -145,7 +145,7 @@ protected function dialog($cmd=NULL,$log=TRUE) { **/ function attach($file,$alias=NULL) { if (!is_file($file)) - user_error(sprintf(self::E_Attach,$file)); + user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); if (is_string($alias)) $file=array($alias=>$file); $this->attachments[]=$file; @@ -162,7 +162,7 @@ function send($message,$log=TRUE) { return FALSE; // Message should not be blank if (!$message) - user_error(self::E_Blank); + user_error(self::E_Blank,E_USER_ERROR); $fw=Base::instance(); // Retrieve headers $headers=$this->headers; @@ -198,7 +198,7 @@ function send($message,$log=TRUE) { $reqd=array('From','To','Subject'); foreach ($reqd as $id) if (empty($headers[$id])) - user_error(sprintf(self::E_Header,$id)); + user_error(sprintf(self::E_Header,$id),E_USER_ERROR); $eol="\r\n"; $str=''; // Stringify headers diff --git a/lib/template.php b/lib/template.php index 2f24690..8c1c0e1 100644 --- a/lib/template.php +++ b/lib/template.php @@ -73,7 +73,7 @@ protected function _include(array $node) { 'token($attrib['if']).') '):''). ('echo $this->render('. - (preg_match('/\{\{(.+?)\}\}/',$attrib['href'])? + (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? $this->token($attrib['href']): Base::instance()->stringify($attrib['href'])).','. '$this->mime,'.$hive.'); ?>'); @@ -259,7 +259,7 @@ function __call($func,array $args) { return call_user_func_array($this->custom[$func],$args); if (method_exists($this,$func)) return call_user_func_array(array($this,$func),$args); - user_error(sprintf(self::E_Method,$func)); + user_error(sprintf(self::E_Method,$func),E_USER_ERROR); } /** diff --git a/lib/template/fooforms.php b/lib/template/fooforms.php new file mode 100644 index 0000000..bdfcd0d --- /dev/null +++ b/lib/template/fooforms.php @@ -0,0 +1,28 @@ + + + @version: 0.1.1 + @date: 12.03.14 + + **/ + +namespace Template; + +class FooForms { + + static public function init(\Template $tmpl=null) { + $tmpl = $tmpl?:\Template::instance(); + $tmpl->extend('input','\Template\Tags\Input::render'); + $tmpl->extend('select','\Template\Tags\Select::render'); + } + +} \ No newline at end of file diff --git a/lib/template/taghandler.php b/lib/template/taghandler.php new file mode 100644 index 0000000..eb0e3f5 --- /dev/null +++ b/lib/template/taghandler.php @@ -0,0 +1,110 @@ + + + @version 0.1.1 + @date: 07.03.14 + **/ + +namespace Template; + +abstract class TagHandler extends \Prefab { + + /** @var \Template */ + protected $template; + + protected static $engine; + + + public function __construct() { + $this->template = ($tmpl=static::$engine) + ? $tmpl::instance() : \Template::instance(); + } + + static public function setEngine(\Template $obj) { + static::$engine = $obj; + } + + /** + * build tag string + * @param $attr + * @param $content + * @return string + */ + abstract function build($attr,$content); + + + /** + * incoming call to render the given node + * @param $node + * @return string + */ + static public function render($node) { + $attr = $node['@attrib']; + unset($node['@attrib']); + + /** @var TagHandler $handler */ + $handler = new static; + $content = (isset($node[0])) ? $handler->template->build($node) : ''; + + return $handler->build($attr,$content); + } + + + /** + * general bypass for unhandled tag attributes + * @param array $params + * @return string + */ + protected function resolveParams(array $params) { + $out = ''; + foreach ($params as $key => $value) { + // build dynamic tokens + if (preg_match('/{{(.+?)}}/s', $value)) + $value = $this->template->build($value); + if (preg_match('/{{(.+?)}}/s', $key)) + $key = $this->template->build($key); + // inline token + if (is_numeric($key)) + $out .= ' '.$value; + // value-less parameter + elseif ($value == NULL) + $out .= ' '.$key; + // key-value parameter + else + $out .= ' '.$key.'="'.$value.'"'; + } + return $out; + } + + + /** + * modify a token to fit into another token + * @param $val + * @return string + */ + protected function makeInjectable($val) { + if (preg_match('/({{.+?}})/s', $val)) { + $split = preg_split('/({{.+?}})/s', $val, -1, + PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + foreach ($split as &$part) { + if (substr($part, 0, 2) == '{{') { + $part = $this->template->token($part); + } else + $part = "'".$part."'"; + } + $val = implode('.', $split); + } else { + $val = "'".$val."'"; + } + return $val; + } +} \ No newline at end of file diff --git a/lib/template/tags/image.php b/lib/template/tags/image.php new file mode 100644 index 0000000..9968464 --- /dev/null +++ b/lib/template/tags/image.php @@ -0,0 +1,67 @@ +null, + 'height'=>null, + 'crop'=>false, + 'enlarge'=>false, + 'quality'=>75, + ); + // merge into defaults + $opt = array_intersect_key($attr + $opt, $opt); + // get dynamic path + $path = $this->template->token($attr['src']); + // clean up attributes + $attr=array_diff_key($attr,$opt); + + /** @var \Base $f3 */ + $f3 = \Base::instance(); + $opt = var_export($opt,true); + + $out='resize('.$path.','.$opt.'); ?>' + .'resolveParams($attr).' />'; + + } else + // just forward / bypass further processing + $out = 'resolveParams($attr).' />'; + + return $out; + } + + function resize($path,$opt) { + $f3 = \Base::instance(); + $hash = $f3->hash($path.$f3->serialize($opt)); + $new_file_name = $hash.'.jpg'; + $dst_path = $f3->get('UPLOADS').'cache/'; + $path = explode('/', $path); + $file = array_pop($path); + if (!is_dir($dst_path)) + mkdir($dst_path,0775); + if (!file_exists($dst_path.$new_file_name)) { + $imgObj = new \Image($file, false, implode('/',$path).'/'); + $ow = $imgObj->width(); + $oh = $imgObj->height(); + if (!$opt['width']) + $opt['width'] = round(($opt['height']/$oh)*$ow); + if (!$opt['height']) + $opt['height'] = round(($opt['width']/$ow)*$oh); + $imgObj->resize((int)$opt['width'], (int)$opt['height'], $opt['crop'], $opt['enlarge']); + $file_data = $imgObj->dump('jpeg', null, $opt['quality']); + $f3->write($dst_path.$new_file_name, $file_data); + } + return $dst_path.$new_file_name; + } +} \ No newline at end of file diff --git a/lib/template/tags/input.php b/lib/template/tags/input.php new file mode 100644 index 0000000..693f832 --- /dev/null +++ b/lib/template/tags/input.php @@ -0,0 +1,50 @@ +makeInjectable($attr['name']); + if ($attr['type'] == 'checkbox') { + $value = $this->makeInjectable(isset($attr['value'])?$attr['value']:'on'); + // basic match + $str = '(isset(@POST['.$name.']) && @POST['.$name.']=='.$value.')'; + // dynamic array match + if (preg_match('/({{.+?}})/s', $attr['name'])) { + $str.= ' || (isset(@POST[substr('.$name.',0,-2)]) && is_array(@POST[substr('.$name.',0,-2)])'. + ' && in_array('.$value.',@POST[substr('.$name.',0,-2)]))'; + } + // static array match + elseif (preg_match('/(\[\])/s', $attr['name'])) { + $name=substr($attr['name'],0,-2); + $str='(isset(@POST['.$name.']) && is_array(@POST['.$name.'])'. + ' && in_array('.$value.',@POST['.$name.']))'; + } + $str = '{{'.$str.'?\'checked="checked"\':\'\'}}'; + $attr[] = $this->template->build($str); + + } elseif ($attr['type'] == 'radio' && isset($attr['value'])) { + $value = $this->makeInjectable(isset($attr['value'])?$attr['value']:'on'); + $attr[] = $this->template->build('{{ isset(@POST['.$name.']) && '. + '@POST['.$attr['name'].']=='.$value.'?\'checked="checked"\':\'\'}}'); + } elseif($attr['type'] != 'password' && !array_key_exists('value',$attr)) { + // all other types, except password fields + $ar_name = preg_replace('/\'*(\w+)(\[.*\])\'*/i','[$1]$2',$name,-1,$i); + $name = $i ? $ar_name : '['.$name.']'; + $attr['value'] = $this->template->build('{{ isset(@POST'.$name.')?@POST'.$name.':\'\'}}'); + } + } + // resolve all other / unhandled tag attributes + $attr = $this->resolveParams($attr); + // create element and return + return ''; + } +} \ No newline at end of file diff --git a/lib/template/tags/select.php b/lib/template/tags/select.php new file mode 100644 index 0000000..9796219 --- /dev/null +++ b/lib/template/tags/select.php @@ -0,0 +1,31 @@ +template->token($attr['group']); + $name = $this->makeInjectable($attr['name']); + $ar_name = preg_replace('/\'*(\w+)(\[.*\])\'*/i','[$1]$2',$name,-1,$i); + $name = $i ? $ar_name : '['.$name.']'; + $content .= ' $val) {?>'. + $this->template->build(''). + ''; + unset($attr['group']); + } + // resolve all other / unhandled tag attributes + $attr = $this->resolveParams($attr); + // create element and return + return ''.$content.''; + } +} \ No newline at end of file diff --git a/lib/test.php b/lib/test.php index a11b3cb..05e6e6c 100644 --- a/lib/test.php +++ b/lib/test.php @@ -32,7 +32,9 @@ class Test { protected //! Test results - $data=array(); + $data=array(), + //! Success indicator + $passed=TRUE; /** * Return test results @@ -42,6 +44,14 @@ function results() { return $this->data; } + /** + * Return FALSE if at least one test case fails + * @return bool + **/ + function passed() { + return $this->passed; + } + /** * Evaluate condition and save test result * @return object @@ -60,6 +70,8 @@ function expect($cond,$text=NULL) { } $this->data[]=$data; } + if (!$out && $this->passed) + $this->passed=FALSE; return $this; } diff --git a/lib/web.php b/lib/web.php index b5f76c0..e9dda75 100644 --- a/lib/web.php +++ b/lib/web.php @@ -418,7 +418,7 @@ function engine($arg='curl') { foreach ($flags as $key=>$val) if ($val) return $this->wrapper=$key; - user_error(E_Request); + user_error(E_Request,E_USER_ERROR); } /** @@ -432,7 +432,7 @@ function subst(array &$old,$new) { $new=array($new); foreach ($new as $hdr) { $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', - $old,PREG_GREP_INVERT); + $old,PREG_GREP_INVERT); array_push($old,$hdr); } } @@ -537,7 +537,7 @@ function request($url,array $options=NULL) { * @param $header bool * @param $path string **/ - function minify($files,$mime=NULL,$header=TRUE,$path='') { + function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { $fw=Base::instance(); if (is_string($files)) $files=$fw->split($files); @@ -546,7 +546,9 @@ function minify($files,$mime=NULL,$header=TRUE,$path='') { preg_match('/\w+$/',$files[0],$ext); $cache=Cache::instance(); $dst=''; - foreach ($fw->split($path?:$fw->get('UI').';./') as $dir) + if (!isset($path)) + $path=$fw->get('UI').';./'; + foreach ($fw->split($path,FALSE) as $dir) foreach ($files as $file) if (is_file($save=$fw->fixslashes($dir.$file))) { if ($fw->get('CACHE') && @@ -641,7 +643,7 @@ function minify($files,$mime=NULL,$header=TRUE,$path='') { if (ctype_space($src[$ptr])) { if ($ptr+1$latitude, - 'lng'=>$longitude, - 'username'=>$fw->hash($fw->get('IP')) + 'lon'=>$longitude ); + $req=$web->request( + 'http://api.openweathermap.org/data/2.5/weather?'. + http_build_query($query)); return ($req=$web->request( - 'http://ws.geonames.org/findNearByWeatherJSON?'. - http_build_query($query))) && - ($data=json_decode($req['body'],TRUE)) && - isset($data['weatherObservation'])? - $data['weatherObservation']: + 'http://api.openweathermap.org/data/2.5/weather?'. + http_build_query($query)))? + json_decode($req['body'],TRUE): FALSE; } diff --git a/ui/css/styles.css b/ui/css/styles.css index 0ee514f..64208e6 100644 --- a/ui/css/styles.css +++ b/ui/css/styles.css @@ -63,3 +63,19 @@ body { margin-right: 10px; } +.stats { + font-size: 12px; + color: #848385; +} + +.error-trace { + background: #191b1d; +} +.error-trace code { + background-color: transparent; + white-space: pre-wrap; /* CSS 3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} \ No newline at end of file diff --git a/ui/templates/error.html b/ui/templates/error.html new file mode 100644 index 0000000..442a4df --- /dev/null +++ b/ui/templates/error.html @@ -0,0 +1,15 @@ + + + +
        +
        +

        {{ @headline }}

        +
        +
        + {{ @text }} + +
        +
        {{ @trace }}
        +
        +
        +
        \ No newline at end of file diff --git a/ui/templates/layout.html b/ui/templates/layout.html index c13cd1c..4b85b04 100644 --- a/ui/templates/layout.html +++ b/ui/templates/layout.html @@ -51,26 +51,12 @@
        -
        -
        - -
        -
        -

        active Tags with count

        - - {*

        Search

        -

        Last Posts

        -

        Last Comments

        -

        Meta Pages

        *} -
        -
        -

        {{ \Base::instance()->format('Page rendered in {0} msecs / Memory usage {1} KB',round(1e3*(microtime(TRUE)-$TIME),2),round(memory_get_usage(TRUE)/1e3,1)) }}

        + + + + + +

        {{ \Base::instance()->format('Page rendered by fabulog v{0}, using {1} in {2} msecs / Memory usage {3} KB',@APP_VERSION,@CONFIG->ACTIVE_DB,round(1e3*(microtime(TRUE)-$TIME),2),round(memory_get_usage(TRUE)/1e3,1)) }}

        diff --git a/ui/templates/post_list.html b/ui/templates/post_list.html index 2f1e3b2..10f025b 100644 --- a/ui/templates/post_list.html +++ b/ui/templates/post_list.html @@ -1,35 +1,55 @@ - -

        {{ @headline }}

        -
        -
        - - - - -
        -

        {{@post.title | esc}}

        - -

        - {{date('d.m.Y',strtotime(@post.publish_date))}} - - - - {{@tag.title | esc}} - - - - - Comments: {{ @post.count_comments ?: 0 }} +

        +
        + + +

        {{ @headline }}

        +
        -

        -
        - -

        {{@post.teaser ?: substr(\Base::instance()->scrub(@text),0,350) | esc }}...

        - Read more -
        - + + + +
        +

        {{@post.title | esc}}

        + +

        + {{'{0,date}', strtotime(@post.publish_date) | format}} + + + + {{@tag.title | esc}} + + + + + Comments: {{ @post.count_comments ?: 0 }} + +

        - +
        + +

        {{@post.teaser ?: substr(\Base::instance()->scrub(@text),0,350) | esc }}...

        + Read more +
        +
        -
        \ No newline at end of file + + + +
        + +
        +

        active Tags with count

        + + {*

        Search

        +

        Last Posts

        +

        Last Comments

        +

        Meta Pages

        *} +
        +
        \ No newline at end of file diff --git a/ui/templates/post_single.html b/ui/templates/post_single.html index 432b553..8e5619f 100644 --- a/ui/templates/post_single.html +++ b/ui/templates/post_single.html @@ -1,43 +1,43 @@
        - -

        - {{date('d.m.Y',strtotime(@content.publish_date))}} - - {{@content.author.name}} - - - - - {{@tag.title | esc}} - - - -

        +
        +

        {{@post.title | esc}}

        +

        + {{ '{0,date}', strtotime(@post.publish_date) | format }} + + {{@post.author.name}} + + + + + {{@tag.title | esc}} + + + +

        +

        - -
        {{ @content.teaser }}
        + +
        {{ @post.teaser }}
        - +
        - - + +
        - {{@content.text}} + {{@post.text}}
        - + -
        \ No newline at end of file +