summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.txt32
-rw-r--r--forward.module213
2 files changed, 196 insertions, 49 deletions
diff --git a/README.txt b/README.txt
index 9fe8516..3c9967d 100644
--- a/README.txt
+++ b/README.txt
@@ -66,7 +66,7 @@ Step 5)
- 'access epostcard': allow user to send an epostcard.
- 'override email address': allow logged in user to change sender address.
- 'administer forward': allow user to configure forward.
- - 'override flood control': allow user to bypass flood control.
+ - 'override flood control': allow user to bypass flood control on send.
Note that you need to enable 'access forward' for users who should be able
to send emails using the forward module.
@@ -101,6 +101,36 @@ Step 8)
forward_recent:Block - a Views list of recently forwarded nodes or pages
forward_clickthroughs:Block - a Views list of nodes with most clicks
+
+DYNAMIC BLOCK ACCESS CONTROL
+
+The 7.x-1.3 release of the Forward module added a new security field
+for administators on the Forward configuration page named Dynamic Block
+Access Control. This field allows the administrator to control which
+permissions are used when Drupal applies access control checks to the nodes,
+comments or users listed in the Dynamic Block. Several access control
+options are available, including a bypass option. The bypass option allows
+the email recipient to possibly view node titles, comment titles, or user
+names that only privileged users should see. The bypass option should not
+normally be selected, but is provided for sites that used prior versions
+of Forward and rely on the access bypass to operate correctly.
+
+IMPORTANT: Because the default for the new field is to apply access control,
+administrators of sites that rely on the access bypass to operate correctly
+need to visit the Forward configuration page and explicitly select the bypass
+option after upgrading from versions of Forward prior to 7.x-1.3.
+
+
+CLICKTHROUGH COUNTER FLOOD CONTROL
+
+The Forward module tracks clicks from links in sent emails to determine which
+nodes get the most clickthroughs. The method used could allow someone to
+manipulate clickthrough counts via CSRF - for example, placing an image on
+a website with a src tag that points to the clickthrough counter link. The
+module uses flood control to limit the number of clickthroughs from a given
+IP address in a given time period to migitate this possibility.
+
+
CREDITS & SUPPORT
Special thanks to Jeff Miccolis of developmentseed.org for supplying the
diff --git a/forward.module b/forward.module
index cde5d44..c4af3f2 100644
--- a/forward.module
+++ b/forward.module
@@ -185,6 +185,13 @@ function forward_admin_settings($form, &$form_state) {
'#rows' => 10,
'#description' => t('This text appears if a user exceeds the flood control limit. The value of the flood control limit setting will appear in place of !number in the message presented to users'),
);
+ $form['forward_options']['forward_flood_control_clicks'] = array(
+ '#type' => 'select',
+ '#title' => t('Flood control limit for clickthrough tracking'),
+ '#default_value' => variable_get('forward_flood_control_clicks', 3),
+ '#options' => array('1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', '10' => '10'),
+ '#description' => t('How many times per minute clickthroughs will be tracked from a single IP address. This will help prevent manipulation of forward clickthrough statistics.'),
+ );
$form['forward_options']['forward_message'] = array(
'#type' => 'select',
'#title' => t('Personal messages'),
@@ -342,7 +349,7 @@ function forward_admin_settings($form, &$form_state) {
'#description' => t('If checked, Forward will use a custom view mode named "Forward" to build the node, if available (clear cache twice after enabling this option)'),
);
- // Forward Form Default Values
+ // e-PostCard Form Default Values
$form['forward_epostcard_defaults'] = array(
'#type' => 'fieldset',
@@ -391,6 +398,23 @@ function forward_admin_settings($form, &$form_state) {
'#rows' => 4,
'#description' => t('This message will be appended as a footer message to the email.'),
);
+ $form['forward_epostcard_defaults']['forward_epostcard_return'] = array(
+ '#type' => 'textfield',
+ '#title' => t('e-Postcard Return URL'),
+ '#default_value' => variable_get('forward_epostcard_return', ''),
+ '#size' => 40,
+ '#description' => t('URL of path to redirect users to after submitting the epostcard form.'),
+ );
+
+ // Dynamic Block settings
+
+ $form['forward_dynamic_block_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Dynamic Block Settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#description' => t('These options control whether sent emails include dynamic block content.')
+ );
$dyn_options = array(
'node' => t('Latest Blog Articles'),
'user' => t('Latest Users'),
@@ -398,22 +422,31 @@ function forward_admin_settings($form, &$form_state) {
'popular' => t('Most Popular Content'),
'none' => t('None')
);
- $form['forward_epostcard_defaults']['forward_dynamic_block'] = array(
+ $form['forward_dynamic_block_settings']['forward_dynamic_block'] = array(
'#type' => 'radios',
'#title' => t('Dynamic Block'),
'#default_value' => variable_get('forward_dynamic_block', 'none'),
'#options' => $dyn_options,
- '#description' => t('Choose the dynamic block to send with these emails'),
+ '#description' => t('Choose the dynamic block to send with these emails. The Most Popular Content block requires the Statistics module to be enabled with the "count content views" option selected.'),
'#required' => TRUE,
'#attributes' => NULL,
);
- $form['forward_epostcard_defaults']['forward_epostcard_return'] = array(
- '#type' => 'textfield',
- '#title' => t('e-Postcard Return URL'),
- '#default_value' => variable_get('forward_epostcard_return', ''),
- '#size' => 40,
- '#description' => t('URL of path to redirect users to after submitting the epostcard form.'),
+ $acl_options = array(
+ 'recipient' => t('If one email address is entered in the Send To field and it corresponds to an active Drupal account, use the permissions of that account. Otherwise use anonymous visitor permissions. <em>(default)</em>'),
+ 'anonymous' => t('Always use the anonymous visitor permissions.'),
+ 'sender' => t('Use the permissions of the person sending the email.'),
+ 'none' => t('Bypass access control. This mimics the behavior prior to release 7.x-1.3. <em>Warning: selecting this option has security implications.</em>'),
+ );
+ $form['forward_dynamic_block_settings']['forward_block_access_control'] = array(
+ '#type' => 'radios',
+ '#title' => t('Dynamic Block Access Control'),
+ '#default_value' => variable_get('forward_block_access_control', 'recipient'),
+ '#options' => $acl_options,
+ '#description' => t('Select the type of access control to apply to the dynamic block. This determines which content will be included in the block. Most sites should use the default.'),
+ '#required' => TRUE,
+ '#attributes' => NULL,
);
+
$form['#submit'][] = 'forward_admin_settings_submit';
return system_settings_form($form);
@@ -445,27 +478,32 @@ function forward_tracker() {
$form_state['values']['path'] = drupal_get_normal_path($_GET['path']);
$args = explode('/', $form_state['values']['path']);
- if (($args[0] == 'node') && (!empty($args[1])) && (is_numeric($args[1]))) {
- $nid = $args[1];
- db_update('forward_statistics')
- ->expression('clickthrough_count', 'clickthrough_count + 1')
- ->condition('nid', $nid)
- ->execute();
- }
-
if ($form_state['values']['path'] == variable_get('site_frontpage', 'node')) {
$form_state['values']['path'] = '<front>';
}
- $id = db_insert('forward_log')
- ->fields(array(
- 'path' => $form_state['values']['path'],
- 'type' => 'REF',
- 'timestamp' => REQUEST_TIME,
- 'uid' => $user->uid,
- 'hostname' => ip_address(),
- ))
- ->execute();
+ // Flood control - only allow a certain number of tracking events per minute per IP address
+ if (flood_is_allowed('forward_tracker', variable_get('forward_flood_control_clicks', 3), 60)) {
+ if (($args[0] == 'node') && (!empty($args[1])) && (is_numeric($args[1]))) {
+ $nid = $args[1];
+ db_update('forward_statistics')
+ ->expression('clickthrough_count', 'clickthrough_count + 1')
+ ->condition('nid', $nid)
+ ->execute();
+ }
+
+ $id = db_insert('forward_log')
+ ->fields(array(
+ 'path' => $form_state['values']['path'],
+ 'type' => 'REF',
+ 'timestamp' => REQUEST_TIME,
+ 'uid' => $user->uid,
+ 'hostname' => ip_address(),
+ ))
+ ->execute();
+ }
+
+ flood_register_event('forward_tracker', 60);
if (!url_is_external($form_state['values']['path'])) {
drupal_goto(drupal_get_path_alias($form_state['values']['path']));
@@ -760,38 +798,102 @@ function forward_form_validate($form, &$form_state) {
*/
function forward_form_submit($form, &$form_state) {
global $base_url, $user;
- $dynamic_content = "";
+ $dynamic_content = '';
+
+ // Access control:
+
+ // Possibly impersonate another user depending on dynamic block configuration settings
+ $access_control = variable_get('forward_block_access_control', 'recipient');
+ $switch_user = ($access_control == 'recipient') || ($access_control == 'anonymous');
+ $impersonate_user = ((variable_get('forward_dynamic_block', 'none') != 'none') && $switch_user);
+
+ if ($impersonate_user) {
+ $original_user = $user;
+ $old_state = drupal_save_session();
+ drupal_save_session(FALSE);
+
+ if ($access_control == 'recipient') {
+ $account = user_load_by_mail(trim($form_state['values']['recipients']));
+ // Fall back to anonymous user if recipient is not a valid account
+ $user = (isset($account->status) && ($account->status == 1)) ? $account : drupal_anonymous_user();
+ } else {
+ $user = drupal_anonymous_user();
+ }
+ }
// Compose the body:
// Note how the form values are accessed the same way they were accessed in the validate function
-
//If selected assemble dynamic footer block.
switch (variable_get('forward_dynamic_block', '')) {
case 'node':
- $dynamic_content = '<h3>' . t('Recent blog posts') . '</h3>';
- $query = "SELECT n.nid, n.title FROM {node} n WHERE n.type = 'blog' AND n.status = 1 ORDER BY n.created DESC";
- $dynamic_content .= forward_top5_list($query, $base_url, 'blog');
+ if (module_exists('blog')) {
+ $dynamic_content_header = '<h3>' . t('Recent blog posts') . '</h3>';
+ $query = db_select('node', 'n');
+ $query->fields('n', array('nid', 'title'));
+ $query->condition('n.type', 'blog');
+ $query->condition('n.status', 1);
+ $query->orderBy('n.created', 'DESC');
+ if (variable_get('forward_block_access_control', 'recipient') != 'none') {
+ $query->addTag('node_access');
+ }
+ $dynamic_content = forward_top5_list($query, $base_url, 'blog');
+ }
break;
case 'user':
- $dynamic_content = '<h3>' . t("Who's new") . '</h3>';
- $query = 'SELECT u.uid, u.name FROM {users} u WHERE status <> 0 ORDER BY uid DESC';
- $dynamic_content .= forward_top5_list($query, $base_url, 'user');
+ if ((variable_get('forward_block_access_control', 'recipient') != 'none') && user_access('access user profiles')) {
+ $dynamic_content_header = '<h3>' . t("Who's new") . '</h3>';
+ $query = db_select('users', 'u');
+ $query->fields('u', array('uid', 'name'));
+ $query->condition('u.status', 0, '<>');
+ $query->orderBy('u.uid', 'DESC');
+ $dynamic_content = forward_top5_list($query, $base_url, 'user');
+ }
break;
case 'comment':
- $dynamic_content = '<h3>' . t('Recent comments') . '</h3>';
- $query = 'SELECT c.nid, c.cid, c.subject FROM {comment} c WHERE c.status = 1 ORDER BY c.created DESC';
- $dynamic_content .= forward_top5_list($query, $base_url, 'comment');
+ if (module_exists('comment')) {
+ $dynamic_content_header = '<h3>' . t('Recent comments') . '</h3>';
+ $query = db_select('comment', 'c');
+ $query->fields('c', array('nid', 'cid', 'subject'));
+ $query->condition('c.status', 1);
+ $query->orderBy('c.created', 'DESC');
+ if (variable_get('forward_block_access_control', 'recipient') != 'none') {
+ $query->addTag('node_access');
+ }
+ $dynamic_content = forward_top5_list($query, $base_url, 'comment');
+ }
break;
case 'popular':
- $dynamic_content = '<h3>' . t('Most Popular Content') . '</h3>';
- $query = "SELECT n.nid, n.title FROM {node_counter} s INNER JOIN {node} n ON s.nid = n.nid WHERE s.totalcount > 0 AND n.status = 1 ORDER BY s.totalcount DESC";
- $dynamic_content .= forward_top5_list($query, $base_url, 'blog');
+ if (module_exists('statistics')) {
+ $dynamic_content_header = '<h3>' . t('Most Popular Content') . '</h3>';
+ $query = db_select('node_counter', 's');
+ $query->join('node', 'n', 's.nid = n.nid');
+ $query->fields('n', array('nid', 'title'));
+ $query->condition('s.totalcount', 0, '>');
+ $query->condition('n.status', 1);
+ $query->orderBy('s.totalcount', 'DESC');
+ if (variable_get('forward_block_access_control', 'recipient') != 'none') {
+ $query->addTag('node_access');
+ }
+ $dynamic_content = forward_top5_list($query, $base_url, 'blog');
+ }
break;
}
+ // Only include header for non-empty dynamic block
+ if ($dynamic_content) {
+ $dynamic_content = $dynamic_content_header . $dynamic_content;
+ }
+
+ // Restore user if impersonating someone else during dynamic block build
+ if ($impersonate_user) {
+ $user = $original_user;
+ drupal_save_session($old_state);
+ }
+
+ // Send email of appropruate type based on module configuration
if ((!$form_state['values']['path']) || ($form_state['values']['path'] == 'epostcard')) {
$emailtype = 'epostcard';
$content = '';
@@ -1385,16 +1487,27 @@ function forward_block_view($delta) {
case 'stats':
if (user_access('access content')) {
$block = array();
+
+ $query = db_select('forward_statistics', 'f');
+ $query->leftJoin('node', 'n', 'f.nid = n.nid');
+ $query->fields('f');
+ $query->fields('n', array('nid', 'title'));
+ $query->range(0, 5);
+ $query->addTag('node_access');
+
switch (variable_get('forward_block_type', 'allTime')) {
case 'allTime':
- $query = "SELECT n.nid, n.title, f.* FROM {forward_statistics} f LEFT JOIN {node} n ON f.nid = n.nid WHERE forward_count > 0 ORDER BY f.forward_count DESC";
+ $query->condition('f.forward_count', 0, '>');
+ $query->orderBy('f.forward_count', 'DESC');
+
$block['subject'] = t("Most Emailed");
- $block['content'] = node_title_list(db_query_range($query, 0, 5));
+ $block['content'] = node_title_list($query->execute());
break;
case 'recent':
- $query = "SELECT n.nid, n.title, f.* FROM {forward_statistics} f LEFT JOIN {node} n ON f.nid = n.nid ORDER BY f.last_forward_timestamp DESC";
+ $query->orderBy('f.last_forward_timestamp', 'DESC');
+
$block['subject'] = t("Most Recently Emailed");
- $block['content'] = node_title_list(db_query_range($query, 0, 5));
+ $block['content'] = node_title_list($query->execute());
break;
}
return $block;
@@ -1446,20 +1559,24 @@ function forward_block_view($delta) {
* Theme the top 5 list of users, nodes or comments
*/
function forward_top5_list($query, $base_url, $type) {
- $items = '<ul>';
- $result = db_query_range($query, 0, 5);
+ $items = '';
+ $query->range(0, 5);
+ $result = $query->execute();
foreach ($result as $item) {
if ($type == 'user') {
$items .= '<li>' . l($item->name, 'user/' . $item->uid, array('absolute' => TRUE)) . '</li>';
}
elseif ($type == 'comment') {
- $items .= '<li>' . l($item->subject, 'node/' . $item->cid . '#comment-' . $item->cid, array('absolute' => TRUE)) . '</li>';
+ $items .= '<li>' . l($item->subject, 'node/' . $item->nid, array('absolute' => TRUE, 'fragment' => 'comment-' . $item->cid)) . '</li>';
}
else {
$items .= '<li>' . l($item->title, 'node/' . $item->nid, array('absolute' => TRUE)) . '</li>';
}
}
- return $items . '</ul>';
+ if ($items) {
+ $items = '<ul>' . $items . '</ul>';
+ }
+ return $items;
}