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) ? '<em>' . t('No roles') . '</em>' : implode(', ', $roles),
+      empty($flag->roles['flag']) ? '<em>' . t('No roles') . '</em>' : 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' => '<em>' . t('Note: You don\'t have the <a href="@token-url">Token</a> 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')) . '</em>',
     );
   }
 
-  $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 <em>authenticated user</em> 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 <em>authenticated user</em> 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), '<em><strong><img>');
   $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');
   }
 ?>
-<span class="flag-wrapper flag-<?php echo $flag_name_css; ?>">
-  <a href="<?php echo $link_href; ?>" title="<?php echo $link_title; ?>" class="<?php print $flag_classes ?>" rel="nofollow"><?php echo $link_text; ?></a><span class="flag-throbber">&nbsp;</span>
+<span class="flag-wrapper flag-<?php print $flag_name_css; ?>">
+  <?php if ($link_href): ?>
+    <a href="<?php print $link_href; ?>" title="<?php print $link_title; ?>" class="<?php print $flag_classes ?>" rel="nofollow"><?php print $link_text; ?></a><span class="flag-throbber">&nbsp;</span>
+  <?php else: ?>
+    <span class="<?php print $flag_classes ?>"><?php print $link_text; ?></span>
+  <?php endif; ?>
   <?php if ($after_flagging): ?>
-    <span class="flag-message flag-<?php echo $last_action; ?>-message">
-      <?php echo $message_text; ?>
+    <span class="flag-message flag-<?php print $last_action; ?>-message">
+      <?php print $message_text; ?>
     </span>
   <?php endif; ?>
 </span>
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');
+  }
+};
