If you have 'administer users' permission then you can access a "contact form" for user 0 on any Drupal site at user/0/contact.

In this code

function _contact_personal_tab_access($account) {
  global $user;
  if (!isset($account->contact)) {
    $account->contact = FALSE;
  }
  return
    $account && $user->uid &&
    (
      ($user->uid != $account->uid && $account->contact) ||
      user_access('administer users')
    );
}

it should probably say

  return
    $account->uid && $user->uid &&
    ...

I imagine that the uid property was omitted in error; $account will always evaluate to TRUE in this context.

This fix will also stop the Views handler which generates the contact user link from showing the link for user 0 (if the current user has 'administer users' permission). See also #523222: Access check is done twice on link to a user's contact form, and incorrectly, which is where I tripped over this problem.

I've tested this tiny fix on 6.12. Let's see what testbot makes of it.

Comments

gpk’s picture

StatusFileSize
new419 bytes

Now with patch.

zoen’s picture

This patch totally works. Before patching, there's a contact form for the anonymous user at user/0/contact, which, if you actually submit it, delivers you to the "Access denied" page at user/0 with a message proudly (and falsely) stating that "Your message has been sent". After patching, user/0/contact gives you an "Access denied" page, no matter what your permissions are.

BUT. I have an issue with "Access denied" messages appearing where, really, we're not denied access to anything; there's just nothing there. I think it would be better if we could display a "Page not found" error at user/0 and user/0/contact instead. Thoughts?

Status: Needs review » Needs work

The last submitted patch failed testing.

gpk’s picture

Status: Needs work » Needs review

>really, we're not denied access to anything; there's just nothing there. I think it would be better if we could display a "Page not found" error at user/0 and user/0/contact instead. Thoughts?

IMO it's not worth trying to do this any better/differently. contact_menu() defines a callback for each user/%user/contact, where %user is a valid uid. So in a sense there really is a page defined at user/0/contact..! Perhaps there shouldnt be, but in that case there shouldn't really be a page at user/0 either. I think having access denied for both is good enough, and is at least consistent. Drupal (Views) doesn't automatically generate links to user/0, and with this fix, won't generate links to user/0/contact either. If people navigate there manually or create their own links to these URLs then that's their problem!

catch’s picture

I think it's fine to issue a 403, but can we add a test to visit the path and assertResponse() that it's actually denied? Should be a two line change to contact.test

gpk’s picture

StatusFileSize
new1.43 KB

Thanks catch, this patch adds the 2 lines and also a few more for another test, to test it more thoroughly.

gpk’s picture

It also occurred to me when looking at the tests that the 'administer site-wide contact form' permission should be 'administer contact forms', since the settings tab contains settings for the personal contact forms. Grateful for any thoughts on whether the permission should be renamed (in which case presumably an update would be needed to preserve existing permissions granted to roles), or just fudge it using the permission's title and description http://api.drupal.org/api/function/contact_permission/7. (In a separate issue.)

[update] see #573510: Usability: contact module permissions need clarifying

damien tournoud’s picture

Given that this is pretty common requirement, I suggest we add a %user_authenticated placeholder, and the corresponding loading function:

function user_authenticated_load($uid) {
  $user = user_load($uid);
  if (!empty($user) && $user->uid > 0) {
    return $user;
  }
  else {
    return FALSE;
  }
}
damien tournoud’s picture

Status: Needs review » Needs work
catch’s picture

I like Damien's suggestion, could clean up a lot of things elsewhere.

gpk’s picture

OK chaps, just a thought about the name of the placeholder. In terms of user roles and permissions, an authenticated user is one whose account is active, i.e. they have not been blocked and if the "Require e-mail verification when a visitor creates an account" checkbox is checked then they will also have verified their email address.

In this context we are talking about user accounts which may not all be fully authenticated. Perhaps %user_registered would be clearer? Or even just %user_not_anonymous??!!?? Thoughts??

catch’s picture

I like %user_registered

gpk’s picture

Ah, I was just coming round to %user_not_anonymous, which is hard to misconstrue. Casting vote anyone ...?

zoen’s picture

I like %user_not_anonymous, too.

gpk’s picture

Status: Needs work » Needs review
StatusFileSize
new1.88 KB

Here's a patch that just applies the new placeholder to the personal contact form tab, for tesbot's verdict.

dave reid’s picture

Status: Needs review » Needs work

Please just check $account->uid instead of $account->uid > 0. Looking good.

gpk’s picture

Status: Needs work » Needs review
StatusFileSize
new2.84 KB

Thanks Dave, I would probably have said the same but Damien suggested $account->uid > 0 at #8. Not sure why!

Also he notes that "this is a pretty common requirement". I guess other places would also benefit from %user_not_anonymous. I assume they should go into a separate patch?

Have also added back 2 tests. The first assertion is not really anything to do with this issue any more but should be in there anyway and was in #15.

dave reid’s picture

Status: Needs review » Needs work
+++ modules/contact/contact.test	Wed Aug 05 10:01:52 2009
@@ -380,5 +380,19 @@ class ContactPersonalTestCase extends Dr
+    // Personal contact form tests for a user administrator.
+    $this->drupalLogout();
+    $admin_user2 = $this->drupalCreateUser(array('administer users'));
+
+    $this->drupalLogin($admin_user2);

Please re-use the $admin_user variable from earlier in the test because it already has the 'administer users' permission.

This review is powered by Dreditor.

gpk’s picture

Thanks Dave, unless I'm mistaken (which is not unlikely!) the $admin_user from earlier in this test only has 'administer site-wide contact form' permission?

dave reid’s picture

Ah crap. That was an overlap from my own patch, so you were right. :)

But I'm a big fan of re-using what we have instead of creating new things. Let's change
$admin_user = $this->drupalCreateUser(array('administer site-wide contact form'));
to
$admin_user = $this->drupalCreateUser(array('administer site-wide contact form', 'administer users'));

Then we can just re-use that user.

gpk’s picture

Yes I see what you mean, although on reflection the patch at #17 does confirm that 'administer users' permission elevates one's permissions such that one can always see other users' contact forms... Doing it as you suggest doesn't quite achieve this because it could have been the 'administer site-wide contact form' permission that gave this ability.

I wonder if that additional verification is worth the non-reuse?

dave reid’s picture

Status: Needs work » Needs review
StatusFileSize
new11.7 KB

Revsied patch with more documentation and way cleaner tests (meaning to do that for a while). gpk does this look good?

dave reid’s picture

Executive summary:
1. Adds a user_not_anonymous_load($uid) menu-load callback in user.module that will return FALSE if the anonymous user is loaded.
2. Cleans up some documentation in user.module.
3. Makes _contact_personal_tab_access() look a lot easier to read and understand, with inline comments to help!
4. Splits the existing personal contact tests into an access test, and a flood test. Adds a submitPersonalContact function and some private user objects in the test to help re-use code.
5. Adds test conditions for:
a. User trying to access their own contact form.
b. Admin user can still access contact forms that have been disabled by users
c. Users cannot contact the anonymous user (user/0/contact)
d. Make sure admins don't get locked out of personal contact forms when flood protection is triggered.

dave reid’s picture

StatusFileSize
new11.25 KB

Re-rolled for recent changes.

dave reid’s picture

Issue tags: +D7 API clean-up

Adding tag

sun’s picture

Issue tags: -D7 API clean-up
+++ modules/contact/contact.module	9 Oct 2009 03:27:05 -0000
@@ -91,7 +91,7 @@ function contact_menu() {
-  $items['user/%user/contact'] = array(
+  $items['user/%user_not_anonymous/contact'] = array(
@@ -104,20 +104,42 @@ function contact_menu() {
 function _contact_personal_tab_access(stdClass $account) {
+++ modules/user/user.module	9 Oct 2009 03:27:06 -0000
@@ -1413,12 +1413,47 @@ function user_menu() {
+function user_not_anonymous_load($uid) {

Given this menu access callback, I do not really understand why we need the new menu argument loader. The access callback seems to handle anonymous users properly already...?

+++ modules/contact/contact.module	9 Oct 2009 03:27:05 -0000
@@ -104,20 +104,42 @@ function contact_menu() {
-/**
- * Determine permission to a user's personal contact form.
- */
+ /**
+  * Menu access callback for a user's personal contact form.

Wrong indentation of entire PHPDoc block here.

+++ modules/contact/contact.test	9 Oct 2009 03:27:05 -0000
@@ -284,78 +288,99 @@ class ContactPersonalTestCase extends Dr
+  protected function submitPersonalContact($account, array $message = array()) {
+    $message += array(
+      'subject' => $this->randomName(16),
+      'message' => $this->randomName(64),
+   );

Wrong indentation of closing parenthesis here.

+++ modules/user/user.module	9 Oct 2009 03:27:06 -0000
@@ -1413,12 +1413,47 @@ function user_menu() {
+ * @param $uid
+ *   An optional user ID of the user to load.
+ * @return
+ *   A user object if $uid was valid or not provided, otherwise FALSE.
+ */
+function user_uid_optional_load($uid = NULL) {
+  if (isset($uid)) {
+    return user_load($uid);
+  }
+  else {
+    return $GLOBALS['user'];
+  }

@return docs do not map to the actual returned value.

+++ modules/user/user.module	9 Oct 2009 03:27:06 -0000
@@ -1413,12 +1413,47 @@ function user_menu() {
+ *   A user object if $uid was valid and not the anonymous user, otherwise

s/was/is/

I'm on crack. Are you, too?

Status: Needs review » Needs work

The last submitted patch failed testing.

dave reid’s picture

Status: Needs work » Needs review
StatusFileSize
new9.92 KB

Good point. I guess the only advantage to having a non-anonymous-user loader would be that it gives a 404 on pages where it would be uid 0 instead of a 403, but not really a big deal either way. I can live without the loader. It probably needs to have more discussion about things like included blocked users, etc.

Rerolled patch without the new menu loader and fixed coding standards.

sun’s picture

+++ modules/contact/contact.module	9 Oct 2009 17:54:05 -0000
@@ -105,19 +105,41 @@ function contact_menu() {
+  // If the contacted user has not set their contact form preference, do not
+  // allow users to content them yet.
+  if (empty($account->contact)) {
+    return FALSE;
+  }
+
+  // If above conditions have not failed, the user can access the contact form.
+  return TRUE;

"to content them" ? :)

This actually also catches the case where the to be contacted user disabled his contact form, while the comment only speaks about a missing preference.

I agree that adding a check for blocked users would be good. But aside from that, I don't see any further options to delay this patch.

+++ modules/user/user.module	9 Oct 2009 17:54:06 -0000
@@ -1413,12 +1413,31 @@ function user_menu() {
+ * @return
+ *   A fully-loaded $user object upon successful user load, FALSE if user
+ *   cannot be loaded, or the current user object if $uid was not provided.

oh wow! I actually didn't know that one can just do user_load() to load the current user. Nice docs! :)

This review is powered by Dreditor.

dave reid’s picture

StatusFileSize
new9.94 KB

We have an existing issue for blocked users in #586664: Users should not be able to contact blocked users but that's another bug report that can wait since I want this access function cleaned up for some other issues in contact.module.

sun’s picture

Status: Needs review » Reviewed & tested by the community

Looks good.

gpk’s picture

Status: Reviewed & tested by the community » Needs work

Minor improvements to comments:

+  // User administers should always have access to personal contact forms.

administrators I think...

+  // Users cannot contact themselves.

"may not" would be more accurate, especially in light of the confusion/debate over this!

+  // If the requested user has disabled their contact form, or has not set
+  // their contact form preference, do not allow users to content them.

If you haven't changed the contact preference then your account's $account->contact will contain the value of variable contact_default_status that was current when your account was created, which may well lead to your form being enabled.

+  // If above conditions have not failed, the user can access the 
+  // requested account's contact form.

I know what you mean, but I think "Otherwise the user..." or "In all other cases the user..." would be more accurate.

dave reid’s picture

Status: Needs work » Needs review
StatusFileSize
new9.79 KB

Good catches in the documentation. I guess it's pretty obvious that in the end of the function is reached, access is granted, so I just chunked out that comment and moved it to the function's @return doc.

gpk’s picture

Status: Needs review » Needs work

@26,28

I do not really understand why we need the new menu argument loader.

the only advantage to having a non-anonymous-user loader would be that it gives a 404 on pages where it would be uid 0 instead of a 403

That came from Damien's @8. I don't think it was ever concerned with 403 vs 404, but rather was intended to do away with the additional !$account->uid check in _contact_personal_tab_access(). Core has similar checks elsewhere, and I guess the idea was to roll out this loader in other places too. I agree it doesn't really belong here.

gpk’s picture

Status: Needs work » Needs review

Crossposted.

gpk’s picture

Also, I have a feeling that global $user is not necessarily the "fully loaded $user object" (certainly that used to be the case.. session handling only loaded as much of $user as was stored in the {users} table), in which case the behavior of user_uid_optional_load() is being changed...

dave reid’s picture

StatusFileSize
new9.79 KB

I see how I messed up user_uid_optional_load(). /me face palms.

gpk’s picture

Status: Needs review » Needs work
+  if (isset($uid)) {

I think we could do with a ! in there somewhere.. ;)

dave reid’s picture

Status: Needs work » Needs review
StatusFileSize
new9.79 KB

The quality of my patches has been decreasing as I've been trying to pump them out with upcoming slush freeze. I need to slow down a little bit. :(

dave reid’s picture

Issue tags: +Needs backport to D6

Adding backport tag.

sun’s picture

Status: Needs review » Reviewed & tested by the community

You can prove me wrong, but I really think that there are no issues left now ;)

dries’s picture

Status: Reviewed & tested by the community » Fixed

Looks great. Committed to CVS HEAD. Thanks.

dave reid’s picture

Version: 7.x-dev » 6.x-dev
Assigned: Unassigned » dave reid
Status: Fixed » Patch (to be ported)

Love to re-roll this for D6 since it doesn't change APIs. Thanks very much Dries!

andypost’s picture

This is a bug (user/0/contact) and should be fixed in d6 too.

dave reid’s picture

Status: Patch (to be ported) » Needs review
StatusFileSize
new2.7 KB

Backport D6 patch attached including the documentation improvements to user.module as well. Does not change any APIs so should be good.

andypost’s picture

Status: Needs review » Reviewed & tested by the community

Applies cleanly, so RTBC as straight backport

gpk’s picture

@29:
>oh wow! I actually didn't know that one can just do user_load() to load the current user. Nice docs! :)
I think you actually need user_uid_optional_load().

gábor hojtsy’s picture

Status: Reviewed & tested by the community » Fixed

Thanks, committed this to Drupal 6. I noticed that the code inside user_uid_optional_load() clearly treated the uid optional (just like the function was named), but $arg was not technically optional. Good catch.

Also, fixed "a either a" to "either a" in the code docs for this function before committing. That minimal fix might be useful to bring in D7 as well.

gpk’s picture

Version: 6.x-dev » 5.x-dev
Status: Fixed » Patch (to be ported)
Issue tags: -Needs backport to D6 +Needs backport to D5

This also afflicts 5.x, so changing tag/version accordingly.

andypost’s picture

Version: 5.x-dev » 7.x-dev
Status: Patch (to be ported) » Reviewed & tested by the community
StatusFileSize
new599 bytes

As pointed in #48 there'sa small typo, so let's back to 7 and return to 5

webchick’s picture

Version: 7.x-dev » 5.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed #50. Now back to 5.x.

dave reid’s picture

Version: 5.x-dev » 7.x-dev
Status: Patch (to be ported) » Fixed

7.0 is out tomorrow, 5.x is obsolete, won't fix, sorry.

Status: Fixed » Closed (fixed)
Issue tags: -Needs backport to D5

Automatically closed -- issue fixed for 2 weeks with no activity.