I suggest limiting this to firing when the user profile form is submitted by a particular user, profile fields have changed as a result of the submit, and hasn't been submitted in the last 15 minutes, as opposed to on each user_save() since that can be triggered by a range of non-user facing events.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ezra-g’s picture

Project: » Drupal Commons
Component: Code » Activity/status streams
Priority: Normal » Major

Moving this to the main Drupal Commons issue queue per #1812492: Consider using central issue queue for Commons projects.

mmilano’s picture

Status: Active » Needs review
FileSize
9.97 KB

The 2 challenges here were to know when the profile was updated last, and detect if there were changes. I approached this by using the standard cache table, although a custom cache table might be more appropriate.

When a user submits their profile, the form values are serialized and stored in the cache table.

The next time the user submits the profile form, the new values are compared against the old values with a string comparison of a serialized array.

Keys we don't care about for change detection (login, access, pass, etc...) are filtered out and sorted before the values are compared and stored.

Using cache_get gives us the additional benefit of knowing when the last time the cache was set. This is how the 15 minute threshold was implemented.

The last check is that the user is submitting their own form. I didn't think that activity should display if an admin was in there changing someone else's profile.

The feature was re-created and copied back to the source.

jpontani’s picture

Status: Needs review » Needs work
+  if ($cache = cache_get($cid)) {
+    $last_update = $cache->created;
+    if ($cache->data != $profile_data) {
+      $change_detected = TRUE;
+      drupal_set_message('change_detected');
+    }
+  }

You'll want to remove the DSM call. Also your message doesn't actually link to anything (no HREF set), looks like the export may have messed up the message details.

+        {
+          "value" : "\\u003Ca class=\\u0022aloha-link-text\\u0022\\u003E@{message:user:name}\\u003C\\/a\\u003E\\u0026nbsp;has an updated profile",
+          "format" : "full_html",
+          "safe_value" : "\\u003Ca class=\\u0022aloha-link-text\\u0022\\u003E@{message:user:name}\\u003C\\/a\\u003E\\u0026nbsp;has an updated profile"
+        },

Returns HTML:
<a class="aloha-link-text">@{message:user:name}</a> has an updated profile.

Other than that, looks good to me.

mmilano’s picture

Status: Needs work » Needs review
FileSize
9.77 KB

Good catch, this update fixes those 2 issues.

ezra-g’s picture

Status: Needs review » Needs work

This message never generates on a fresh installation of Commons with #4 applied, probably because $change_detected is never set to TRUE if there's no cache entry for the user having edited her profile, so $change_detected won't be set to TRUE on the first profile edit:

 if ($cache = cache_get($cid)) {
    $last_update = $cache->created;
    if ($cache->data != $profile_data) {
      $change_detected = TRUE;
      cache_set($cid, $profile_data);
    }
  }
mmilano’s picture

@ezra, that does not look like the code from the patch in #4.

if ($cache = cache_get($cid)) {
  $last_update = $cache->created;
  if ($cache->data != $profile_data) {
    $change_detected = TRUE;
  }
}
cache_set($cid, $profile_data);
jpontani’s picture

@mmilano, It would still not detect a change if the cache wasn't set initially.

if ($cache = cache_get($cid)) {
  $last_update = $cache->created;
  if ($cache->data != $profile_data) {
    $change_detected = TRUE;
  }
}
else {
  cache_set($cid, $profile_data);
  $change_detected = TRUE;
}

That would see that no cache exists, and thus a "change" would necessarily exist because they have never updated their profile previously.

Taking a look at it again, would this not also trigger on a new user's initial registration? No cache would exist because you're checking for a cache bin of: commons_up_ (no UID), depending on if the user is created prior to this submit handler at least. I haven't tried it to see, so I'm just thinking "out loud", so to speak.

mmilano’s picture

jpontani, that would only set cache if it was never set before. cache_set() definitely needs to be outside the conditions.

I'm still concerned how Ezra's code got the way it is, but I'll ignore that for now.

A business logic question is: Should the updated message generate when a user registers? (I think not)

The confirmed bug is: The message will not generate until the second time the user submits their profile.

If we move the data comparison outside of the cache check, it will fix that bug.

  $cache_data = '';
  if ($cache = cache_get($cid)) {
    $last_update = $cache->created;
    $cache_data = $cache->data;
  }
  if ($cache_data != $profile_data) {
    $change_detected = TRUE;
  }
  cache_set($cid, $profile_data);

But I need to know if the message is expected to be generated when a user registers.

Once I get that info, I'll recreate the patch.

ezra-g’s picture

@mmilano, You're correct - I accidentally quoted a modified version of the code rather than your patch - Sorry about the mistake.

This message does not need to appear when a user first registers (though a separate issue for a "User registered for site" message is a good idea).

jpontani’s picture

@mmilano, Yea that's true, just was posting quickly. Your code in #8 should remedy the issue if the cache isn't set, as per #9 should accomodate only existing users and not a user that is just registering. One quick thought to do that would be to set that specific submit handler to be executed first, and then if the uid exists, continue, otherwise skip that handler.

mmilano’s picture

@jpontani - This code is not triggered on registration, it is a submit handler attached to form id: edit_profile_user_profile_form

For registration, I think the cleanest solution would be to add a separate submit handler to the user register form. Then we wouldn't have to clutter up our user edit form handler with conditionals and the alternate message.

ezra-g’s picture

For registration, I think the cleanest solution would be to add a separate submit handler to the user register form. Then we wouldn't have to clutter up our user edit form handler with conditionals and the alternate message.

Agreed - Seems like a great separate post 3.0 feature :).

mmilano’s picture

Assigned: Unassigned » mmilano
Status: Needs work » Needs review
FileSize
9.81 KB

Here is a new patch as per #8.

ezra-g’s picture

Status: Needs review » Needs work

In functional testing of #13, with all the caches cleared, submitting the user profile edit form for the first time with no changes results in an activity stream message being created. That's probably an acceptable behavior for the first submit after a cache clear, but seems unideal given that we've introduced some non-trivial code to avoid generating messages in this scenario.

As an alternative approach, we could follow the pattern that Commons Follow uses to avoid duplicative messages: Use a simple EFQ for a message of the same type about the same user within the last 15 minutes, and don't create a new message if one has been created within that timespan.

It would be possible for users to generate a new message every 15 minutes if they wanted to, but even with #13, a determined user could simply alter the body field by one character to bypass the check.

mmilano’s picture

I don't see how in #13, the user could bypass the 15 minute check by altering a character since it would not get past the test for $last_update > $time - 900 except after a cache clear. Maybe you weren't referring to the time check and just the character difference. In that case, if you could define the additional requirements that are required for the message to trigger, we could work on a solution for that.

I think the EFQ is a good solution to avoid breaking the 15 minute check shortly after cache is cleared.

Attached is a patch with that logic vs basing the time check on when the cache was last updated.

mmilano’s picture

Status: Needs work » Needs review
ezra-g’s picture

Status: Needs review » Needs work

Maybe you weren't referring to the time check and just the character difference. In that case, if you could define the additional requirements that are required for the message to trigger, we could work on a solution for that.

Sorry I was unclear there.

My point was that we've introduced some code complexity to avoid generating a message when the profile hasn't changed, and that currently this code isn't performing as expected because I can submit the user profile form with no changes and still generate an activity stream message.
From my perspective, that's why this still needs work. The patch in #15 also has this issue.

Secondarily, I was suggesting that perhaps we don't need to attempt to prevent messages from being generated when the user profile contents haven't actually changed since it's easy for users to bypass this check as long as they only do so once every 15 minutes. However, after further thought I realized I was focusing only on malicious users attempting to generate extra messages (which we basically can't prevent) when really it seems worth keeping this check in place to prevent extra messages from being generated by well-intentioned users who save the form without changes.

So, to reiterate, I think this just needs a re-roll to prevent an activity stream messages from being generated when the form is saved without any changes for the first time (presumably, when no cache entry has been created).

ezra-g’s picture

PS, feel free to find me in #drupal-commons to discuss.

mmilano’s picture

Status: Needs work » Needs review
FileSize
10.06 KB
10.06 KB

Ok, so this patch contains the change detection logic from the patch in #4, and the EFQ method of detecting if a message was created recently from the patch in #15.

These are now the use cases where the message should logically trigger, but will not because of going with the cache architecture route:

- User updates profile information for the first time with changes.
- User updates profile information with changes after a cache clear.

I have some alternate solution ideas:

1) We no longer use the cache timestamp, so the only benefit we're getting is easy storage.

While I know using $user->data is not encouraged, using cache tables isn't exactly a golden for this either.

This would remove the failure in the cache clear use case above. The first time with changes use case might still be a little tricky.

2) Preprocess the form to get the default values, store them in $form_state['storage'] and compare them on the submit.

This would resolve both cases above, although may come with more risk. I'll make a new post with this concept in a patch.

mmilano’s picture

Oops, I attached the last patch twice. It is the same file.

This patch is a proof of concept for alternative solution idea number 2 in #19 above.

The idea is that we programmatically get and store the default $form_state['values'] array when the form loads. Then we compare it on submit.

I am not sure this is entirely sane, but was just having some fun tinkering with alternate solutions. My concerns are if it's an appropriate use of prepare and process form this way, as well as potential issues with array keys we need to remove in the $remove_keys array as users install or create other modules. I suppose they keys could be a problem with the other change detection logic as well.

ezra-g’s picture

Status: Needs review » Fixed

#20 works as expected in my functional testing - A message isn't created until the user updates a profile field value. Nice!

I got an undefined index error with

$message = message_create($message_type, array('uid' => $account->uid, 'timestamp' => $time));

so I changed to

$_SERVER['REQUEST_TIME']

, tweaked code comments and committed.

Thanks, mmilano!

jpontani’s picture

re #21, might look into switching to REQUEST_TIME constant, as per http://api.drupal.org/api/drupal/includes%21bootstrap.inc/constant/REQUE...

This differs from $_SERVER['REQUEST_TIME'], which is stored as a float since PHP 5.4.0. Float timestamps confuse most PHP functions (including date_create()).

Just a thought for future functionality.

jpontani’s picture

Status: Fixed » Needs review
FileSize
852 bytes

Attached a patch to updated to REQUEST_TIME constant.

ezra-g’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me.

ezra-g’s picture

Status: Reviewed & tested by the community » Fixed
ezra-g’s picture

Status: Fixed » Reviewed & tested by the community

Oops, I missed that this issue was open for just the followup in 23.

ezra-g’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

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