? facebook-platform Index: fb_user.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/fb/fb_user.module,v retrieving revision 1.4 diff -u -r1.4 fb_user.module --- fb_user.module 15 Oct 2007 18:39:09 -0000 1.4 +++ fb_user.module 18 Mar 2008 02:50:47 -0000 @@ -1,5 +1,13 @@ FB_USER_SYNC_PATH, + 'access' => $user->uid > 0, // TODO: support anonymous adds + 'callback' => 'fb_user_sync_cb', + 'type' => MENU_CALLBACK, + ); + $items[] = array('path' => FB_USER_POST_ADD_PATH, + 'access' => TRUE, + 'callback' => 'fb_user_post_add_cb', + 'type' => MENU_CALLBACK, + ); + $items[] = array('path' => FB_USER_POST_REMOVE_PATH, + 'access' => TRUE, + 'callback' => 'fb_user_post_remove_cb', + 'type' => MENU_CALLBACK, + ); + } + + return $items; +} + +/** + * The post-add page is where the user is sent after adding the application. + * + * Note that the Facebook App settings must be set up. See "post-add" + * callback. To customize this behavior, use form_alter to modify the default + * form, or create your own callback. + * + * Special case when $_REQUEST['destination'] is set. In this case we've come + * after a request from our sync callback (or any call to $fb->require_add) + * and we're going to cache some data so it can be used on the other end. + */ +function fb_user_post_add_cb() { + global $fb, $fb_app, $user; + + // This page is for canvas pages only + if (!$fb) { + drupal_access_denied(); + exit(); + } + + // Destination will be set if we've called $fb->require_add. In this case + // we forward the user on, adding some useful information to the URL. + if ($_REQUEST['destination']) { + _fb_user_sync_redirect($_REQUEST['destination']); + } + + // Destination was not passed in, present a form prompting the user to + // decide what to do next. + watchdog('debug', 'fb_user_post_add_cb' . dprint_r($_REQUEST, 1)); + $output = drupal_get_form('fb_user_post_add_bifurcate_form'); + return $output; +} + +/** + * A form which helps a facebook user either register a new local drupal + * account or synchronize facebook account with an existing account. Really, + * all we do is redirect the user to the login or registration forms. + */ +function fb_user_post_add_bifurcate_form() { + global $fb, $fb_app, $user; + + // This form only works on canvas pages. + if (!$fb_app) { + drupal_access_denied(); + exit(); + } + // And if the user's account is recognized, we can skip this. + if ($user->uid && FALSE) { + drupal_goto(""); + exit(); + } + + // Substitutions for translation + $t = array('%sitename' => variable_get('site_name', t('this website')), + '%appname' => $fb_app->title); + + drupal_set_title(t('Welcome to %appname', $t)); + + $parents = array('redirect'); + // Could use 'radios' type, but using 'radio' is more flexible. + $form['redirect']['user/login'] = array('#type' => 'radio', + '#title' => t('I already have an account on %sitename', $t), + '#description' => t('You will be prompted for your password.', $t), + '#return_value' => 'user/login', + '#parents' => $parents, + ); + $form['redirect']['user/register'] = array('#type' => 'radio', + '#title' => t('Complete my registration now', $t), + '#description' => t('You will be asked for additional information and given a new password.', $t), + '#return_value' => 'user/register', + '#parents' => $parents, + '#default_value' => 'user/register', + ); + $form['redirect']['frontpage'] = array('#type' => 'radio', + '#title' => t('I will register later', $t), + '#description' => t('Use this application without registering on %sitename.', $t), + '#return_value' => '', + '#parents' => $parents, + ); + $form['submit'] = array('#type' => 'submit', + '#value' => t('Continue') + ); + + + return $form; +} + +function fb_user_post_add_bifurcate_form_submit($form_id, $values) { + dpm(func_get_args(), 'fb_user_post_add_bifurcate_form_submit'); + + // Here we simply redirect the user to the appropriate page + return $values['redirect']; +} + +/** + * The post-remove page is visited by Facebook after a user removes the + * application. The user never visits the page, it is simply called by + * Facebook to notify us of the change. + */ +function fb_user_post_remove_cb() { + global $fb, $fb_app, $user; + watchdog('debug', 'fb_user_post_remove_cb' . dprint_r($_REQUEST, 1) . dprint_r($user, 1) . dprint_r($fb_app, 1)); + // Update our database to reflect application is NOT added + _fb_user_track($fb, $fb_app); + // Nothing to return + exit(); +} + + +function _fb_user_sync_redirect($destination) { + global $user; + global $fb, $fb_app; + $cache_data = array('request' => $_REQUEST, + 'fbu' => fb_facebook_user(), + 'fb_app' => $fb_app); + // Generate a one-time key for cached data. We used to use Facebook's + // auth_token here, hence the name in the url, but it's not always available + // so better to generate our own. + $key = uniqid('fb_user_'); + + cache_set($key, 'cache', serialize($cache_data), CACHE_TEMPORARY); + + // Note that drupal_goto will fail here, but $fb->redirect succeeds. + $fb->redirect(url($destination, 'auth_token='.$key)); + exit(); +} + +/** + * The sync callback is invoked first on a canvas page, in which case we + * require the user to add the application. Later the user will be redirected + * to this callback on the locale server, with an auth_token that allows us to + * write the necessary row to the authmap table. + */ +function fb_user_sync_cb() { + global $user; + global $fb, $fb_app; + // TODO: ensure that user does not already have a mapping to some other facebook id. + + // On canvas pages, require user to add the app... + if ($fb) { + watchdog('debug', 'fb_user_sync_cb request ' . dprint_r($_REQUEST, 1)); + + $fb->require_add(); + + // The user has already added the app. + // Redirect to the local page where the authmap will be generated. + _fb_user_sync_redirect(fb_local_url(FB_USER_SYNC_PATH)); + } + else { + // Double check user is logged in. No point syncing the anonymous account. + if (!$user->uid) { + drupal_access_denied(); + exit(); + } + + $output = ''; + // On non canvas pages, we have returned to the local server. Hopefully + // the user has added the app and we can now sync the two accounts. + if ($_REQUEST['auth_token']) { + $key = $_REQUEST['auth_token']; + $cache = cache_get($key); + cache_clear_all($key, 'cache'); // So noone else uses this key + $data = unserialize($cache->data); + if (!$data) { + drupal_set_message(t('Unable to locate Facebook account information.'), 'error'); + drupal_access_denied(); + exit(); + } + watchdog('debug', 'got the data ' . dprint_r($data, 1)); + $fbu = $data['fbu']; + $fb_app = $data['fb_app']; + drupal_set_title(t('Added %appname Application', + array('%appname' => $fb_app->title))); + if ($fbu && $fb_app) { + $authname = _fb_user_authname($fb_app, $fbu); + watchdog('fb_user', t('Syncing local user %uid with facebook user %fbu via authmap entry %authname', + array('%fbu' => $fbu, + '%uid' => $user->uid, + '%authname' => $authname))); + user_set_authmaps($user, array('authname_fb_user' => $authname)); + watchdog('fb_user', t('Synced local user %uid with facebook user %fbu via authmap entry %authname', + array('%fbu' => $fbu, + '%uid' => $user->uid, + '%authname' => $authname))); + + + drupal_set_message(t('Your local account is linked to your Facebook profile.', + array('!localurl' => url('user/'.$user->uid), + '!facebookurl' => url('http://www.facebook.com/profile.php', 'id='.$fbu)))); + + // Give the user feedback that the sync has been successful. + // Query facebook to learn more about their facebook account. + $fb = fb_api_init($fb_app, FB_FBU_ANY); + if ($fb) { + $info = $fb->api_client->users_getInfo(array($fbu), + array('about_me', + 'affiliations', + 'name', + 'is_app_user', + 'pic_big', + 'profile_update_time', + 'status', + )); + if (count($info)) { + $output .= theme('fb_app_user_info', $fb_app, $info[0]); + } + } + } + } + + } + + // TODO: allow modules a way to customize the output of this function. + return $output; +} + +// There are several pages where we don't want to automatically create a new +// account or use an account configured for this app. +function _fb_user_special_page() { + return (arg(0) == 'user' || arg(0) == 'fb_user'); +} + +function _fb_user_track($fb, $fb_app) { + // Keep track of all our app users. We need this info when updating + // profiles during cron. We keep session keys in case user has an + // infinite session, and we can actually log in as them during cron. + // TODO: is this a violation of facebook terms? + db_query("REPLACE INTO {fb_user_app} (apikey, fbu, added, time_access, session_key, session_key_expires) VALUES ('%s', %d, %d, %d, '%s', %d)", + $fb_app->apikey, fb_facebook_user(), + $fb->api_client->users_isAppAdded(), time(), + $fb->api_client->session_key, $_REQUEST['fb_sig_expires'] + ); +} + /** * Implementation of hook_fb. */ @@ -47,11 +320,12 @@ ($fb_user_data['create_account'] == FB_USER_OPTION_CREATE_LOGIN)) { // Check if the local account is already made. - if ($user->fbu != fb_facebook_user()) { + if ($user->fbu != fb_facebook_user() && (!_fb_user_special_page())) { // We need to make a local account for this facebook user. - $user = fb_user_create_local_user(fb_facebook_user(), - $fb_user_data['unique_account'], - array($fb_user_data['new_user_rid'] => TRUE)); + $user = fb_user_create_local_user($fb_app, fb_facebook_user(), + array('app_specific' => $fb_user_data['unique_account'], + 'roles' => array($fb_user_data['new_user_rid'] => TRUE), + )); watchdog('fb_user', t("Created new user !username for application %app", array('!username' => theme('username', $user), '%app' => $fb_app->label))); @@ -76,17 +350,11 @@ // Perhaps we need to do a goto??? } - // Keep track of all our app users. We need this info when updating - // profiles during cron. We keep session keys in case user has an - // infinite session, and we can actually log in as them during cron. - // TODO: is this a violation of facebook terms? - db_query("REPLACE INTO {fb_user_app} (apikey, fbu, added, time_access, session_key, session_key_expires) VALUES ('%s', %d, %d, %d, '%s', %d)", - $fb_app->apikey, fb_facebook_user(), - $fb->api_client->users_isAppAdded(), time(), - $fb->api_client->session_key, $_REQUEST['fb_sig_expires'] - ); - - if (!$user->uid) { + // Keep a record of when user accesses app, and whether they have added it. + _fb_user_track($fb, $fb_app); + + // Don't mess with the user info if the user is visiting the login pages or submitting a form (i.e. the login form). + if (!$user->uid && !_fb_user_special_page() && !$_REQUEST['form_id']) { if ($fbu = fb_facebook_user()) { $uid = $fb_app_data['fb_user']['logged_in_uid']; } @@ -103,9 +371,11 @@ } } - // This is an experiment. Trying to get login links to prompt for a facebook login. + // We don't want user's who are not logged in (in the facebook sense) to + // login locally. So let's make sure they've added the app before doing + // anything related to Drupal accounts. if (strpos($_GET['q'],'user/login') === 0) { - $fb->require_login(); + $fb->require_add(); } else if (strpos($_GET['q'],'user/register') === 0) { $fb->require_add(); @@ -138,6 +408,21 @@ } +function fb_user_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { + if ($op == 'view' && $node->type == 'fb_app') { + dpm(func_get_args(), 'fb_user_nodeapi'); + if (user_access('administer fb apps')) { + $fb_app = $node->fb_app; + $output = theme('dl', array(t('Post-add URL') => "http://apps.facebook.com/{$fb_app->canvas}/".FB_USER_POST_ADD_PATH."?destination=", + t('Post-remove URL') => fb_local_url(FB_USER_POST_REMOVE_PATH), + t('Add URL') => "http://apps.facebook.com/{$fb_app->canvas}/".FB_USER_SYNC_PATH . '
' . t('(Send an authenticated user to this URL so that local account will be authmapped to facebook account.)'))); + $node->content['fb_user'] = array('#value' => $output, + '#weight' => 2, + ); + } + } +} + function fb_user_form_alter($form_id, &$form) { //drupal_set_message("fb_user_form_alter($form_id) " . dpr($form, 1)); @@ -173,7 +458,7 @@ $form['fb_app_data']['fb_user']['not_logged_in_uid'] = array('#type' => 'textfield', '#title' => t('Not logged in user (uid)'), - '#description' => t('If allowing non-logged in users, when such a user visits the site, which Drupal user should they be treated as? Use 0 for the anonymous user.'), + '#description' => t('If allowing non-logged in users, when such a user visits the site, which Drupal user should they be treated as? Use 0 for the anonymous user (recommended - this feature is experimental and likely to disappear).'), '#default_value' => $fb_user_data['not_logged_in_uid'], ); $form['fb_app_data']['fb_user']['logged_in_uid'] = @@ -186,7 +471,7 @@ $form['fb_app_data']['fb_user']['create_account'] = array('#type' => 'radios', '#title' => t('Create Local Account'), - '#description' => t('When logged-in facebook user visits this app, should we create a local account for them?'), + '#description' => t('This option will create a local account automatically and map the local account to the Facebook account. This happens whenever the user visits a canvas page, except user/ pages and the landing page for anonymous users.'), '#options' => array(FB_USER_OPTION_CREATE_NEVER => t('Never (I\'ll map the accounts some other way)'), FB_USER_OPTION_CREATE_LOGIN => t('If user has logged in'), FB_USER_OPTION_CREATE_ADD => t('If user has added this app'), @@ -195,6 +480,16 @@ '#default_value' => $fb_user_data['create_account'], '#required' => TRUE, ); + $form['fb_app_data']['fb_user']['map_account'] = + array('#type' => 'radios', + '#title' => t('Map Accounts'), + '#description' => t('This option maps a Facebook account to a local account, when a user logs in or registers via a canvas page.'), + '#options' => array(FB_USER_OPTION_MAP_NEVER => t('Never map accounts'), + FB_USER_OPTION_MAP_ALWAYS => t('Map account when user logs in or registers'), + ), + '#default_value' => $fb_user_data['map_account'], + '#required' => TRUE, + ); // TODO: prompt for role with a select. Don't make user figure out id $form['fb_app_data']['fb_user']['new_user_rid'] = array('#type' => 'textfield', @@ -213,58 +508,202 @@ } } +/** + * Implementation of hook_user. + */ +function fb_user_user($op, &$edit, &$account, $category = NULL) { + global $fb, $fb_app; // Set only in canvas pages. -function fb_user_create_local_user($fbu = NULL, $app_specific = FALSE, $roles = array()) { - global $fb; + // if the user has logged in via a facebook canvas page, a number of parameters are included in the request which allow us to determine the application and facebook user id. + // TODO: do we need additional validation here? + if ($_REQUEST['fb_sig']) { + //watchdog('debug', dprint_r($_REQUEST, 'fb_user_user request')); + $fb_app = fb_get_app(array('apikey' => $_REQUEST['fb_sig_api_key'])); + $fbu = $_REQUEST['fb_sig_user']; + } - if (!$fbu) - $fbu = fb_facebook_user(); - if (!$fbu) - return; // User not logged into facebook, can't create local account + if ($fb_app != NULL && $op == 'insert' || $op == 'login') { + // A facebook user has logged in. We can map the two acounts together. + $fb_app_data = fb_app_get_data($fb_app); + $fb_user_data = $fb_app_data['fb_user']; // our configuration + if ($fbu && + $fb_user_data['map_account'] == FB_USER_OPTION_MAP_ALWAYS) { + $authname = _fb_user_authname($fb_app, $fbu); + + if ($op == 'insert') { + // User has registered, we set up the authmap this way... + $edit['authname_fb_user'] = $authname; + } + else if ($op == 'login') { + // On login, we set up the map this way... + user_set_authmaps($account, array('authname_fb_user' => $authname)); + } + // debug + //watchdog('debug', "fb_user_user created authmap on $op. $account->uid --> $authname"); - $info = $fb->api_client->users_getInfo($fbu, array('first_name', 'last_name')); - $username = $info[0]['first_name'] .' '. $info[0]['last_name']; - - // debugging. - //drupal_set_message("Facebook knows you as $username ($fbu)"); - + // TODO: if the app has a role, make sure the user gets that role. XXX + } + } + + // Add tabs on user edit pages to manage maps between local accounts and facebook accounts. + if ($op == 'categories') { + $items[] = array('name' => 'fb_user', + 'title' => t('Facebook Applications'), + 'weight' => 0); + return $items; + } + else if ($op == 'form' && $category == 'fb_user') { + $form['map'] = array('#tree' => TRUE); + // Iterate through all facebook apps, because they do not all use the same + // map scheme. + $result = _fb_app_query_all(); + while ($fb_app = db_fetch_object($result)) { + $fb_app_data = fb_app_get_data($fb_app); + $fb_user_data = $fb_app_data['fb_user']; // our configuration + + $fbu = _fb_user_get_fbu($account->uid, $fb_app); + $is_added = FALSE; + if ($fbu && !$info[$fbu]) { + // The drupal user is a facebook user. Now, learn more from facebook. + $fb = fb_api_init($fb_app, FB_FBU_ANY); + $info[$fbu] = $fb->api_client->users_getInfo(array($fbu), + array('name', + 'is_app_user', + )); + dpm($info[$fbu], "Info from facebook for $fbu"); + } + if ($fb_user_data['unique_account']) { + $form['map'][$fb_app->nid] = array('#type' => 'checkbox', + '#title' => $fb_app->title, + '#default_value' => $fbu, + ); + } + else { + $shared_maps[] = $fb_app->title; + $shared_fbu = $fbu; // Same for all shared apps. + } + } + if ($shared_maps) { + $form['map']['global'] = array('#type' => 'checkbox', + '#title' => implode('
', $shared_maps), + '#default_value' => $shared_fbu, + ); + if ($info[$shared_fbu]) { + $data = $info[$shared_fbu][0]; + $fb_link = l($data['name'], 'http://www.facebook.com/profile.php', NULL, 'id='.$data['uid']); + + $form['map']['global']['#description'] .= t('Local account (!username) corresponds to !profile_page on Facebook.com.', + array('!username' => theme('username', $account), + '!profile_page' => $fb_link)); + } + } + return $form; + } +} + +function theme_fb_app_name_with_links($fb_app, $is_added = NULL) { + $output = $fb_app->title; + // TODO add link to about page + if ($is_added === FALSE) { + $links[] = 'add link'; //XXX + } + return $output; +} + + +/** + * Helper function to create an authname for the authmap table. + * + * When a single Drupal instance hosts multiple Facebook apps, the apps can + * share the same mapping, or each have their own. + */ +function _fb_user_authname($fb_app, $fbu) { + $fb_app_data = fb_app_get_data($fb_app); + $fb_user_data = $fb_app_data['fb_user']; // our configuration + $app_specific = $fb_user_data['unique_account']; // map fbu to uid, include apikey if user is app_specific if ($app_specific) // would rather use the shorter app id (not apikey), but no way to query it $authmap = "$fbu-$fb_app->apikey@facebook.com"; else $authmap = "$fbu@facebook.com"; + + return $authmap; +} + +/** + * Creates a local Drupal account for the specified facebook user id. + * + * @param fbu + * The facebook user id corresponding to this account. + * + * @param config + * An associative array with user configuration. Possible values include: + * 'app_specific' - Set to true if the same facebook id might correspond to different local accounts, depending on which apps the user has used. Set to false if the user shares one local account across facebook apps. + * 'roles' - an array with keys corresponding to role ids the new user should receive. + */ +function fb_user_create_local_user($fb_app, $fbu, + $config = array()) { + + // TODO: ensure $fbu is a real user, not FB_FB_ANY or FB_FBU_CURRENT + + // debugging. + //drupal_set_message("Facebook knows you as $username ($fbu)"); + + $authmap = _fb_user_authname($fb_app, $fbu); $account = user_external_load($authmap); if (!$account) { // Create a new user in our system - // TODO: handle case when username is already taken. - $user_default = array('name' => $username, + + // We need a username that will not collide with any already in our + // system. Could use $authmap, but this will be just slightly more + // user-friendly. + if ($config['app_specific'] && !$config['username']) + $config['username'] = "$fbu-$fb_app->label@facebook"; + else + $config['username'] = "$fbu@facebook"; + + // Allow third-party module to adjust any of our settings before we create + // the user. + $config = _fb_invoke($fb_app, FB_OP_PRE_USER, + $config, array('fbu' => $fbu)); + + // TODO: double-check that username is not taken. + $user_default = array('name' => $config['username'], 'pass' => user_password(), - 'init' => db_escape_string($username), + 'init' => db_escape_string($config['username']), 'status' => 1, 'authname_fb_user' => $authmap, ); + $user_default['roles'][DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - foreach ($roles as $rid => $value) - if ($rid) - $user_default['roles'][$rid] = $value; + if (count($config['roles'])) + foreach ($config['roles'] as $rid => $value) + if ($rid) + $user_default['roles'][$rid] = $value; $user_default['fbu'] = $fbu; // Will get saved as user data. - + $account = user_save('', $user_default); - } + // Is there a need for POST_USER hook_fb call? Or is hook_user sufficient? + } + if (!$account->fbu) { // This should only happen on older, automatically created accounts. $account->fbu = $fbu; user_save($account, array('fbu' => $fbu)); } - + return $account; } +/** + * Given an app and facebook user id, return the corresponding local user. + */ function fb_user_get_local_user($fbu, $fb_app) { + // TODO: this query probably needs to search for one authname or the other, not both. $result = db_query("SELECT am.* FROM authmap am WHERE am.authname='%s' OR am.authname='%s' ORDER BY am.authname", "$fbu-$fb_app->apikey@facebook.com", "$fbu@facebook.com"); if ($data = db_fetch_object($result)) { @@ -297,8 +736,10 @@ $cache[$uid]['global'] = $parts[0]; } } - - if ($cache[$uid][$fb_app->apikey]) + // Return either the global or the app-specific mapping, depending on the app configuration. + $fb_app_data = fb_app_get_data($fb_app); + $fb_user_data = $fb_app_data['fb_user']; // our configuration + if ($fb_user_data['unique_account']) // Return the app-specific mapping return $cache[$uid][$fb_app->apikey]; else