Index: includes/flag.views_default.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/includes/Attic/flag.views_default.inc,v retrieving revision 1.1.2.5 diff -u -r1.1.2.5 flag.views_default.inc --- includes/flag.views_default.inc 22 Feb 2009 22:20:39 -0000 1.1.2.5 +++ includes/flag.views_default.inc 28 Sep 2009 01:58:10 -0000 @@ -103,7 +103,7 @@ $access = array( 'type' => 'role', - 'role' => drupal_map_assoc($flag->roles), + 'role' => drupal_map_assoc($flag->roles['flag']), 'perm' => '', ); Index: includes/flag.admin.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/includes/Attic/flag.admin.inc,v retrieving revision 1.1.4.2.2.5 diff -u -r1.1.4.2.2.5 flag.admin.inc --- includes/flag.admin.inc 27 Sep 2009 22:59:01 -0000 1.1.4.2.2.5 +++ includes/flag.admin.inc 28 Sep 2009 01:58:10 -0000 @@ -28,11 +28,11 @@ 'flags_delete' => array('title' => t('delete'), 'href' => "admin/build/flags/delete/". $flag->name), )); - $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles)); + $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles['flag'])); $rows[] = array( $flag->name, $flag->content_type, - empty($flag->roles) ? '' . t('No roles') . '' : implode(', ', $roles), + empty($flag->roles['flag']) ? '' . t('No roles') . '' : implode(', ', $roles), $flag->types ? implode(', ', $flag->types) : '-', $flag->global ? t('Yes') : t('No'), $ops, @@ -55,7 +55,7 @@ 'flags_enable' => array('title' => t('enable'), 'href' => "admin/build/flags/edit/". $flag->name), )); - $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles)); + $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles['flag'])); $rows[] = array( $flag->name, $flag->module, @@ -201,6 +201,7 @@ '#maxlength' => 32, '#required' => TRUE, '#access' => empty($flag->locked['name']), + '#weight' => -3, ); if (!empty($flag->fid)) { @@ -215,9 +216,24 @@ '#maxlength' => 255, '#required' => TRUE, '#access' => empty($flag->locked['title']), + '#weight' => -2, ); - $form['flag_short'] = array( + $form['global'] = array( + '#type' => 'checkbox', + '#title' => t('Global flag'), + '#default_value' => $flag->global, + '#description' => t('If checked, flag is considered "global" and each node is either flagged or not. If unchecked, each user has individual flags on content.'), + '#access' => empty($flag->locked['global']), + '#weight' => -1, + ); + + $form['messages'] = array( + '#type' => 'fieldset', + '#title' => t('Messages'), + ); + + $form['messages']['flag_short'] = array( '#type' => 'textfield', '#title' => t('Flag link text'), '#default_value' => $flag->flag_short, @@ -226,7 +242,7 @@ '#access' => empty($flag->locked['flag_short']), ); - $form['flag_long'] = array( + $form['messages']['flag_long'] = array( '#type' => 'textfield', '#title' => t('Flag link description'), '#default_value' => $flag->flag_long, @@ -234,15 +250,7 @@ '#access' => empty($flag->locked['flag_long']), ); - $form['flag_confirmation'] = array( - '#type' => 'textfield', - '#title' => t('Flag confirmation message'), - '#default_value' => $flag->flag_confirmation, - '#description' => t('Message displayed if the user has clicked the "flag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to flag this content?"'), - '#access' => empty($flag->locked['flag_confirmation']), - ); - - $form['flag_message'] = array( + $form['messages']['flag_message'] = array( '#type' => 'textfield', '#title' => t('Flagged message'), '#default_value' => $flag->flag_message, @@ -250,7 +258,7 @@ '#access' => empty($flag->locked['flag_message']), ); - $form['unflag_short'] = array( + $form['messages']['unflag_short'] = array( '#type' => 'textfield', '#title' => t('Unflag link text'), '#default_value' => $flag->unflag_short, @@ -259,7 +267,7 @@ '#access' => empty($flag->locked['unflag_short']), ); - $form['unflag_long'] = array( + $form['messages']['unflag_long'] = array( '#type' => 'textfield', '#title' => t('Unflag link description'), '#default_value' => $flag->unflag_long, @@ -267,15 +275,7 @@ '#access' => empty($flag->locked['unflag_long']), ); - $form['unflag_confirmation'] = array( - '#type' => 'textfield', - '#title' => t('Unflag confirmation message'), - '#default_value' => $flag->unflag_confirmation, - '#description' => t('Message displayed if the user has clicked the "unflag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to unflag this content?"'), - '#access' => empty($flag->locked['unflag_confirmation']), - ); - - $form['unflag_message'] = array( + $form['messages']['unflag_message'] = array( '#type' => 'textfield', '#title' => t('Unflagged message'), '#default_value' => $flag->unflag_message, @@ -284,7 +284,7 @@ ); if (module_exists('token')) { - $form['token_help'] = array( + $form['messages']['token_help'] = array( '#title' => t('Token replacement'), '#type' => 'fieldset', '#description' => t('The above six options may contain the following wildcard replacements. For example, "Mark Link" could be entered as "Add [title] to your flags" or "Add this [type-name] to your flags". These wildcards will be replaced with the appropriate field from the node.') . theme('flag_token_help', $flag->get_labels_token_types()), @@ -293,38 +293,21 @@ ); } else { - $form['token_help'] = array( + $form['messages']['token_help'] = array( '#value' => '' . t('Note: You don\'t have the Token module installed. If you have it installed, and enabled, you\'ll be able to embed tokens in the six labels above.', array('@token-url' => 'http://drupal.org/project/token')) . '', ); } - $form['global'] = array( - '#type' => 'checkbox', - '#title' => t('Global flag'), - '#default_value' => $flag->global, - '#description' => t('If checked, flag is considered "global" and each node is either flagged or not. If unchecked, each user has individual flags on content.'), - '#weight' => 1, - '#access' => empty($flag->locked['global']), - ); - - $form['roles'] = array( - '#type' => 'checkboxes', - '#title' => t('Roles that may use this flag'), - '#options' => user_roles(TRUE), - '#default_value' => $flag->roles, - '#description' => t('Checking authenticated user will allow all logged-in users to flag content with this flag. Anonymous users may not flag content.'), - '#weight' => 5, - '#access' => empty($flag->locked['roles']), + $form['access'] = array( + '#type' => 'fieldset', + '#title' => t('Flag access'), + '#tree' => FALSE, + '#weight' => 10, ); - // Disabled access breaks checkboxes unless #value is hard coded. - if (!empty($flag->locked['roles'])) { - $form['roles']['#value'] = $flag->roles; - } - - $form['types'] = array( + $form['access']['types'] = array( '#type' => 'checkboxes', - '#title' => t('What nodes this flag may be used on'), + '#title' => t('Flaggable content'), '#options' => array_map('check_plain', node_get_types('names')), '#default_value' => $flag->types, '#description' => t('Check any node types that this flag may be used on. You must check at least one node type.'), @@ -335,9 +318,44 @@ // Disabled access breaks checkboxes unless #value is hard coded. if (!empty($flag->locked['types'])) { - $form['types']['#value'] = $flag->types; + $form['access']['types']['#value'] = $flag->types; } + $form['access']['roles'] = array( + '#title' => t('Roles that may use this flag'), + '#access' => empty($flag->locked['roles']), + '#description' => t('Users may only unflag content if they have access to flag the content initially. Checking authenticated user will allow access for all logged-in users. Anonymous users may not flag content.'), + '#theme' => 'flag_form_roles', + '#weight' => -2, + ); + $form['access']['roles']['flag'] = array( + '#type' => 'checkboxes', + '#options' => user_roles(TRUE), + '#default_value' => $flag->roles['flag'], + '#parents' => array('roles', 'flag'), + ); + $form['access']['roles']['unflag'] = array( + '#type' => 'checkboxes', + '#options' => user_roles(TRUE), + '#default_value' => $flag->roles['unflag'], + '#parents' => array('roles', 'unflag'), + ); + + // Disabled access breaks checkboxes unless #value is hard coded. + if (!empty($flag->locked['roles'])) { + $form['access']['roles']['#type'] = 'value'; + $form['access']['roles']['#value'] = $flag->roles; + } + + $form['access']['unflag_denied_text'] = array( + '#type' => 'textfield', + '#title' => t('Unflag not allowed text'), + '#default_value' => $flag->unflag_denied_text, + '#description' => t('If a user is allowed to flag but not unflag, this text will be displayed after flagging. Often this is the past-tense of the link text, such as "flagged".'), + '#access' => empty($flag->locked['unflag_denied_text']), + '#weight' => -1, + ); + $form['display'] = array( '#type' => 'fieldset', '#title' => t('Display options'), @@ -351,12 +369,37 @@ '#title' => t('Link type'), '#options' => _flag_link_type_options(), '#option_descriptions' => _flag_link_type_descriptions(), - '#after_build' => array('flag_expand_option_description', 'flag_check_link_types'), + '#flag_link_fields' => _flag_link_type_fields(), + '#after_build' => array('flag_expand_link_option', 'flag_check_link_types'), '#default_value' => $flag->link_type, '#weight' => 2, '#access' => empty($flag->locked['link_type']), ); + $form['link_options'] = array( + '#type' => 'fieldset', + '#title' => t('Link options'), + '#description' => t('The selected link type may require these additional settings.'), + '#attributes' => array('id' => 'link-options'), + '#weight' => 21, + ); + + $form['link_options']['flag_confirmation'] = array( + '#type' => 'textfield', + '#title' => t('Flag confirmation message'), + '#default_value' => $flag->flag_confirmation, + '#description' => t('Message displayed if the user has clicked the "flag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to flag this content?"'), + '#access' => empty($flag->locked['flag_confirmation']), + ); + + $form['link_options']['unflag_confirmation'] = array( + '#type' => 'textfield', + '#title' => t('Unflag confirmation message'), + '#default_value' => $flag->unflag_confirmation, + '#description' => t('Message displayed if the user has clicked the "unflag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to unflag this content?"'), + '#access' => empty($flag->locked['unflag_confirmation']), + ); + $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit'), @@ -410,6 +453,36 @@ } /** + * Output the access options for roles in a table. + */ +function theme_flag_form_roles($element) { + drupal_add_css(drupal_get_path('module', 'flag') . '/theme/flag-admin.css', 'module', 'all', FALSE); + drupal_add_js(drupal_get_path('module', 'flag') . '/theme/flag-admin.js', 'module', 'header', FALSE, TRUE, FALSE); + + $header = array( + array('class' => 'checkbox', 'data' => t('Flag')), + array('class' => 'checkbox', 'data' => t('Unflag')), + t('Role'), + ); + $rows = array(); + foreach (element_children($element['flag']) as $role) { + $row = array(); + $role_name = $element['flag'][$role]['#title']; + unset($element['flag'][$role]['#title']); + unset($element['unflag'][$role]['#title']); + $element['flag'][$role]['#attributes']['class'] = 'flag-access'; + $element['unflag'][$role]['#attributes']['class'] = 'unflag-access'; + $row[] = array('class' => 'checkbox', 'data' => drupal_render($element['flag'][$role])); + $row[] = array('class' => 'checkbox', 'data' => drupal_render($element['unflag'][$role])); + $row[] = $role_name; + $rows[] = $row; + } + + $element['#children'] = theme('table', $header, $rows, array('class' => 'flag-admin-table', 'id' => 'flag-roles')); + return theme('form_element', $element, $element['#children']); +} + +/** * Delete flag page. */ function flag_delete_confirm(&$form_state, $name) { @@ -442,12 +515,21 @@ /** * FormAPI after_build function to add descriptions to radio buttons. */ -function flag_expand_option_description($element) { +function flag_expand_link_option($element) { + drupal_add_js(drupal_get_path('module', 'flag') . '/theme/flag-admin.js', 'module', 'header', FALSE, TRUE, FALSE); + foreach (element_children($element) as $key) { + // Add a description to the link option. if (isset($element['#option_descriptions'][$key])) { $element[$key]['#description'] = $element['#option_descriptions'][$key]; } + // Add a list of fields dependent on this link type using the rel attribute. + if (isset($element['#flag_link_fields'][$key])) { + $element[$key]['#attributes']['rel'] = implode(' ', $element['#flag_link_fields'][$key]); + } } + $element['#attributes']['class'] = 'flag-link-options'; + return $element; } Index: flag.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/Attic/flag.install,v retrieving revision 1.2.2.32.2.1 diff -u -r1.2.2.32.2.1 flag.install --- flag.install 14 Sep 2009 19:11:10 -0000 1.2.2.32.2.1 +++ flag.install 28 Sep 2009 01:58:09 -0000 @@ -453,6 +453,29 @@ return array(); } +/** + * Convert role access to have separate "flag" and "unflag" permissions. + */ +function flag_update_6200() { + $ret = array(); + + if (db_column_exists('flags', 'roles')) { + $result = db_query('SELECT * FROM {flags}'); + while ($flag = db_fetch_object($result)) { + $roles = array_filter(explode(',', $flag->roles)); + $options = unserialize($flag->options); + $options['roles'] = array( + 'flag' => $roles, + 'unflag' => $roles, + ); + db_query("UPDATE {flags} SET options = '%s' WHERE fid = %d", serialize($options), $flag->fid); + } + db_drop_field($ret, 'flags', 'roles'); + } + + return $ret; +} + // This is a replacement for update_sql(). The latter doesn't support placeholders. function _flag_update_sql($sql) { $args = func_get_args(); Index: flag.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/Attic/flag.module,v retrieving revision 1.11.2.72.2.12 diff -u -r1.11.2.72.2.12 flag.module --- flag.module 28 Sep 2009 01:21:54 -0000 1.11.2.72.2.12 +++ flag.module 28 Sep 2009 01:58:09 -0000 @@ -113,7 +113,7 @@ * See documentation for $flag->user_access(). */ function flag_access($flag, $account = NULL) { - return $flag->user_access($account); + return $flag->user_access('flag', $account); } /** @@ -154,8 +154,10 @@ // Flag is not configured to show its link here. continue; } - if (!$flag->access($content_id)) { - // User has no permission to use this flag or flag does not apply to this content. + if (!$flag->access($content_id) && (!$flag->is_flagged($content_id) || !$flag->access($content_id, 'flag'))) { + // User has no permission to use this flag or flag does not apply to this + // content. The link is not skipped if the user has "flag" access but + // not "unflag" access (this way the unflag denied message is shown). continue; } @@ -192,7 +194,7 @@ function flag_flag_link(&$flag, $action, $content_id) { $token = flag_get_token($content_id); return array( - 'href' => "flag/". ($flag->link_type == 'confirm' ? 'confirm/' : '') ."$action/$flag->name/$content_id", + 'href' => 'flag/'. ($flag->link_type == 'confirm' ? 'confirm/' : '') ."$action/$flag->name/$content_id", 'query' => drupal_get_destination() . ($flag->link_type == 'confirm' ? '' : '&token='. $token), ); } @@ -213,6 +215,10 @@ 'confirm' => array( 'title' => t('Confirmation form'), 'description' => t('The user will be taken to a confirmation form on a separate page to confirm the flag.'), + 'options' => array( + 'flag_confirmation' => '', + 'unflag_confirmation' => '', + ), ), ); } @@ -599,6 +605,46 @@ } /** + * Implementation of hook_flag_access(). + */ +function flag_flag_access($flag, $content_id, $action, $account) { + // Restrict access by authorship. It's important that TRUE is never returned + // here, otherwise we'd grant permission even if other modules denied access. + if ($flag->content_type == 'node') { + $node = node_load($content_id); + if ($flag->access_author == 'own' && $node->uid != $account->uid) { + return FALSE; + } + elseif ($flag->access_author == 'others' && $node->uid == $account->uid) { + return FALSE; + } + } +} + +/** + * Implementation of hook_flag_access_multiple(). + */ +function flag_flag_access_multiple($flag, $content_ids, $account) { + if ($flag->content_type == 'node') { + // Restrict access by authorship. This is similar to flag_flag_access() + // above, but returns an array of 'nid' => $access values. Similarly, we + // should never return TRUE in any of these access values, only FALSE if we + // want to deny access, or use the current access value provided by Flag. + $nids = implode(',', array_map('intval', array_keys($content_ids))); + $placeholders = implode(',', array_fill(0, sizeof($flag->types), "'%s'")); + $result = db_query("SELECT nid, uid FROM {node} WHERE nid IN ($nids) AND type in ($placeholders)", $flag->types); + while ($row = db_fetch_object($result)) { + if ($flag->access_author == 'own') { + $passed[$row->nid] = $row->uid != $account->uid ? FALSE : $passed[$row->nid]; + } + elseif ($flag->access_author == 'others') { + $passed[$row->nid] = $row->uid == $account->uid ? FALSE : $passed[$row->nid]; + } + } + return $passed; + } +} +/** * Trim a flag to a certain size. * * @param $fid @@ -691,6 +737,9 @@ 'flag_admin_page' => array( 'arguments' => array('flags' => NULL, 'default_flags' => NULL), ), + 'flag_form_roles' => array( + 'arguments' => array('element' => NULL), + ), 'flag_rules_radios' => array( 'arguments' => array(), ), @@ -720,17 +769,18 @@ $content_id = $variables['content_id']; // Generate the link URL. - $link_types = flag_get_link_types(); - if (!isset($link_types[$flag->link_type])) { - // Provide a fallback if the link type no longer exists. - $flag->link_type = 'normal'; - } - $link_type_module = $link_types[$flag->link_type]['module']; - $link = module_invoke($link_type_module, 'flag_link', $flag, $action, $content_id); + $link_type = $flag->get_link_type(); + $link = module_invoke($link_type['module'], 'flag_link', $flag, $action, $content_id); if (isset($link['title']) && empty($link['html'])) { $link['title'] = check_plain($link['title']); } + // Replace the link with the access denied text if unable to flag. + if ($action == 'unflag' && !$flag->access($content_id, 'unflag')) { + $link['title'] = $flag->get_label('unflag_denied_text'); + unset($link['href']); + } + if ($flag->link_type == 'toggle' && $first_time) { $variables['setup'] = $first_time; $first_time = FALSE; @@ -739,7 +789,7 @@ $variables['setup'] = FALSE; } - $variables['link_href'] = check_url(url($link['href'], $link)); + $variables['link_href'] = isset($link['href']) ? check_url(url($link['href'], $link)) : FALSE; $variables['link_text'] = isset($link['title']) ? $link['title'] : strip_tags($flag->get_label($action . '_short', $content_id), ''); $variables['link_title'] = isset($link['attributes']['title']) ? check_plain($link['attributes']['title']) : check_plain(strip_tags($flag->get_label($action . '_long', $content_id))); $variables['flag_name_css'] = str_replace('_', '-', $flag->name); @@ -853,6 +903,18 @@ return $options; } +/** + * Return an array of flag link fields that are dependent on a link type. + */ +function _flag_link_type_fields() { + $options = array(); + $types = flag_get_link_types(); + foreach ($types as $type_name => $type) { + $options[$type_name] = array_keys($type['options']); + } + return $options; +} + // --------------------------------------------------------------------------- // Non-Views public API @@ -1218,13 +1280,16 @@ foreach (module_implements('flag_link_types') as $module) { $module_types = module_invoke($module, 'flag_link_types'); foreach ($module_types as $type_name => $info) { - $link_types[$type_name] = array( - 'module' => $module, - 'title' => $info['title'], - 'description' => $info['description'] + $link_types[$type_name] = $info; + $link_types[$type_name]['module'] = $module; + $link_types[$type_name] += array( + 'title' => '', + 'description' => '', + 'options' => array(), ); } } + drupal_alter('flag_link_types', $link_types); } return $link_types; Index: flag.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/Attic/flag.inc,v retrieving revision 1.1.2.30.2.7 diff -u -r1.1.2.30.2.7 flag.inc --- flag.inc 27 Sep 2009 22:59:00 -0000 1.1.2.30.2.7 +++ flag.inc 28 Sep 2009 01:58:09 -0000 @@ -109,7 +109,6 @@ // Various non-serialized properties of the flag, corresponding directly to // database columns. var $title = ''; - var $roles = array(DRUPAL_AUTHENTICATED_RID); var $global = FALSE; // The sub-types, e.g. node types, this flag applies to. var $types = array(); @@ -136,7 +135,10 @@ // ...but skip the following two. unset($flag->options, $flag->type); - $options = (array)unserialize($row->options); + // Populate the options with the defaults. + $options = (array) unserialize($row->options); + $options += $flag->options(); + // Make the unserialized options accessible as normal properties. foreach ($options as $option => $value) { $flag->$option = $value; @@ -147,8 +149,6 @@ $flag->types[] = $row->type; } - $flag->roles = empty($row->roles) ? array() : explode(',', $row->roles); - return $flag; } @@ -189,13 +189,21 @@ 'flag_short' => '', 'flag_long' => '', 'flag_message' => '', - 'flag_confirmation' => '', 'unflag_short' => '', 'unflag_long' => '', 'unflag_message' => '', - 'unflag_confirmation' => '', + 'unflag_denied_text' => '', 'link_type' => 'toggle', + 'roles' => array( + 'flag' => array(DRUPAL_AUTHENTICATED_RID), + 'unflag' => array(DRUPAL_AUTHENTICATED_RID), + ), ); + + // Merge in options from the current link type. + $link_type = $this->get_link_type(); + $options = array_merge($options, $link_type['options']); + // Allow other modules to change the flag options. drupal_alter('flag_options', $this, $options); return $options; @@ -229,7 +237,8 @@ $this->$field = $value; } // But checkboxes need some massaging: - $this->roles = array_values(array_filter($this->roles)); + $this->roles['flag'] = array_values(array_filter($this->roles['flag'])); + $this->roles['unflag'] = array_values(array_filter($this->roles['unflag'])); $this->types = array_values(array_filter($this->types)); // Clear internal titles cache: $this->get_title(NULL, TRUE); @@ -242,9 +251,17 @@ * A list of errors encountered while validating this flag's options. */ function validate() { - return $this->validate_name(); + // TODO: It might be nice if this used automatic method discovery rather + // than hard-coding the list of validate functions. + return array_merge_recursive( + $this->validate_name(), + $this->validate_access() + ); } + /** + * Validates that the current flag's name is valid. + */ function validate_name() { $errors = array(); @@ -268,6 +285,39 @@ } /** + * Validates that the current flag's access settings are valid. + */ + function validate_access() { + $errors = array(); + + // Require an unflag access denied message a role is not allowed to unflag. + if (empty($this->unflag_denied_text)) { + foreach ($this->roles['flag'] as $key => $rid) { + if ($rid && empty($this->roles['unflag'][$key])) { + $errors['unflag_denied_text'][] = array( + 'error' => 'flag_denied_text_required', + 'message' => t('The "Unflag not allowed text" is required if any user roles are not allowed to unflag.'), + ); + break; + } + } + } + + // Do not allow unflag access without flag access. + foreach ($this->roles['unflag'] as $key => $rid) { + if ($rid && empty($this->roles['flag'][$key])) { + $errors['roles'][] = array( + 'error' => 'flag_roles_unflag', + 'message' => t('Any user role that has the ability to unflag must also have the ability to flag.'), + ); + break; + } + } + + return $errors; + } + + /** * Fetches, possibly from some cache, a content object this flag works with. */ function fetch_content($content_id, $object_to_remember = NULL) { @@ -335,13 +385,16 @@ /** * Returns TRUE if user has access to use this flag. * + * @param $action + * Optional. The action to test, either "flag" or "unflag". If none given, + * "flag" will be tested, which is the minimum permission to use a flag. * @param $account * Optional. The user object. If none given, the current user will be used. * * @return * Boolean TRUE if the user is allowed to flag/unflag. FALSE otherwise. */ - function user_access($account = NULL) { + function user_access($action = 'flag', $account = NULL) { if (!isset($account)) { $account = $GLOBALS['user']; } @@ -351,8 +404,8 @@ return FALSE; } - $matched_roles = array_intersect($this->roles, array_keys($account->roles)); - return !empty($matched_roles) || empty($this->roles) || $account->uid == 1; + $matched_roles = array_intersect($this->roles[$action], array_keys($account->roles)); + return !empty($matched_roles) || !empty($this->roles[$action]) || $account->uid == 1; } /** @@ -377,19 +430,23 @@ return FALSE; } - // Allow modules to disallow (or allow) access to flagging. if (!isset($action)) { $action = $this->is_flagged($content_id, $account->uid) ? 'unflag' : 'flag'; } + + // Base initial access on the user's basic permission to use this flag. + $access = $this->user_access($action, $account); + + // Allow modules to disallow (or allow) access to flagging. $access_array = module_invoke_all('flag_access', $this, $content_id, $action, $account); - foreach ($access_array as $access) { - if (isset($access)) { - return $access; + + foreach ($access_array as $set_access) { + if (isset($set_access)) { + $access = $set_access; } } - // Fall back on the flag's user-specific access check. - return $this->user_access($account); + return $access; } /** @@ -618,6 +675,14 @@ } /** + * Get the link type for this flag. + */ + function get_link_type() { + $link_types = flag_get_link_types(); + return isset($link_types[$this->link_type]) ? $link_types[$this->link_type] : $link_types['normal']; + } + + /** * Replaces tokens in a label. Only the 'global' token context is regognized * by default, so derived classes should override this method to add all * token contexts they understand. @@ -771,7 +836,7 @@ * Saves an existing flag to the database. Better use save(). */ function update() { - db_query("UPDATE {flags} SET name = '%s', title = '%s', roles = '%s', global = %d, options = '%s' WHERE fid = %d", $this->name, $this->title, implode(',', $this->roles), $this->global, $this->get_serialized_options(), $this->fid); + db_query("UPDATE {flags} SET name = '%s', title = '%s', global = %d, options = '%s' WHERE fid = %d", $this->name, $this->title, $this->global, $this->get_serialized_options(), $this->fid); db_query("DELETE FROM {flag_types} WHERE fid = %d", $this->fid); foreach ($this->types as $type) { db_query("INSERT INTO {flag_types} (fid, type) VALUES (%d, '%s')", $this->fid, $type); @@ -782,16 +847,8 @@ * Saves a new flag to the database. Better use save(). */ function insert() { - if (function_exists('db_last_insert_id')) { - // Drupal 6. We have a 'serial' primary key. - db_query("INSERT INTO {flags} (content_type, name, title, roles, global, options) VALUES ('%s', '%s', '%s', '%s', %d, '%s')", $this->content_type, $this->name, $this->title, implode(',', $this->roles), $this->global, $this->get_serialized_options()); - $this->fid = db_last_insert_id('flags', 'fid'); - } - else { - // Drupal 5. We have an 'integer' primary key. - $this->fid = db_next_id('{flags}_fid'); - db_query("INSERT INTO {flags} (fid, content_type, name, title, roles, global, options) VALUES (%d, '%s', '%s', '%s', '%s', %d, '%s')", $this->fid, $this->content_type, $this->name, $this->title, implode(',', $this->roles), $this->global, $this->get_serialized_options()); - } + db_query("INSERT INTO {flags} (content_type, name, title, global, options) VALUES ('%s', '%s', '%s', %d, '%s')", $this->content_type, $this->name, $this->title, $this->global, $this->get_serialized_options()); + $this->fid = db_last_insert_id('flags', 'fid'); foreach ($this->types as $type) { db_query("INSERT INTO {flag_types} (fid, type) VALUES (%d, '%s')", $this->fid, $type); } @@ -881,6 +938,7 @@ 'show_on_page' => TRUE, 'show_on_teaser' => TRUE, 'show_on_form' => FALSE, + 'access_author' => '', 'i18n' => 0, ); return $options; @@ -902,6 +960,19 @@ '#access' => module_exists('translation_helpers'), '#weight' => 5, ); + + $form['access']['access_author'] = array( + '#type' => 'radios', + '#title' => t('Flag access by content authorship'), + '#options' => array( + '' => t('No additional restrictions'), + 'own' => t('Users may only flag content they own'), + 'others' => t('Users may only flag content of others'), + ), + '#default_value' => $this->access_author, + '#description' => t("Restrict access to this flag based on the user's ownership of the content. Users must also have access to the flag through the role settings."), + ); + $form['display']['show_on_teaser'] = array( '#type' => 'checkbox', '#title' => t('Display link on node teaser'), @@ -939,16 +1010,15 @@ $account = $GLOBALS['user']; } - if (!$this->user_access($account)) { - // User has no permission to use this flag. - return FALSE; - } $passed = array(); $nids = implode(',', array_map('intval', array_keys($content_ids))); $placeholders = implode(',', array_fill(0, sizeof($this->types), "'%s'")); $result = db_query("SELECT nid FROM {node} WHERE nid IN ($nids) AND type in ($placeholders)", $this->types); while ($row = db_fetch_object($result)) { - $passed[$row->nid] = $content_ids[$row->nid] == 'flag' ? FALSE : TRUE; + // First check basic user access for this action. + $passed[$row->nid] = $this->user_access($content_ids[$row->nid], $account); + + // Allow other modules to modify access. $access_array = module_invoke_all('flag_access_multiple', $this, $content_ids, $account); foreach ($access_array as $access) { if (isset($access)) { @@ -1099,16 +1169,15 @@ $account = $GLOBALS['user']; } - if (!$this->user_access($account)) { - // User has no permission to use this flag. - return FALSE; - } $passed = array(); $content_ids = implode(',', array_map('intval', array_keys($content_ids))); $placeholders = implode(',', array_fill(0, sizeof($this->types), "'%s'")); $result = db_query("SELECT cid FROM {comments} c INNER JOIN {node} n ON c.nid = n.nid WHERE cid IN ($content_ids) and n.type IN ($placeholders)", $this->types); while ($row = db_fetch_object($result)) { - $passed[$row->cid] = TRUE; + // First check basic user access for this action. + $passed[$row->cid] = $this->user_access($content_ids[$row->cid], $account); + + // Allow other modules to modify access. $access_array = module_invoke_all('flag_access_multiple', $this, $content_ids, $account); foreach ($access_array as $access) { if (isset($access)) { @@ -1197,17 +1266,25 @@ $options = parent::options(); $options += array( 'show_on_profile' => TRUE, + 'access_uid' => '', ); return $options; } function options_form(&$form) { parent::options_form($form); - $form['types'] = array( - // A user flag doesn't support node types. (Maybe will support roles instead, in the future.) + $form['access']['types'] = array( + // A user flag doesn't support node types. + // TODO: Maybe support roles instead of node types. '#type' => 'value', '#value' => array(0 => 0), ); + $form['access']['access_uid'] = array( + '#type' => 'checkbox', + '#title' => t('Users may flag themselves'), + '#description' => t('Disabling this option may be useful when setting up a "friend" flag, when a user flagging themself does not make sense.'), + '#default_value' => $this->access_uid ? 0 : 1, + ); $form['display']['show_on_profile'] = array( '#type' => 'checkbox', '#title' => t('Display link on user profile page'), @@ -1216,6 +1293,13 @@ ); } + function form_input($form_values) { + parent::form_input($form_values); + // The access_uid value is intentionally backwards from the UI, to avoid + // confusion caused by checking a box to disable a feature. + $this->access_uid = empty($form_values['access_uid']) ? 'others' : ''; + } + function _load_content($content_id) { return user_load(array('uid' => $content_id)); } @@ -1229,20 +1313,41 @@ return FALSE; } + function access($content_id, $action = NULL, $account = NULL) { + $access = parent::access($content_id, $action, $account); + $account = isset($account) ? $account : $GLOBALS['user']; + + // Prevent users from flagging themselves. + if ($this->access_uid == 'others' && $content_id == $account->uid) { + $access = FALSE; + } + + return $access; + } + function access_multiple($content_ids, $account = NULL) { if (!isset($account)) { $account = $GLOBALS['user']; } - if (!$this->user_access($account)) { - // User has no permission to use this flag. - return FALSE; - } // This user flag doesn't currently support subtypes so all users are // applicable for flagging. $passed = array(); foreach (array_keys($content_ids) as $uid) { - $passed[$uid] = (bool) $uid; // Exclude anonymous. + // First check basic user access for this action. + $passed[$uid] = $this->user_access($content_ids[$uid], $account); + + // Exclude anonymous. + if ($uid == 0) { + $passed[$uid] = FALSE; + } + + // Prevent users from flagging themselves. + if ($this->access_uid == 'others' && $uid == $account->uid) { + $passed[$uid] = FALSE; + } + + // Allow other modules to modify access. $access_array = module_invoke_all('flag_access_multiple', $this, $content_ids, $account); foreach ($access_array as $access) { if (isset($access)) { Index: theme/flag.tpl.php =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/theme/Attic/flag.tpl.php,v retrieving revision 1.1.2.7 diff -u -r1.1.2.7 flag.tpl.php --- theme/flag.tpl.php 17 Mar 2009 02:10:30 -0000 1.1.2.7 +++ theme/flag.tpl.php 28 Sep 2009 01:58:10 -0000 @@ -40,11 +40,15 @@ drupal_add_js(drupal_get_path('module', 'flag') .'/theme/flag.js'); } ?> - -   + + +   + + + - - + + Index: tests/flag.test =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/flag/tests/Attic/flag.test,v retrieving revision 1.1.2.1 diff -u -r1.1.2.1 flag.test --- tests/flag.test 22 Feb 2009 21:32:47 -0000 1.1.2.1 +++ tests/flag.test 28 Sep 2009 01:58:10 -0000 @@ -36,13 +36,12 @@ 'title' => $this->randomName(), 'flag_short' => 'flag short [nid]', 'flag_long' => 'flag long [nid]', - 'flag_confirmation' => 'flag confirm [nid]', 'flag_message' => 'flag message [nid]', 'unflag_short' => 'unflag short [nid]', 'unflag_long' => 'unflag long [nid]', - 'unflag_confirmation' => 'unflag confirm [nid]', 'unflag_message' => 'unflag message [nid]', - 'roles[2]' => TRUE, + 'roles[flag][2]' => TRUE, + 'roles[unflag][2]' => TRUE, 'types[story]' => FALSE, 'types[page]' => TRUE, 'show_on_teaser' => FALSE, @@ -51,9 +50,9 @@ 'link_type' => 'toggle', ); $saved = $edit; - $saved['roles'] = array(2); + $saved['roles'] = array('flag' => array(2), 'unflag' => array(2)); $saved['types'] = array('page'); - unset($saved['roles[2]'], $saved['types[story]'], $saved['types[page]']); + unset($saved['roles[flag][2]'], $saved['roles[unflag][2]'], $saved['types[story]'], $saved['types[page]']); $this->drupalPost('admin/build/flags/add/node/' . $edit['name'], $edit, t('Submit')); @@ -73,13 +72,12 @@ 'title' => $this->randomName(), 'flag_short' => 'flag 2 short [nid]', 'flag_long' => 'flag 2 long [nid]', - 'flag_confirmation' => 'flag 2 confirm [nid]', 'flag_message' => 'flag 2 message [nid]', 'unflag_short' => 'unflag 2 short [nid]', 'unflag_long' => 'unflag 2 long [nid]', - 'unflag_confirmation' => 'unflag 2 confirm [nid]', 'unflag_message' => 'unflag 2 message [nid]', - 'roles[2]' => TRUE, + 'roles[flag][2]' => TRUE, + 'roles[unflag][2]' => TRUE, 'types[story]' => TRUE, 'types[page]' => FALSE, 'show_on_teaser' => TRUE, @@ -88,9 +86,9 @@ 'link_type' => 'normal', ); $saved = $edit; - $saved['roles'] = array(2); + $saved['roles'] = array('flag' => array(2), 'unflag' => array(2)); $saved['types'] = array('story'); - unset($saved['roles[2]'], $saved['types[story]'], $saved['types[page]']); + unset($saved['roles[flag][2]'], $saved['roles[unflag][2]'], $saved['types[story]'], $saved['types[page]']); $this->drupalPost('admin/build/flags/edit/' . $flag->name, $edit, t('Submit')); Index: theme/flag-admin.css =================================================================== RCS file: theme/flag-admin.css diff -N theme/flag-admin.css --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ theme/flag-admin.css 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,15 @@ +/* $Id$ */ + +table.flag-admin-table { + width: auto; + margin: 0; +} +table.flag-admin-table th { + font-weight: normal; + padding-left: 2em; + padding-right: 2em; +} +table.flag-admin-table td { + padding-left: 2em; + padding-right: 2em; +} Index: theme/flag-admin.js =================================================================== RCS file: theme/flag-admin.js diff -N theme/flag-admin.js --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ theme/flag-admin.js 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,67 @@ +// $Id$ + +/** + * Behavior to disable the "unflag" option if "flag" is not available. + */ +Drupal.behaviors.flagRoles = function(context) { + $('#flag-roles input.flag-access', context).change(function() { + var unflagCheckbox = $(this).parents('tr:first').find('input.unflag-access').get(0); + if (this.checked) { + // If "flag" is available, restore the state of the "unflag" checkbox. + unflagCheckbox.disabled = false; + if (typeof(unflagCheckbox.previousFlagState) != 'undefined') { + unflagCheckbox.checked = unflagCheckbox.previousFlagState; + } + else { + unflagCheckbox.checked = true; + } + } + else { + // Remember if the "unflag" option was checked or unchecked, then disable. + unflagCheckbox.previousFlagState = unflagCheckbox.checked; + unflagCheckbox.disabled = true; + unflagCheckbox.checked = false; + } + }); + + $('#flag-roles input.unflag-access', context).change(function() { + if ($(this).parents('tr:first').find('input.unflag-access:checked:not(:disabled)').size() > 0) { + $('#edit-unflag-denied-text-wrapper').slideUp(); + } + else { + $('#edit-unflag-denied-text-wrapper').slideDown(); + } + }); + + // Hide the link options by default if needed. + if ($('#flag-roles input.unflag-access:checked:not(:disabled)').size() > 0) { + $('#edit-unflag-denied-text-wrapper').css('display', 'none'); + } +}; + + +/** + * Behavior to make link options dependent on the link radio button. + */ +Drupal.behaviors.flagLinkOptions = function(context) { + $('.flag-link-options input.form-radio', context).change(function() { + var radioButton = this; + $('#link-options').slideUp(function() { + $('#link-options input').each(function() { + $(this).parents('.form-item:first').css('display', 'none'); + }); + var linkOptionFields = $(radioButton).attr('rel'); + if (linkOptionFields) { + linkOptionFields = linkOptionFields.split(' '); + for (var n in linkOptionFields) { + $('#link-options input[name=' + linkOptionFields[n] + ']').parents('.form-item:first').css('display', 'block'); + } + $('#link-options').slideDown(); + } + }); + }); + // Hide the link options by default if needed. + if (!$('.flag-link-options input.form-radio:checked').attr('rel')) { + $('#link-options').css('display', 'none'); + } +};