Index: themes/fb_fbml/styles.tpl.php
===================================================================
--- themes/fb_fbml/styles.tpl.php (revision 941)
+++ themes/fb_fbml/styles.tpl.php (working copy)
@@ -87,8 +87,8 @@
.header h1 {clear: both; margin: 10px 10px 20px 10px;}
.header {
- border-bottom: 1px solid #cccccc;
margin-bottom: 0px;
+ border: 1px solid #cccccc;
}
.admin_sidebar {
@@ -188,6 +188,7 @@
background-image: url(http://www.facebook.com/images/newsfeed_line.gif);
background-repeat: repeat-y;
background-attachment: scroll;
+ float: left;
}
div.clear {clear: both;}
Index: fb_app.install
===================================================================
--- fb_app.install (revision 941)
+++ fb_app.install (working copy)
@@ -15,6 +15,7 @@
nid int(11) unsigned NOT NULL,
label varchar(128) NOT NULL,
apikey varchar(128) NOT NULL,
+id varchar(128) NOT NULL,
secret varchar(128) NOT NULL,
canvas varchar(128) NOT NULL,
require_login int(4) NOT NULL,
@@ -55,4 +56,10 @@
drupal_set_message(t('Facebook Application module installed. Please grant yourself permissions and then browse to Create Content => Facebook Application to get started.', array('!perm' => url('admin/user/access'), '!create' => url('node/add/fb-app'))));
}
+
+function fb_app_update_1() {
+ // Add id field
+ $ret[] = update_sql('ALTER TABLE {fb_app} ADD id varchar(128) NOT NULL');
+ return $ret;
+}
?>
\ No newline at end of file
Index: fb_user.module
===================================================================
--- fb_user.module (revision 941)
+++ fb_user.module (working copy)
@@ -8,6 +8,75 @@
define('FB_USER_OPTION_CREATE_LOGIN', 2);
define('FB_USER_OPTION_CREATE_ADD', 3);
+define('FB_USER_OPTION_MAP_NEVER', 1);
+define('FB_USER_OPTION_MAP_ALWAYS', 2);
+
+define('FB_USER_SYNC_PATH', 'fb_user_sync');
+
+function fb_user_menu($may_cache) {
+ $items = array();
+ if ($may_cache) {
+ global $user;
+ $items[] = array('path' => FB_USER_SYNC_PATH,
+ 'title' => t('Facebook Account'),
+ 'access' => TRUE, // XXX
+ 'callback' => 'fb_user_sync_cb',
+ );
+ }
+
+ return $items;
+}
+
+// This is a work in progress. Not really working, in part because facebook does not actually forward the user to the right place after they add the app.
+function fb_user_sync_cb($fb_app_nid, $uid = NULL) {
+ global $user;
+
+ // A user can only sync their own account.
+ if (!$user->uid || ($uid && !($user->uid == $uid))) {
+ drupal_access_denied();
+ exit();
+ }
+ else if (!$uid) {
+ // redirect to user-specific path
+ drupal_goto(FB_USER_SYNC_PATH . "/$fb_app_nid/$user->uid");
+ }
+
+ // Find the fb_app.
+ $fb_app_node = node_load($fb_app_nid);
+
+ if ($fb_app_node && $fb_app_node->status && $fb_app_node->type == 'fb_app') {
+ $fb_app = $fb_app_node->fb_app;
+ // Associate the user account with the app...
+ // First, connect to facebook.
+ $fb = fb_api_init($fb_app, FB_FBU_ANY);
+
+ dpm($fb->get_add_url(), "fb_user_sync_path add url");
+ // Next, require that the user has added this app.
+ //$fbu = $fb->require_add();
+ $fb->redirect($fb->get_login_url('node/1')); // test XXX
+ if ($fbu) {
+ // The user has added the app, let's associate the accounts.
+ $fb_app_data = fb_app_get_data($fb_app);
+ $fb_user_data = $fb_app_data['fb_user']; // our configuration
+ if ($fbu = fb_facebook_user() &&
+ $fb_user_data['map_account'] == FB_USER_OPTION_MAP_ALWAYS) {
+ $authname = _fb_user_authname($fb_app, $fbu,
+ array('app_specific' => $fb_user_data['unique_account']));
+ //dpm($authname, "fb_user_sync_path mapping user $user->uid to facebook acount");
+ user_set_authmaps($account, array('authname_fb_user' => $authname));
+ }
+ }
+ }
+ else {
+ drupal_not_found();
+ exit();
+ }
+
+ $output = "XXX"; // Work in progress
+ return $output;
+}
+
+
/**
* Implementation of hook_fb.
*/
@@ -47,11 +116,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() && (arg(0) != 'user')) {
// 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)));
@@ -86,7 +156,8 @@
$fb->api_client->session_key, $_REQUEST['fb_sig_expires']
);
- if (!$user->uid) {
+ // 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 && arg(0) != 'user' && !$_REQUEST['form_id']) {
if ($fbu = fb_facebook_user()) {
$uid = $fb_app_data['fb_user']['logged_in_uid'];
}
@@ -103,9 +174,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();
@@ -173,7 +246,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 +259,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 +268,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 +296,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,
+ array('app_specific' => $fb_user_data['unique_account']));
+
+ 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, $config) {
+ //$args = func_get_args();
+ //watchdog('debug', dprint_r($args, 1));
// map fbu to uid, include apikey if user is app_specific
- if ($app_specific)
+ if ($config['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, $config);
+
$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);
- $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 +524,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
Index: fb_app.module
===================================================================
--- fb_app.module (revision 941)
+++ fb_app.module (working copy)
@@ -140,6 +140,12 @@
'#default_value' => $node->fb_app->canvas,
'#description' => t('Type only the part that comes after "http://apps.facebook.com/"'),
);
+ $form['fb_app']['id'] = array('#type' => 'textfield',
+ '#title' => t('Facebook App ID'),
+ '#required' => FALSE,
+ '#default_value' => $node->fb_app->id,
+ '#description' => t('To learn this number, visit your app\'s About Page (or other links from Facebook\'s Developer App). The URL will end in ?id=1234... Enter the number that follows "?id=" here.'),
+ );
// fb_app_data is a placeholder to make it easier for other module to attach
// various settings to the node.
@@ -208,11 +214,17 @@
}
function theme_fb_app($data) {
- $canvas_url = "http://apps.facebook.com/$data->canvas";
+ $links[] = l(t('Canvas URL'), "http://apps.facebook.com/$data->canvas");
+ if ($data->id) {
+ $links[] = l(t('About page'), "http://www.facebook.com/apps/application.php",
+ array(), "id=$data->id");
+ }
+
return theme('dl', array(t('Label') => $data->label,
t('API Key') => $data->apikey,
t('Secret') => $data->secret,
- t('Canvas URL') => l($canvas_url, $canvas_url),
+ t('Links') => theme('item_list', $links),
+
t('Callback URL') => t("If using clean urls (recommended), use %clean. Otherwise, use %dirty.",
array("%clean" => url('', NULL, NULL, TRUE),
"%dirty" => url("", "q=", NULL, TRUE),
@@ -246,8 +258,9 @@
$data = serialize($node->fb_app_data);
- db_query("INSERT INTO {fb_app} (nid, label, apikey, secret, canvas, require_login, create_account, unique_account, rid, data) VALUES (%d, '%s', '%s', '%s', '%s', %d, %d, %d, %d, '%s')",
+ db_query("INSERT INTO {fb_app} (nid, label, apikey, secret, id, canvas, require_login, create_account, unique_account, rid, data) VALUES (%d, '%s', '%s', '%s', '%s', %d, %d, %d, %d, '%s')",
$node->nid, $fb_app->label, $fb_app->apikey, $fb_app->secret,
+ $fb_app->id,
$fb_app->canvas, $fb_app->require_login, $fb_app->create_account,
$fb_app->unique_account, $role->rid, $data
);
@@ -281,8 +294,9 @@
$data = serialize($node->fb_app_data);
// TODO: test if role name needs to change
- db_query("UPDATE {fb_app} SET label='%s', apikey='%s', secret='%s', canvas='%s', require_login=%d, create_account=%d, unique_account=%d, data='%s' WHERE nid=%d",
+ db_query("UPDATE {fb_app} SET label='%s', apikey='%s', secret='%s', id='%s', canvas='%s', require_login=%d, create_account=%d, unique_account=%d, data='%s' WHERE nid=%d",
$fb_app->label, $fb_app->apikey, $fb_app->secret,
+ $fb_app->id,
$fb_app->canvas, $fb_app->require_login, $fb_app->create_account,
$fb_app->unique_account, $data,
$node->nid);
@@ -342,24 +356,20 @@
$result = db_query("SELECT fb.*, n.title FROM {fb_app} fb INNER JOIN {node} n ON n.nid=fb.nid WHERE status=1");
return $result;
}
+function _fb_app_query_by_label($label) {
+ $result = db_query("SELECT fb.*, n.title FROM {fb_app} fb INNER JOIN {node} n ON n.nid=fb.nid WHERE n.status=1 AND fb.label = '%s'",
+ $label);
+ return $result;
+}
/**
* Implementation of hook_user.
*/
function fb_app_user($op, &$edit, &$account, $category = NULL) {
$items = array();
- if ($op == 'categories') {
+ if ($op == 'view') {
$result = _fb_app_query_all();
while ($fb_app = db_fetch_object($result)) {
- $items[] = array('name' => $fb_app->label,
- 'title' => $fb_app->title,
- 'weight' => 0);
- }
- return $items;
- }
- else if ($op == 'view') {
- $result = _fb_app_query_all();
- while ($fb_app = db_fetch_object($result)) {
// Learn this user's FB id
$fbu = fb_get_fbu($account->uid, $fb_app);
if ($fbu) {
Index: fb.module
===================================================================
--- fb.module (revision 941)
+++ fb.module (working copy)
@@ -273,8 +273,9 @@
}
function _fb_handling_form() {
- // Is this a good way to test form submit?
- if ($_REQUEST['form_token'] && $_REQUEST['fb_sig'])
+ global $fb;
+ // Test whether a form has been submitted via facebook canvas page.
+ if (!$fb && $_REQUEST['form_id'] && $_REQUEST['fb_sig'])
return TRUE;
}
@@ -432,7 +433,7 @@
function fb_form_alter($form_id, &$form) {
- // We will send all for submission directly to us, not via
+ // We will send all form submission directly to us, not via
// app.facebook.com/whatever. Since we've tricked drupal into writing our
// URLs as if we were there, we need to untrick the form action.
if ($_REQUEST['fb_sig']) {