I am having some issues with full view caching in cakephp.
The url that I would like to cache is /posts/badge/23/size:180x150 I have been able to cache the view successfully by adding $cacheAction = "1 hour"; to the controller.
Since I do not want to cache all methods in the controller (just specific methods) I tried to use the array syntax to match the url /posts/badge/23/size:180x150
where 23 is the post ID and size is a named parameter (there are 2
variations of size).
So my question is what is the proper (if any) match to place in
cacheAction to cache all posts/badges/* Here are some examples that I
have tried:
var $cacheAction = array(
'badge/*' => '+1 hour',
'posts/badge/23/size:180x150' => '1 hour',
'badge/23/size:180x150' => '1 hour',
'posts/badge/:id/:size' => '1 hour',
'badge/:id/:size' => '1 hour',
);
None of these seem to match (or at least do not cache for some
reason). I do need to cache posts/badge//size: and trap each of the
parameters being passed.
Again, if I set $cacheAction = "1 hour"; then the cache file is
created (the file tmp/cache/views/posts_badge_23_size_180x150.php is
created)
Has anyone been able to create a cache for a dynamic url?
CakePHP's CacheHelper currently isn't capable of Routes. CacheHelper::cache(), which is responsible for parsing and checking the $cacheAction settings does not use any Router functionality, it just does some string checking.
You can use your own CacheHelper, just modify the cache() method appropriately and place the file cache.php in app/views/helpers.
This ticket is similiar to your problem, the solution posted there may help you: trac.cakephp.org/ticket/6192
On this page: Caching in the Controller it says:
Remember to use your routes in the $cacheAction if you have any.
So try creating some routes that match your URL structure and using them for cacheAction keys. Please report back if it works.
Related
I am using CakePHP in my project and I am looking for a proper way to check advanced user rights in my views.
I have several pages in which the contents depend of your rights (you can view some blocks or not, edit some infos or not, etc...)
I searched and the only way I found is to implement an Auth Helper, but I thought the best way to to that is to implement methods in my "UserController" (such as canPerformAction($action, $controller = 'default_controller')), am I wrong ? And if I'm right, how to call that methods properly ?
Thanks.
EDIT : More precisions
For example I have an action "editEventProducts" that a user can perform only if he's the event owner and if the event status is <= 2.
I check that in my controller "isAuthorized" function, works like a charm.
But I have a page called "eventDetails", form which you can perfom several actions such as this one, and I want to show the edit button, only if you can do it.
If fact what I need is the output of the "isAuthorized" function for each action that you can call, but can I properly get it from a view ?
Solution
I implemented a Auth helper who does several check such as this one, which is finally a whitelist check, depending of the status of my event, hope it will help, the code :
App::uses('AppHelper', 'View/Helper');
class AuthHelper extends AppHelper {
var $helpers = array('Session');
private $_whitelist = array(
'controller1' => array(
'events' => array(
'action1' => array(1 => true, 2 => true),
'action2' => array(1 => true, 2 => true),
'action3' => array(3 => true),
'action4' => array(6 => true)
)
),
'user' => array(
'controller1' => array(
'action1' => array(1 => true, 2 => true),
'action2' => array(1 => true, 2 => true)
)
)
);
public function canPerformAction ($action, $event_infos, $controller = 'events') {
return isset($this->_whitelist[$this->Session->read('Auth.User.role')][$controller][$action][$event_infos['Event']['state_id']]);
}
}
It sounds to me like you just want to render some parts of a view based on the permissions of the user. Well, in this case I think a helper is the right choice. The user should already have all the permissions he has loaded - except they're very fine grained and you got thousands of permissions.
Check this AuthHelper, it allows you to check if the user is logged in, for a role or a set of roles in a field. Alternatively implement your own solution to match whatever your permission system is.
Note that the helper relies on passing the user data to the view in a view variable. It can be also configured to read the data from the auth part of the session directly.
Here is the example taken from it's documentation:
if ($this->Auth->isLoggedIn()) {
echo __('Hello %s!', $this->Auth->user('username'));
}
if ($this->Auth->isMe($record['Record']['user_id']) {
// or your edit button here
echo '<h2>' . __('Your records') . '</h2>';
}
if ($this->Auth->hasRole('admin') {
echo $this->Html->link(__('delete'), array('action' => 'delete'));
}
What you need is called authorization, and is the process of granting/denying actions usually built on top of an authentication step, which maps HTTP requests to logical users.
The authorization scheme can be implemented in a number of ways, for example with simple role-based rules, where users are grouped exactly for the purpose of assigning rights, or with more complex ACL (access control lists). Both can be adopted at the same time for different parts of the system, depending on your needs.
Whatever scheme you pick, you absolutely need to query it at the beginning of your controllers actions (if applicable, you may and up with a standardized authorization filter in your AppController), because the HTTP request doesn't need to come from a previously sent HTTP page, but could be a (possibly) malicious, hand-craften one. Also, you'll likely need to adjust the UI after the user rights. Maybe you'll better start with a bunch of if statements, and then after some days of work you'll be able to identify your needs and build your libraries/helpers/blocks/whatever to avoid code duplication and easing reading the templates.
If you have predefined user permissions (like 'admin', 'moderator', 'editor', 'publisher'...) you can just read the user role and current action in the controller function isAuthorized and set it to true or false.
If you want custom permissions per user, you can store those values in the database, read them in the isAuthorized function and make your logic to determine if you should allow him or not.
My solution to this was a separate table user_permissions that was something like this:
user_id | action
where action would be `controller/action' or 'view/block' or whatever you want to save there.
I would read all values for current user in the controller and if the current controller/action was found in the array, i'd set isAuthorized to true. You can apply your logic to the blocks also.
You can call function of controller from view using
requestAction(string $url, array $options)
Or you can create your custom Helper which will do this for you!
I'm building a dynamic view (Page) that consists of multiple elements (widgets) called via $this->element('messages_unread'). Some of these elements need data that is not related to the Page model.
In real life words: my users will be able to construct their own Page by choosing from a multitude of elements ("top 5 posts", "10 unread messages", etc...)
I get the data by calling $this->requestAction(array('controller'=>'events','action'=>'archive') from within the element, the url-variables differ per element .
I'm aware of the fact that requestAction() is expensive and I plan on limiting the costs by proper caching.
The actual question:
My problem is Pagination. When I'm in the Page view and call requestAction('/events/archive') the PaginatorHelper in the Page view will be unaware of the Event model and its paginator variables and $this->Paginator->next() etc... will not work.
How can I implement proper Pagination? I've tried to set the model by calling $this->Paginator->options(array('model'=>'Event')) but that doesn't work.
Do I maybe need to return custom defined Pagination variables in the requestAction and thus construct my own?
Or is there another approach that maybe even avoids requestAction()? And keep in mind here that the requested data is unrelated to the Page.
Kind regards,
Bart
[Edit] My temporary solution but still open for comments/solutions:
In the requestedAction Event/archive, return paginator variables along with the data like this:
return array('data'=>$this->paginate(), 'paging' => $this->params['paging']);
I've tinkered a bit more and the following works for me, and the PaginationHelper works:
In the element:
// requestAction returns an array('data'=>... , 'paging'=>...)
$data = $this->requestAction(array('controller'=>'events','action'=>'archive'));
// if the 'paging' variable is populated, merge it with the already present paging variable in $this->params. This will make sure the PaginatorHelper works
if(!isset($this->params['paging'])) $this->params['paging'] = array();
$this->params['paging'] = array_merge( $this->params['paging'] , $data['paging'] );
foreach($data['events'] as $event) {
// loop through data...
}
In the Controller:
public function archive() {
$this->paginate = array(
'limit' => 10
);
if ($this->params['requested'])
return array('events'=>$this->paginate('Event'), 'paging' => $this->params['paging']);
$this->set('events', $this->paginate('Event') );
}
I've set up web services using Drupal's services module. It outputs JSON for me which I am requesting through a Backbone.js front-end application.
I'm having issues with this set-up. If I request data through Backbone.js' fetch method of a model, the model's attributes are all typed as string after fetching, while there are some attributes that should be e.g. integer.
For example:
I have enabled the user resource, which is standard available in the Drupal services module
I can request a user, e.g.:
http://mydevmachine/services/user/8
...which results in the following response (slimmed down version from the real response):
{"uid":"8","name":"itsme","mail":"me#mydomain.nl"}
What I see in the response from the web service above, all values are quoted, however uid is really not a string but an integer in the database.
If I fetch the same user in my Backbone.js model, by setting the uid field of my model to 8 (integer), then call the fetch method. After fetching the uid field is typed as 'string'.
I assume the above leads to my model ending up with a uid attribute of not integer, but string. It also happens with all other web service resources I have created, using my own entities.
I need correct typing of attributes in my model due to sorting issues using Backbone's collection sorting. I.e. sorting a collection of models using a field of type 'integer' leads to different sorting results when sorting the field with the same values although stored as a string.
I'm not sure exactly where to look:
Is the JSON format output by the Drupal services module according to standards?
Is the JSON output format configurable or overridable in the Drupal services module?
Is it perhaps possible to keep the type of a model's attribute after a fetch in Backbone.js?
Should I provide a specific implementation for Backbone's collection comparator function, which handles this situation (seems hackey)?
Should I introduce other solutions, e.g. like posted here: How can I enforce attribute types in a Backbone model? (feels too heavy).
Thanks for any help.
So I finally managed to crack this issue and I found my solution here: How to get numeric types from MySQL using PDO?. I thought I'd document the solution.
Drupal 7 uses PDO. Results fetched using PDO, using Drupal's default PDO settings result in stringified values.
In Drupal's includes/database.inc file you will find this around lines 40-50:
$connection_options['pdo'] += array(
// So we don't have to mess around with cursors and unbuffered queries by default.
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE,
);
The statement here that MySQL's prepared statements skip the query cache is not entirely true, as can be found here: http://dev.mysql.com/doc/refman/5.1/en/query-cache-operation.html. It states MySQL > 5.1.17 prepared statements use the query cache under certain conditions.
I used the info from the other stack overflow question/answers to override the PDO settings for the database connection in Drupal's sites/default/settings.php (please note I only did this for the database I was querying, which is different than Drupal's own database):
'database_name' =>
array (
'default' =>
array (
'database' => 'database_name',
'username' => 'user_name',
'password' => 'user_pass',
'host' => 'localhost',
'port' => '',
'driver' => 'mysql',
'prefix' => '',
'pdo' => array(
PDO::ATTR_STRINGIFY_FETCHES => FALSE,
PDO::ATTR_EMULATE_PREPARES => FALSE
),
),
),
This resulted in integers being integers. Floats/decimals are incorrectly returned by PDO still, but this is different issue. At least my problems are solved now.
Okay, this will require some setup:
I'm working on a method of using nice post title "slugs" in the URL's of my cakePHP powered blog.
For example: /blog/post-title-here instead of /blog/view_post/123.
Since I'm obviously not going to write a new method for every post, I'm trying to be slick and use CakePHP callbacks to emulate the behavior of PHP 5's __call() magic method. For those who do not know, CakePHP's dispatcher checks to see if a method exists and throws a cakePHP error before __call() can be invoked in the controller.
What I've done so far:
In the interest of full disclosure ('cause I have no Idea why I'm having a problem) I've got two routes:
Router::connect('/blog/:action/*', array('controller' => 'blog_posts'));
Router::connect('/blog/*', array('controller' => 'blog_posts'));
These set up an alias for the BlogPostsController so that my url doesn't look like /blog_posts/action
Then in the BlogPostsController:
public function beforeFilter() {
parent::beforeFilter();
if (!in_array($this->params['action'], $this->methods)) {
$this->setAction('single_post', $this->params['action']);
}
}
public function single_post($slug = NULL) {
$post = $this->BlogPost->get_post_by_slug($slug);
$this->set('post', $post);
//$this->render('single_post');
}
The beforeFilter catches actions that do not exist and passes them to my single_post method. single_post grabs the data from the model, and sets a variable $post for the view.
There's also an index method that displays the 10 most recent posts.
Here's the confounding part:
You'll notice that there is a $this->render method that is commented-out above.
When I do not call $this->render('single_post'), the view renders once, but the $post variable is not set.
When I do call $this->render('single_post'), The view renders with the $post variable set, and then renders again with it not set. So in effect I get two full layouts, one after the other, in the same document. One with the content, and one without.
I've tried using a method named single_post and a method named __single_post and both have the same problem. I would prefer the end result to be a method named __single_post so that it cannot be accessed directly with the url /blog/single_post.
Also
I've not yet coded error handling for when the post does not exist (so that when people type random things in the url they don't get the single_post view). I plan on doing that after I figure out this problem.
This doesn't explicitly answer your question, but I'd just forego the whole complexity by solving the problem using only routes:
// Whitelist other public actions in BlogPostsController first,
// so they're not caught by the catch-all slug rule.
// This whitelists BlogPostsController::other() and ::actions(), so
// the URLs /blog/other/foo and /blog/actions/bar still work.
Router::connect('/blog/:action/*',
array('controller' => 'blog_posts'),
array('action' => 'other|actions'));
// Connect all URLs not matching the above, like /blog/my-frist-post,
// to BlogPostsController::single_post($slug). Optionally use RegEx to
// filter slug format.
Router::connect('/blog/:slug',
array('controller' => 'blog_posts', 'action' => 'single_post'),
array('pass' => array('slug') /*, 'slug' => 'regex for slug' */));
Note that the above routes depend on a bug fix only recently, as of the time of this writing, incorporated into Cake (see http://cakephp.lighthouseapp.com/projects/42648/tickets/1197-routing-error-when-using-regex-on-action). See the edit history of this post for a more compatible solution.
As for the single_post method being accessible directly: I won't. Since the /blog/:slug route catches all URLs that start with /blog/, it'll catch /blog/single_post and invoke BlogPostsController::single_post('single_post'). You will then try to find a post with the slug "single_post", which probably won't exist. In that case, you can throw a 404 error:
function single_post($slug) {
$post = $this->BlogPost->get_post_by_slug($slug);
if (!$post) {
$this->cakeError('error404');
}
// business as usual here
}
Error handling: done.
I have a songs controller. Within the songs controller i have a 'view' action which get's passed an id, eg
/songs/view/1
/songs/view/5
/songs/view/500
When a user visits /songs/view/1, the file is cached correctly and saved as 'songs_view_1.php'
Now for the problem, when a user hit's a different song, eg /songs/view/2, the 'songs_view_1.php' is deleted and '/songs/view/2.php' is in it's place.
The cahced files will stay there for a day if I don't visit a different url, and visiting a different action will not affect any other action's cached file.
I've tried replacing my 'cake' folder (from 1.2 to 1.2.6), but that didn't do anything. I get no error messages at all and nothing in the logs.
Here's my code, I've tried umpteen variations all ending up with the same problem.
var $helpers = array('Cache');
var $cacheAction = array(
'view/' => '+1 day'
);
Any ideas?
EDIT:
After some more testing, this code
var $cacheAction = array(
'view/1' => "1 day",
'view/2' => "1 day"
);
will cache 'view/1' or 'view/2', but delete the previous page as before. If I visit '/view/3' it will delete the cached page from before... sigh
EDIT:
Having the same issue on another server with same code...
After working hours on this, I finally figure out the reason why the cache keep being deleted, the REASON is because you had some operations that update your 'song' record in the database after you view the 'song'. For my case, I keep a column in my database called 'Hits' to store the number of hits/reads, and it updates it everytime it read the record.
Cakephp has a feature to aumotically detect changes to your database and clear the cache for you.
Try remove any operations that update your 'song' record and the cacheaction should be working properly.
An alternative is to redefine the clearcache function in your 'song' model... it will disable the function to auto-clear off the cache.. but then remember to manually clear the cache yourself when an update is performed.
function _clearCache($type = null) {
}
After working hours on this, I finally figured out the reason why the cache keeps on being deleted. The reason is because you had some operations that update your 'song' record in the database after you view the 'song'. For my case, I keep a column in my database called 'Hits' to store the number of hits/reads, and it updates it everytime it read the record.
Cakephp has a feature to automatically detect changes to your database and clear the cache for you.
Try remove any operations that update your 'song' record and the cacheaction should work properly.
After fixing it, there will be another issue. Let's say you cache many of your records, for example song/1, song/5, song/100...etc, if there is any update for any 1 of the record... all of the caches for song/1, song/5, song/100 will be deleted. This makes cacheaction useless for frequently update website.
The solution to this is to redefine the clearcache function in your 'song' model... it will disable the function to auto-clear off the cache.. so if there is any update, none of the caches will be deleted. But then remember to manually clear the cache yourself when an update is performed.
function _clearCache($type = null) {
}
to remove cache manually, you could use
#unlink(CACHE.'views'.DS.'website_songs_view_50.php');
I think that kind of caching method is depreceted. Perhaps you should use Cache:
$song = Cache::read('songs/view/'.$id, 'cache_time');
if(empty($song)){
$song = $this->Song->findById($id);
Cache::write('songs/view/'.$id, $song, 'cache_time');
}
cache_time is a variable you define in core.php:
Cache::config('cache_time', array('engine' => 'File', 'duration' => 60*60*24));
Hope it helps.
Check some setting in the config.php file. Do you have the following setting enabled?
Configure::write('debug', 0);
//Configure::write('Cache.disable', true);
Configure::write('Cache.check', true);
Cache::config('default', array('engine' => 'File'));