friend_uid] = user_load(array('uid' => $rel->friend_uid)); } } return $rels[$uid][$fid]; } /** * Retrieve the number of active relationships of the user. * * @param $uid * The user id. * @param $fid * The relationship (Flag) id. * @param $reset * Boolean trigger to reset the static cache. * @return * Number of relationships. */ function flag_friend_get_relationship_count($uid, $fid, $reset = NULL) { static $rels_count = array(); if (!isset($rels_count[$uid][$fid]) || $reset) { $rels_count[$uid][$fid] = (int) db_result(db_query("SELECT COUNT(1) FROM {flag_friend} WHERE uid = %d AND fid = %d", $uid, $fid)); } return $rels_count[$uid][$fid]; } /** * Theme a string representing number of Flag Friend relationships for the * given user. * * @param $flag * flag object * @param $account * user account whose relationships to count * @return * String representing number of relationships */ function theme_flag_friend_relationship_count($flag, $account) { $count = flag_friend_get_relationship_count($account->uid, $flag->fid); return $count > 0 ? format_plural($count, '1 friend', '@count friends', array('@count' => $count)) : t('No friends'); } /** * Get list of Friend Flags. * * @param $type * What type of results to return. Can be one of the following: "names", * "titles". When not provided, the function returns objects. * @return * Array of flag objects or flag properties (depending on $type) keyed by fid */ function flag_friend_get_flags($type = NULL) { $flags = flag_get_flags('user'); $result = array(); foreach ($flags as $fid => $flag) { if ($flag->link_type == 'msg_confirm') { switch ($type) { case 'names': $result[$fid] = $flag->name; break; case 'titles': $result[$fid] = $flag->get_label('title'); break; default: $result[$fid] = $flag; } } } return $result; } /** * Determines the status of the relationship by testing various conditions. * * @param $flag * The flag object. * @param $uid1 * The account id of one of the users. * @param $uid2 * The account id of the other user. * @return * A string describing the status of the relationship. */ function flag_friend_relationship_status($flag, $uid1, $uid2, $reset = NULL) { static $status_cache = array(); if ($reset) { unset($status_cache); } $fid = $flag->fid; if (!isset($status_cache[$uid1][$uid2][$fid])) { // Always check both flags and relationship for consistency. $you_are_flagged = $flag->is_flagged($uid1, $uid2); $they_are_flagged = $flag->is_flagged($uid2, $uid1); $relationship = db_result(db_query("SELECT * FROM {flag_friend} WHERE uid = %d AND friend_uid = %d AND fid = %d", $uid1, $uid2, $fid)); if ($relationship) { // see if these users have flagged eachother if ($you_are_flagged && $they_are_flagged) { $status = FLAG_FRIEND_RELATIONSHIP; } // new transitional state in the relationship breaking process. elseif ($you_are_flagged && !$they_are_flagged) { $status = FLAG_FRIEND_BREAKING; } else { $status = FLAG_FRIEND_ERROR; } } else { if ($you_are_flagged && $they_are_flagged) { $status = FLAG_FRIEND_BOTH; } if (!$you_are_flagged && !$they_are_flagged) { $status = FLAG_FRIEND_UNFLAGGED; } if ($you_are_flagged && !$they_are_flagged) { $status = FLAG_FRIEND_APPROVAL; } if (!$you_are_flagged && $they_are_flagged) { $status = FLAG_FRIEND_PENDING; } } $status_cache[$uid1][$uid2][$fid] = $status; } return $status_cache[$uid1][$uid2][$fid]; } /** * Return flag friend default link options. */ function _flag_friend_default_link_options() { $options = array(); // Request $options['request_short'] = 'Add friend'; $options['request_long'] = 'Add [user] to your list of friends.'; $options['request_question'] = 'Are you sure you want to add [user] to your list of friends?'; $options['request_yes'] = 'Send'; $options['request_no'] = 'Cancel'; $options['request_success_message'] = 'Request sent successfully.'; $options['request_failed_message'] = 'Request failed.'; // Message $options['request_msg'] = 'Hello [user], please let me add you to my friend list.'; $options['request_msg_title'] = 'Send [user] a message (optional)'; $options['request_msg_desc'] = 'Enter a message to send to this user.'; // Cancel pending $options['cancel_short'] = 'Friend Requested. Cancel?'; $options['cancel_long'] = 'Click here to cancel this request.'; $options['cancel_question'] = 'Are you sure you want to cancel your pending friend request?'; $options['cancel_yes'] = 'Yes'; $options['cancel_no'] = 'No'; $options['cancel_success_message'] = 'Your friendship request to [user] was canceled.'; $options['cancel_failed_message'] = 'Friendship request cancellation failed.'; // Approve. Default is almost the same as request, but we let define // different strings. $options['approve_short'] = 'Approve'; $options['approve_long'] = 'Add [user] to your list of friends.'; $options['approve_question'] = 'Are you sure you want to add [user] to your list of friends?'; $options['approve_yes'] = 'Yes'; $options['approve_no'] = 'No'; $options['approve_success_message'] = '[user] has been added to your list of friends.'; $options['approve_failed_message'] = "Couldn't add [user] to your list of friends."; // Deny $options['deny_short'] = 'Deny'; $options['deny_long'] = "Deny [user]'s friendship request."; $options['deny_question'] = "Are you sure you don't want to be friends with [user]?"; $options['deny_yes'] = 'Yes'; $options['deny_no'] = 'No'; $options['deny_success_message'] = "[user]'s friendship request has been denied."; $options['deny_failed_message'] = "Couldn't deny [user]'s friendship request."; // Remove $options['remove_short'] = 'Remove friend'; $options['remove_long'] = 'Remove [user] from your list of friends.'; $options['remove_question'] = 'Are you sure you want to remove [user] from your list of friends?'; $options['remove_yes'] = 'Remove'; $options['remove_no'] = 'Cancel'; $options['remove_success_message'] = '[user] has been removed from your list of friends.'; $options['remove_failed_message'] = 'Failed to remove [user] from your list of friends.'; return $options; } /** * Public API function to manage Flag Friend relationships. Handles both Flag * states and flag_friend table entries. * * Using transactions because whole operation is not atomic and DB could end up * in an inconsistent state. * * @param $op * One of the following operations: * "create" - creates relationship * "create_no_trigger" - same as create but skips hook_flag_friend() * invocation and Flag Friend trigger. * "delete" - deletes relationship * "delete_no_trigger" - same as delete but skips hook_flag_friend() * invocation and Flag Friend trigger. * * @param $requestor * requestor user id * @param $requestee * requestee user id * @param $flagname * flag name * @return boolean * TRUE on success, FALSE otherwise. */ function flag_friend_relationship($op, $requestor, $requestee, $flagname) { global $flag_friend_no_action_required; $flag = flag_get_flag($flagname); // Start transaction. _flag_friend_transaction('BEGIN'); switch ($op) { case 'create': $result = _flag_friend_flag_both('flag', $requestor, $requestee, $flag); break; case 'create_no_trigger': // Run "create" operation with semaphore set, so our hook_flag() // doesn't stand on our way. $flag_friend_no_action_required = TRUE; $result = _flag_friend_flag_both('flag', $requestor, $requestee, $flag); unset($flag_friend_no_action_required); // Manually create relationship in the DB because of the semaphore. if ($result) { $result = _flag_friend_relationship_db('create', $requestor, $requestee, $flag); } break; case 'delete': $result = _flag_friend_flag_both('unflag', $requestor, $requestee, $flag); break; case 'delete_no_trigger': // Run "delete" operation with semaphore set, so our hook_flag() // doesn't stand on our way. $flag_friend_no_action_required = TRUE; $result = _flag_friend_flag_both('unflag', $requestor, $requestee, $flag); unset($flag_friend_no_action_required); // Manually create relationship in the DB because of the semaphore. if ($result) { $result = _flag_friend_relationship_db('delete', $requestor, $requestee, $flag); } } // commit or rollback if ($result) { _flag_friend_transaction('COMMIT'); return TRUE; } else { _flag_friend_transaction('ROLLBACK'); return FALSE; } } /** * Helper function to wrap our operations in transactions if they are enabled. */ function _flag_friend_transaction($op) { if (variable_get('flag_friend_use_transactions', FALSE)) { db_query($op); } } /** * Run flag action for both users. Let our hook_flag() handle the relationship. * and catch "rollback" if our hook_flag() operation failed to complete. */ function _flag_friend_flag_both($action, $uid1, $uid2, $flag) { global $flag_friend_rollback; $status = flag_friend_relationship_status($flag, $uid1, $uid2, TRUE); if (($status == FLAG_FRIEND_UNFLAGGED && $action == 'flag') || ($status == FLAG_FRIEND_RELATIONSHIP && $action == 'unflag')) { $account1 = user_load(array('uid' => $uid1)); $result1 = $flag->flag($action, $uid2, $account1); if (!$result1) { return FALSE; } $account2 = user_load(array('uid' => $uid2)); // Run backward flag action and catch global "rollback" flag. if ($flag->flag($action, $uid1, $account2) && !isset($flag_friend_rollback)) { return TRUE; } else { unset($flag_friend_rollback); } } return FALSE; } /** * Manage relationship DB entries. * * In most cases you would want to use flag_friend_relationship() instead of * this function. * */ function _flag_friend_relationship_db($op, $uid1, $uid2 = NULL, $flag = NULL) { switch ($op) { case 'create': $query = "INSERT INTO {flag_friend} VALUES(%d, %d, %d, %d)"; $q1 = db_query($query, $uid1, $uid2, $_SERVER['REQUEST_TIME'], $flag->fid); $q2 = db_query($query, $uid2, $uid1, $_SERVER['REQUEST_TIME'], $flag->fid); return ($q1 !== FALSE && $q2 !== FALSE); // Delete 1 or 2 entries of single relationship between the users. case 'delete': $q = db_query('DELETE FROM {flag_friend} WHERE (uid = %d AND friend_uid = %d AND fid = %d) OR (uid = %d AND friend_uid = %d AND fid = %d)', $uid1, $uid2, $flag->fid, $uid2, $uid1, $flag->fid); return ($q !== FALSE); // Delete all relationships of the user. case 'delete_user': $q = db_query("DELETE FROM {flag_friend} WHERE uid = %d OR friend_uid = %d", $uid1, $uid1); return ($q !== FALSE); } } /** * Helper function to return Flag Friend operation based on Flag action and * current relationship status. */ function _flag_friend_get_op($action, $status) { if ($action == 'flag' && $status == FLAG_FRIEND_UNFLAGGED) { return 'request'; } if ($action == 'unflag' && $status == FLAG_FRIEND_PENDING) { return 'cancel'; } if ($action == 'deny' && $status == FLAG_FRIEND_APPROVAL) { return 'deny'; // special "deny" flag action } if ($action == 'flag' && $status == FLAG_FRIEND_APPROVAL) { return 'approve'; } if ($action == 'unflag' && $status == FLAG_FRIEND_RELATIONSHIP) { return 'remove'; } return FALSE; } /** * Helper function to return Flag action based on Flag Friend operation and * current relationship status. */ function _flag_friend_get_action($op, $status) { if ($op == 'request' && $status == FLAG_FRIEND_UNFLAGGED) { return 'flag'; } if ($op == 'cancel' && $status == FLAG_FRIEND_PENDING) { return 'unflag'; } if ($op == 'deny' && $status == FLAG_FRIEND_APPROVAL) { // This is special part: "deny" action turns into "deny" operation to // properly theme "Deny" link. But when we turn "deny" operation into // an action, we make it "unflag" because that's what we pass to the // $flag->flag(). return 'unflag'; } if ($op == 'approve' && $status == FLAG_FRIEND_APPROVAL) { return 'flag'; } if ($op == 'remove' && $status == FLAG_FRIEND_RELATIONSHIP) { return 'unflag'; } return FALSE; } /** * Access callback for confirm form menu item. */ function _flag_friend_menu_access($link_type, $op, $flag_name, $content_id) { // Initial sanity checks. if (empty($link_type) || empty($op) || empty($flag_name) || !is_numeric($content_id)) { return FALSE; } // Check if link type is supported. if ($link_type != 'msg_confirm') { return FALSE; } // Load and check flag. $flag = flag_get_flag($flag_name); if (!$flag) { return FALSE; } // Get current status. $status = flag_friend_relationship_status($flag, $GLOBALS['user']->uid, $content_id); // Check if operation is applicable. $action = _flag_friend_get_action($op, $status); if (!$action) { return FALSE; } // "deny" is simulated action to deny incoming request. Flag with deny // action points to the requesting user and we unflag requestee on behalf // of requester. // Thus we must check if the requester can "unflag" the requestee if ($op == 'deny') { $account = user_load(array('uid' => $content_id)); $content_id = $GLOBALS['user']->uid; } else { $account =& $GLOBALS['user']; } return $flag->access($content_id, $action, $account); } /** * * Flag hooks. * */ /** * Implementation of hook_flag(). * * Most of our magic happens here. We work here with Flag's actions and not our * operations since Flag doesn't process custom actions. * Using $flag_friend_no_action_required semaphore when flagging to not call * ourselves. * Using $flag_friend_rollback flag when non-atomic operation fails. */ function flag_friend_flag($event, $flag, $content_id, $account, $fcid) { global $user, $flag_friend_no_action_required, $flag_friend_rollback; // Check semaphore to not call ourselves. if ($flag_friend_no_action_required) { return; } // We only operate on user flags that have "msg_confirm" link type. if ($flag->content_type != 'user' || $flag->link_type != 'msg_confirm') { return; } // See the status of the relationship. Note that we clear statically cached // status so it certainly contains value AFTER operation. $status = flag_friend_relationship_status($flag, $account->uid, $content_id, TRUE); $target_account = user_load(array('uid' => $content_id)); if ($event == 'flag') { // If both are now flagged, record the relationship. if ($status == FLAG_FRIEND_BOTH) { // Create the relationship DB entry. if (_flag_friend_relationship_db('create', $account->uid, $content_id, $flag)) { // fire trigger module_invoke_all('flag_friend', 'approve', $target_account, $account, $flag); } else { $flag_friend_rollback == TRUE; } } else { // Save request message. if (isset($GLOBALS['flag_friend_request_message'])) { $message = $GLOBALS['flag_friend_request_message']; _flag_friend_save_message($fcid, $message); } // fire trigger module_invoke_all('flag_friend', 'request', $target_account, $account, $flag); } } elseif ($event == 'unflag') { switch ($status) { case FLAG_FRIEND_BREAKING: // $account unflagged $target_account. // First, unflag $account on behalf of $target_account. Use semaphore // so we don't call ourselves. $flag_friend_no_action_required = TRUE; $result = $flag->flag('unflag', $account->uid, $target_account); unset($flag_friend_no_action_required); // Then delete relationship. if ($result && _flag_friend_relationship_db('delete', $account->uid, $content_id, $flag)) { module_invoke_all('flag_friend', 'remove', $target_account, $account, $flag); } else { // Use global rollback flag as the only way to return information // back to our submit callback. $flag_friend_rollback = TRUE; } break; case FLAG_FRIEND_UNFLAGGED: // Final status is same for denied and canceled request. Need // additional info to know flag operation. if ($content_id == $user->uid && $account->uid != $user->uid) { // User unflags himself on behalf of $account, i.e. denies incoming // request. module_invoke_all('flag_friend', 'deny', $account, $target_account, $flag); } elseif ($account->uid == $user->uid && $account->uid != $content_id) { // User cancels own request. module_invoke_all('flag_friend', 'cancel', $target_account, $account, $flag); } } } } /** * Implementation of hook_flag_default_flags(). */ function flag_friend_flag_default_flags() { $flags = array(); // Exported flag: "Friend". $flags[] = array ( 'content_type' => 'user', 'name' => 'friend', 'title' => 'Friend', 'global' => false, 'types' => array (), 'flag_short' => 'Add friend', 'flag_long' => 'Add this user to your list of friends.', 'flag_message' => '', 'unflag_short' => 'Remove friend', 'unflag_long' => 'Remove this user from your list of friends.', 'unflag_message' => '', 'unflag_denied_text' => '', 'link_type' => 'msg_confirm', 'roles' => array ( 'flag' => array (0 => '2'), 'unflag' => array (0 => '2'), ), 'show_on_profile' => true, 'access_uid' => '', 'status' => false, 'locked' => array ( 'name' => 'name', 'global' => 'global', ), 'module' => 'flag_friend', 'api_version' => 2, ); return $flags; } /** * Implementation of hook_flag_link(). * * Routes flag actions to custom callback and provides custom operations to it. * Actions are ones supported by Flag plus "deny" action that allows us * to distinguish "deny" and "approve" operations. */ function flag_friend_flag_link($flag, $action, $content_id) { global $user; $link = array('query' => drupal_get_destination()); // Determine what the status in the friend process is. $status = flag_friend_relationship_status($flag, $user->uid, $content_id); // Transform Flag action to a Flag Friend operation. $op = _flag_friend_get_op($action, $status); if (!$op) { return array(); // invalid status. } $link['title'] = $flag->get_label("${op}_short", $content_id); $link['attributes']['title'] = $flag->get_label("${op}_long", $content_id); $link['href'] = "ff/msg_confirm/${op}/$flag->name/$content_id"; return $link; } /** * Implementation of hook_flag_link_types(). */ function flag_friend_flag_link_types() { $options = _flag_friend_default_link_options(); // Let modules alter our link options. Need to alter them here rather than // in hook_flag_options_alter() so Flag module renders additions as dependant // fields too. drupal_alter('flag_friend_link_options', $options); return array( 'msg_confirm' => array( 'title' => t('Relationship confirmation form'), 'description' => t('Flag Friend: the user will be taken to a confirmation form with optional message on a separate page to confirm the relationship.'), 'options' => $options, ), ); } /** * * All other hooks. * */ /** * Implementation of hook_user(). */ function flag_friend_user($op, &$edit, &$account, $category = NULL) { if ($op == 'delete') { // Remove any relationship DB entries if an account is removed. I believe // flags are handled by Flag itself. _flag_friend_relationship_db('delete_user', $account); } } /** * Implementation of hook_menu(). */ function flag_friend_menu() { $items = array(); $items['ff/%/%/%/%'] = array( 'title' => 'Flag friend confirm', 'page callback' => 'drupal_get_form', 'page arguments' => array('flag_friend_confirm', 1, 2, 3, 4), 'access callback' => '_flag_friend_menu_access', 'access arguments' => array(1, 2, 3, 4), 'type' => MENU_CALLBACK, 'file' => 'includes/flag_friend.forms.inc', ); return $items; } /** * Implementation of hook_form_FORMID_alter(). * * Insert Flag Friend link options into flag add/edit form. */ function flag_friend_form_flag_form_alter(&$form, &$form_state) { module_load_include('inc', 'flag_friend', 'includes/flag_friend.forms'); _flag_friend_form_flag_form_alter($form, $form_state); } /** * Register theme functions. */ function flag_friend_theme() { return array( 'flag_friend_confirm_form' => array( 'arguments' => array('form' => NULL), 'file' => 'includes/flag_friend.forms.inc', ), 'flag_friend_relationship_count' => array( 'arguments' => array('flag' => NULL, 'account' => NULL), ), ); } /** * Implementation of hook_init(). */ function flag_friend_init() { // Include modules integrations. // Integrations that are not ported or not tested yet are commented below. $contrib = array('author_pane', 'token', 'trigger'); // 'activity', 'popups', $path = drupal_get_path('module', 'flag_friend'); foreach ($contrib as $module) { if (module_exists($module)) { include_once $path ."/contrib/flag_friend.$module.inc"; } } } /** * Implementation of hook_views_api(). */ function flag_friend_views_api() { return array( 'api' => 2.0, 'path' => drupal_get_path('module', 'flag_friend') .'/includes', ); } /** * Implementation of hook_ctools_plugin_directory(). */ function flag_friend_ctools_plugin_directory($module, $plugin) { if ($module == 'ctools') { return 'plugins/'. $plugin; } }