Currently, we check access using two methods:
if (!$flag->user_access($user)) {
// User has no permission to use this flag.
continue;
if (!$flag->applies_to_content_object($object)) {
// Flag does not apply to this content.
continue;
}
These two methods don't inspect the node itself. We're planning access control based on the node too (to be able, say, to forbid a user to flag his own nodes).
So we should have a method similar to node_access() and use it instead. node_access()'s signature is:
function node_access($op, $node, $account)
However, for performance reasons, we can't have this exact signature: First, to know the $op (either 'flag' or 'unflag') we need to query the DB for the status of the flag, but we only need to do this if some perliminary access checks haven't failed (e.g., the user doesn't have permission to use this flag at all); Second, to have a $node we must fetch is first, and this too may not be needed if some preceding access checks failed. So a more efficient signature would be:
function flag::access($content_id, $account, $op = NULL)
(The node will be loaded, and the flag status will be queries by the method itself, only if needed.)
This mathod will call the current two access methods, and then, once $op is known, will continue to do more more access checks.
Comments:
- We shouldn't get rid of the applies_to_content_object() method because we should be able to skip the access() method. We should probably rename that method (applies_to_content_object) to make it clearer to the programmer that it only inspects the node type and nothing more.
- I'm writing this hastily in reply to this.
| Comment | File | Size | Author |
|---|---|---|---|
| #27 | flag_access.patch | 15.1 KB | quicksketch |
| #26 | flag_access.patch | 15.11 KB | quicksketch |
| #7 | flag_access.patch | 10.73 KB | quicksketch |
| #5 | flag_access3.patch | 12.09 KB | mooffie |
| #3 | flag_access2.patch | 6.29 KB | mooffie |
Comments
Comment #1
mooffie commentedI don't mean to say that this issue, #322034, deals also with the access checks described in #285237: Ability to disallow a flag/unflag operation. No. That would entail schema and UI changes and we'll never finish it.
My intention here is to replace the existing user_access()+applies_to_content_object() with a single access().
Comment #2
Anonymous (not verified) commentedhere's a patch that doesn't change the feature set of flag module but moves all the access and validity checks into a new access method in the flag object.
Comment #3
mooffie commentedBenjamin, that's very good!
Here's a re-roll with minor changes.
- The patch was adjusted to accomodate #320271: [API] Possibility to skip user permission check, which has just got committed.
- I added some comments with pseudo code to the end of access() to demonstrate what kind of code we should expect to see there in the future. This is the rationale for this patch.
I haven't yet tested the patch, though (that's why I'm not yet marking this 'needs review', it's in the brainstorming stage yet, I think).
Nathan, what are you thoughts on this plan?
Comment #4
mooffie commentedThings to do:
1.
We should rename $content_id to $content_id_or_object because sometimes we already have an object at our hands. The method will have to do
is_object($content_id_or_object)to detect what it got.2. 'applies_to_content_id_array()' is the SQL counterpart of our access checking method. We should rename it to, say, 'array_access()'.
Comment #5
mooffie commentedHere's a patch with these things done.
Comment #6
quicksketchI haven't reviewed this code extensively, but an argument like
$content_id_or_objectis not going to be acceptable. We should make it one or the other. I'm leaning towards $content_id, since it's what we use in most other locations. If we need to the object we can always use$flag->fetch_content($content_id);.Comment #7
quicksketchHere's a re-roll, a little more fleshed out in terms of how a hook_flag_access could be implemented. I switched the $flag->access method to always accept an object (contrary to my post in #6), to help match hook_access() provided by node module. I ran into an obvious problem though, how could we check flag access when displaying in Views? Do we just not support such a feature? I'm not sure that node module does anything for this scenario either.
Comment #8
mooffie commentedFirst, I shall give an overview:
We provide two ways to do access checking:
1. At the "PHP level" (that's the $flag->access() method).
2. At the "SQL level" (that's the $flag->access_array() method).
We're mimicing Drupal itself here. Drupal too has two ways to do access checking:
1. At the "PHP level" (that's the node_access() function, and hook_access).
2. At the "SQL level" (that's the node_db_rewrite_sql hook).
The "SQL level" is used when displaying lists of nodes; it allows us not to load every node prior to determining if we have access to flag/unflag it. It's useful for views that are styled as "table" or "fields".
Of course, the comparison I make here between our module and Drupal's Node module isn't accurate. (Our $flag->access_array() doesn't really work in the SQL level: it uses PHP code.) My intention here is only to explain why we need both access() and access_array().
Views will be using $flag->access_array().
(It already does this, in the patch.)
I mentioned in #285237: Ability to disallow a flag/unflag operation that:
Note that one important thing this patch does is to pass to access_array() the flagging state for each node. Future patches (to implement the various access controls in #285237) will use this flagging state to determine if a flag/unflag action is permitted.
(For example: when a node is flagged, the "Roles that are permitted to unflag an item" setting will be consulted. When a node is unflagged, the "Roles that are permited to flag an item" setting will be consulted.)
Do you mean "How can we call hook_flag_access in access_array()"? Well, access_array() isn't really working in the SQL level, so there's no problem calling there the hook for every node. (Implementations of this hook might load the node --they'll most probably do this-- and there's an efficiency issue here. That's why I don't think hook_flag_access's signature should follow that of Drupal's Node module).
Comment #9
mooffie commented(Yes, I've noticed your hook_flag_access can either allow or disallow an operation, whereas my proposal could only disallow an operation.)
I don't think doing something merely to match an existing signature in Drupal is a good thing.
("signature" is the types and order of the parameters passed to a function. But I don't include the name of the function in this ad-hoc definition.)
For example, fetch_content() could be avoided in some scenarios, e.g. if the node is of type 'page' and the flag only applies to the 'story' type. Addmittedly, Drupal's node_load() has an internal cache, and user_load()'s lack of cache can be compensated by MySQL's query cache, and furthermore I don't have a good example for demonstrating performance loss by always doing fetch_content(). You may be right (that we should pass an object to access() and that's it.) It's just that I think we need to think about this a little bit and not do things just because Drupal's hook_access() has a certain signature.
Comment #10
quicksketchThanks for your thoughts Mooffie. I was looking at this last night and my head nearly imploded. The problem I was specifically meaning to bring up was how hook_flag_access() would work in access_array() (such as for Views).
You also found the most likely solution, that modules implementing hook_flag_access() would likely load whatever the object was in order to check it's access. The alternative, the access check would need to be provided in some other way, possibly through db_rewrite_sql or hook_views_data_alter(), and the hook would never be called at all for listings of fields.
I switched the access method to take an object mostly because 2 of 3 times we use the check we already had the object available. More than that I try to avoid functions that take 2 different kinds of data for the same parameter. We'd be telling every module that implements hook_flag_access() to always check is_object() before using the data.
Comment #11
mooffie commentedI think that hook_flag_access() should get an array of content IDs. Programmers wishing to write efficient code would base their code mostly on DB queries. Lazier programmers would do a loop that fetch_content()'s each node. We'll leave this choice to the programmer.
It's worth iterating that the purpose of access_array() is only to make "table" views not load the nodes when they show the flag/unflag link.
No, things aren't that complicated. For example, if we'd have a "Users can flag only content they have created" setting, then we need our access_array() to SELECT the UID column as well, and then inspect it.
Comment #12
mitchell commented@moofie: in irc, quicksketch said your patch would allow for hook_flag_access() to limit flagging to users who have node_access privelages for view, edit, or delete of a node.
If you could do this, then and easy way to setup an access control list is to use the userreference and nodecccess_userreference modules, and base permissions off of node_access.
Comment #13
quicksketchPerhaps a point of compromise here between the array of IDs versus single objects is to simply offer both methods as hooks. Since flag implements two approaches to access, doesn't it make sense that contributed solutions follow the same pattern? So we could implement two hooks: hook_flag_access() and hook_flag_access_array(). The first would receive the full object and be used in scenarios such as node or comment links, while the second approach would receive an array of IDs for use in Views. Then we get consistency in our access function parameters and modules implementing the hooks can be efficient as needed.
Comment #14
phdhiren commentedany plan to include this in stable version?
Comment #15
mooffie commented(I'll comment shortly.)Oops, I replied in the wrong issue.
But, I also wanted to comment in this issue.
Instead of doing the whole plan in one step, we could, as a first step, just commit a flag::access() method. A very very minimal one (one that doesn't call further hooks). We can postpone deciding on the other issues. Having a flag::access() for awhile perhaps will make us a bit wiser (and make our code more elegant).
Comment #16
quicksketchI agree having a flag::access() method will make our code a bit cleaner. I'll see if I can put together a starting patch for this.
As you may have noticed I started driving hard for a 1.0 stable release fixing up a bunch of the current issues. I'd like to save the addition of hooks for the next release, since I don't want to rush into making a public API just before a release.
Comment #17
mooffie commented(Sure, I see your last commits. Great work! To finaly get rid of all those nagging issues.)
(As for a minimal access(), it's better not to do this before '1.0'. We know the current code doesn't have bugs. It's seems needlessly risky to introduce even the most minimal code factorization right now.)
Comment #18
quicksketchAwesome. Let's finish up then and we'll come back to this after 1.0. :)
Comment #19
sunProposal:
s/hook_flag_access_array/hook_flag_access_multiple/
Comment #20
RoboPhred commentedSuscribing, watching this for attempting a patch for #285237: Ability to disallow a flag/unflag operation
Comment #21
johnnytyranno commentedSubscribe. The day that this is in a working version of Flag I will celebrate and hail you all as heroes!
Comment #22
gunzip commentedsubscribe
Comment #23
mitchell commentedMoving
Comment #24
crea commentedSubscribing. When this will be implemented finally, it will make Trashbin module obsolete.
Comment #25
Kane commentedSubscribing
Looking forward for the possibility to allow users flag only their own content.
Comment #26
quicksketchHere's an updated patch for the new 2.x version. It makes the following changes:
- Calls to $flag->access() now use the $content_id variable instead of entire objects for the sake of efficiency (as recommended by mooffie some time ago).
- Renamed $flag->access_array() to $flag->access_multiple() (as suggested by sun) which makes us consistent with all the *_multiple hooks in Drupal 7.
- The $account argument is now passed into $flag->access_multiple() to make it work the same as $flag->access().
- Added hook_flag_access_multiple() so that 3rd party modules can check access against multiple items.
Even though there's no UI yet for testing this functionality, I found a bad usability problem when using the AJAX links. If the user has access to Flag but not to Unflag, the AJAX link still displays for the Unflag action. Clicking the link then causes a Javascript message telling the user they don't have access to flag (or unflag) that item. So while everything is working properly, it causes a usability problem that we'll need to address at some point (likely with yet another text field option for an "Access denied text" for flag and unflag).
Since we've opened the 2.x branch for development, I'd be happy to get what we have in committed and fix such details in a followup.
Comment #27
quicksketchSlightly revised patch to make $flag the first parameter in both hook_flag_access() and hook_flag_access_multiple().
I went ahead and committed this patch since it's been a long time coming. We can fix the UI problems and actually use this functionality in #285237: Ability to disallow a flag/unflag operation.
Comment #28
crea commentedwooohoo!! finally! Nate you rock man.