'fb_connect_receiver', 'type' => MENU_CALLBACK, 'access callback' => TRUE, ); // Ajax helper $items['fb_connect/js'] = array( 'page callback' => 'fb_connect_js_cb', 'type' => MENU_CALLBACK, 'access callback' => TRUE, ); // Admin pages $items['admin/fb/fb_connect'] = array( 'title' => 'Facebook Connect', 'description' => t('Configure Facebook Connect'), 'page callback' => 'drupal_get_form', 'page arguments' => array('fb_connect_admin_settings'), 'access arguments' => array('administer fb apps'), 'file' => 'fb_connect.admin.inc', ); return $items; } function fb_connect_js_cb() { $extra = fb_invoke(FB_OP_CONNECT_JS_INIT, NULL, array()); $extra_js = implode("\n", $extra); print $extra_js; exit(); } /** * Without a receiver file, cross-domain javascript will not work. * * In their infinite wisdom, facebook has decreed that the URL for * this static page be in the same place as the app's callback URL. * So we have to make a Drupal callback for what would otherwise be a * simple file. * * Supports SSL, http://wiki.developers.facebook.com/index.php/Facebook_Connect_Via_SSL. */ function fb_connect_receiver() { global $facebook_config; $src_suffix = $facebook_config['debug'] ? '.debug' : '' ; $src = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https://ssl.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver$src_suffix.js" : "http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver$src_suffix.js" ; $output = << HTML; print $output; die(); // prevent Drupal from writing anything else. } /** * Returns an apikey, if the current page has facebook connect enabled. */ function fb_connect_current_apikey() { // TODO: return false on canvas pages if ($GLOBALS['fb_connect_apikey']) // Learned apikey from cookies. return $GLOBALS['fb_connect_apikey']; else if ($apikey = variable_get(FB_CONNECT_VAR_PRIMARY, NULL)) { // Primary connect app. return $apikey; } } /** * Prepare for fbConnect use. Because a single Drupal might support * multiple apps, we don't know in advance which is the fbConnect app. * Theoretically, it might be possible to simultaneously use multiple * apps and fbConnect, but my suspicion is facebook would throw a * total hissy-fit if you tried. */ function fb_connect_app_init($fb_app) { if ($fb = fb_api_init($fb_app, FB_FBU_CURRENT)) { $fbu = $fb->get_loggedin_user(); // TODO: sometimes this method indicates we are logged in when we really are not. Find a way to verify. if ($fbu && (!isset($GLOBALS['fb_app']) || $GLOBALS['fb_app']->apikey != $fb_app->apikey)) { // The user has authorized the app and we now know something about them. Use a hook to trigger the actions of other modules. fb_invoke(FB_OP_APP_IS_AUTHORIZED, array('fbu' => $fbu, 'fb_app' => $fb_app, 'fb' => $fb)); } // Store state in session if (!isset($_SESSION['fb_connect'])) { $_SESSION['fb_connect'] = array(); } $_SESSION['fb_connect'][$fb_app->apikey] = $fbu; } return $fb; } /** * Are we already logged in to fbConnect? */ function fb_connect_already_loggedin($fb_app) { if ($_SESSION['fb_connect']) return $_SESSION['fb_connect'][$fb_app->apikey]; return FALSE; } /** * Which apps are fbConnect enabled? */ function fb_connect_enabled_apps() { // We do a bit of work for each enabled app, so really we want to restrict this list to only apps which have been "turned on". // But for now we're lazy and just get the list of all apps. $apps = fb_get_all_apps(); return $apps; } /** * Implementation of hook_fb(). */ function fb_connect_fb($op, $data, &$return) { //dpm(func_get_args(), "fb_connect_fb($op)"); if ($op == FB_OP_CURRENT_APP && !$return) { // This will cause fb.module to set the global $fb when user is logged in via fbConnect. if ($apikey = variable_get(FB_CONNECT_VAR_PRIMARY, NULL)) { $return = fb_get_app(array('apikey' => $apikey)); } } else if ($op == FB_OP_POST_INIT) { if ($apikey = variable_get(FB_CONNECT_VAR_PRIMARY, NULL)) { if ($data['fb_app']->apikey == $apikey) { $fb_app = $data['fb_app']; // Init Facebook javascript for primary app _fb_connect_add_js(); fb_connect_require_feature('XFBML', $fb_app); // fb_connect_init_option('reloadIfSessionStateChanged', TRUE, $fb_app); fb_connect_init_option('ifUserConnected', "{FB_Connect.on_connected}", $fb_app); fb_connect_init_option('ifUserNotConnected', "{FB_Connect.on_not_connected}", $fb_app); } } } else if ($op == FB_OP_CONNECT_JS_INIT) { foreach (fb_connect_init_js() as $js) { $return[] = $js; } } else if ($op == FB_OP_SET_PROPERTIES) { // We need to set the Facebook Connect URL, but currently Facebook's APIs do not allow it. $return['connect_url'] = fb_connect_get_connect_url($data['fb_app']); } else if ($op == FB_OP_LIST_PROPERTIES) { $return[t('Connect URL')] = 'connect_url'; } } /** * This wrapper function around drupal_add_js() ensures that our * settings are added once and only once when needed. */ function _fb_connect_add_js() { static $just_once; if (!isset($just_once)) { $base = drupal_get_path('module', 'fb_connect'); if (isset($GLOBALS['fb_connect_apikey'])) { // If here, fb connect cookies are set. $fbu = $_COOKIE[$GLOBALS['fb_connect_apikey'] . "_user"]; } else { $fbu = NULL; } drupal_add_js(array('fb_connect' => array( 'front_url' => url(''), 'fbu' => $fbu, // Is there a better way to keep track of iframe? 'in_iframe' => (isset($_REQUEST['fb_sig_in_iframe']) && $_REQUEST['fb_sig_in_iframe']), ), ), 'setting'); drupal_add_js($base . '/fb_connect.js'); $just_once = TRUE; } } /** * Implementation of hook_user * * On logout, redirect the user so facebook can expire their session. * Should be a facebook API to do this, but there's none I know of. */ function fb_connect_user($op, &$edit, &$account, $category = NULL) { if ($op == 'logout') { // Affect connect pages, but not canvas pages. if (fb_is_fbml_canvas() || fb_is_iframe_canvas()) return; $apps = fb_connect_enabled_apps(); foreach ($apps as $fb_app) { try { $fb = fb_api_init($fb_app, FB_FBU_CURRENT); if ($fb && $fb->api_client->session_key) { // Log out of facebook $session_key = $fb->api_client->session_key; // Figure out where to send the user. if ($_REQUEST['destination']) { $next = url($_REQUEST['destination'], array('absolute' => TRUE)); // It pains me to muck with $_REQUEST['destination'], but facebook's weak-ass API leaves us little choice. unset($_REQUEST['destination']); } else { $next = url('', array('absolute' => TRUE)); } // http://forum.developers.facebook.com/viewtopic.php?id=21879 // Use next parameter to expire session. drupal_goto("http://www.facebook.com/logout.php?app_key={$fb_app->apikey}&session_key={$session_key}&next=" . $next); } } catch (Exception $e) { fb_log_exception($e, t('Failed to log out of fbConnect session')); } } } } function fb_connect_exit($url = NULL) { if (isset($GLOBALS['fb_connect_logging_out'])) { global $fb; session_write_close(); // drupal_goto calls this, so why not us? if (!isset($url)) $url = url('', NULL, NULL, TRUE); $fb->logout($url); } } /** * Allows other modules to specify which Facebook Connect features are * required. This will affect how the FB_RequireFeatures javascript method is * called. */ function fb_connect_require_feature($feature = NULL, $fb_app = NULL) { if ($feature && !isset($fb_app) && isset($GLOBALS['fb_app'])) $fb_app = $GLOBALS['fb_app']; // some features may apply without an app, but for now let's enforce that an app is required. if ($feature && !isset($fb_app)) return; static $features; if (!$features) $features = array(); if ($fb_app && !isset($features[$fb_app->apikey])) $features[$fb_app->apikey] = array('fb_app' => $fb_app, 'features' => array()); if ($feature) $features[$fb_app->apikey]['features'][$feature] = $feature; return $features; } /** * Add an option when initializing facebook's javascript api. */ function fb_connect_init_option($option = NULL, $value = NULL, $fb_app = NULL) { if ($option && !isset($fb_app)) $fb_app = $GLOBALS['fb_app']; if ($option && !$fb_app) return; static $options; if (!$options) $options = array(); if ($fb_app && !isset($options[$fb_app->apikey])) { $options[$fb_app->apikey] = array(); } if ($option) $options[$fb_app->apikey][$option] = $value; return $options; } /** * Add javascript to a facebook connect page. * * Use this to add calls to facebook JS, * http://wiki.developers.facebook.com/index.php/JS_API_Index. * * We use Drupal's cache to store the javascript until it is rendered * to a page. This approach is analogous to drupal_set_message * storing data temporarily in the session. We use cache instead of * session, because the session is not shared between Facebook's event * callbacks and regular page requests. * */ function fb_connect_init_js($js = NULL) { $fbu = fb_facebook_user(); $fb_app = $GLOBALS['fb_app']; $cid = 'fb_connect_init_js_' . $fb_app->apikey . '_' . $fbu; $cache = cache_get($cid, 'cache'); if (!isset($cache->data)) { $cache = new stdClass(); $cache->data = array(); } if ($js) { $cache->data[] = $js; cache_set($cid, $cache->data, 'cache', time() + 60000); // Update cache } else if ($js === NULL) { cache_clear_all($cid, 'cache'); } return $cache->data; } /** * Include facebook javascript in the footer of the current page. */ function fb_connect_footer($is_front) { // Do nothing on FBML pages if (fb_is_fbml_canvas()) { return; } global $base_path; $fb_feature_src = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https://ssl.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" : "http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" ; $feature_data = fb_connect_require_feature(); $option_data = fb_connect_init_option(); if (count($feature_data)) { foreach ($feature_data as $data) { // Give other modules a chance to add javascript which executes after init. $extra = fb_invoke(FB_OP_CONNECT_JS_INIT, $data, array()); $extra_js = implode("\n", $extra); $fb_app = $data['fb_app']; $features = $data['features']; $options = json_encode($option_data[$fb_app->apikey]); // Hack! What's the way to json_encode a function name? $options = str_replace('"{', '', $options); $options = str_replace('}"', '', $options); // drupal_add_js cannot add external javascript, so we use hook_footer instead. $output = ''; $output .= "\n"; $feature_list = '["' . implode('","', $features) . '"]'; // Put together the URL for the receiver. The prefix must be identical to the apps connect URL. $receiver = fb_connect_get_connect_url($fb_app) . "fb_connect/receiver"; $output .= " \n"; } } return $output; } /** * Convenience method to get an apps connect URL. * */ function fb_connect_get_connect_url($fb_app) { // absolute URL with no rewriting applied global $base_url; $suffix = FB_SETTINGS_APP_NID . '/' . $fb_app->nid . '/'; // In regular pages, we need to add the suffix. $url = $base_url . '/' . $suffix; return $url; } function _fb_connect_block_login_defaults() { return array('anon_not_connected' => array( 'title' => t('Facebook Connect'), 'body' => t('Facebook users login here. !button', array('!button' => "")), ), 'user_not_connected' => array( 'title' => t('Facebook Connect'), 'body' => t('Link your account with Facebook. !button', array('!button' => "")), ), 'connected' => array( 'title' => t('Facebook Connect'), 'body' => "", ), ); } /** * Implementation of hook_block. */ function fb_connect_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { $items = array(); foreach (fb_connect_enabled_apps() as $fb_app) { $d = 'login_' . $fb_app->label; $items[$d] = array( 'info' => t('Facebook Connect Login to !app', array('!app' => $fb_app->title)), ); } return $items; } else if ($op == 'configure') { $defaults = variable_get('fb_connect_block_' . $delta, _fb_connect_block_login_defaults()); $form['config'] = array('#tree' => TRUE); foreach(array('anon_not_connected', 'user_not_connected', 'connected') as $key) { $form['config'][$key] = array( '#type' => 'fieldset', // title and description below '#collapsible' => TRUE, '#collapsed' => FALSE, ); $form['config'][$key]['title'] = array( '#type' => 'textfield', '#title' => t('Default title'), //'#description' => t('Default title.'), '#default_value' => $defaults[$key]['title'], ); $form['config'][$key]['body'] = array( '#type' => 'textarea', '#title' => t('Body'), //'#description' => t('Block body'), '#default_value' => $defaults[$key]['body'], ); } $form['config']['anon_not_connected']['#title'] = t('Anonymous user, not connected'); $form['config']['anon_not_connected']['#description'] = t('Settings when local user is Anonymous, and not connected to Facebook. Typically a new account will be created when the user clicks the connect button.'); $form['config']['user_not_connected']['#title'] = t('Registered user, not connected'); $form['config']['user_not_connected']['#description'] = t('Settings when local user is registered, and not connected to Facebook. Typically the facebook id will be linked to the local id after the user clicks the connect button.'); $form['config']['connected']['#title'] = t('Connected user'); $form['config']['connected']['#description'] = t('Settings when local user is connected to Facebook. You may render facebook\'s logout button, and/or information about the user.'); $form['config']['connected']['body']['#description'] .= t('Note that !fbu will be replaced with the user\'s facebook id.'); $form['config']['format'] = filter_form($defaults['format']); $form['config']['format']['#description'] .= t('Format selected will apply to all body fields above. Be sure to select a format which allows FBML tags!'); $form['config']['format']['#collapsed'] = FALSE; return $form; } else if ($op == 'save') { $edit['config']['format'] = $edit['format']; variable_set('fb_connect_block_' . $delta, $edit['config']); } else if ($op == 'view') { if (strpos($delta, 'login_') === 0) { // Login block $label = substr($delta, 6); // length of 'login_' $fb_app = fb_get_app(array('label' => $label)); $fb = fb_connect_app_init($fb_app); $fbu = $fb->get_loggedin_user(); fb_connect_require_feature('XFBML', $fb_app); //fb_connect_init_option('reloadIfSessionStateChanged', TRUE, $fb_app); //fb_connect_init_option('doNotUseCachedConnectState', TRUE, $fb_app); $base = drupal_get_path('module', 'fb_connect'); _fb_connect_add_js(); $defaults = variable_get('fb_connect_block_' . $delta, _fb_connect_block_login_defaults()); if ($fbu) { $subject = $defaults['connected']['title']; $content = $defaults['connected']['body']; // substitute %fbu $content = str_replace('!fbu', $fbu, $content); } else if ($GLOBALS['user']->uid > 1) { $subject = $defaults['user_not_connected']['title']; $content = $defaults['user_not_connected']['body']; } else if ($GLOBALS['user']->uid == 1) { $subject = $defaults['user_not_connected']['title']; $content = '' . t('Facebook Connect login disabled for user #1.') . ''; } else { $subject = $defaults['anon_not_connected']['title']; $content = $defaults['anon_not_connected']['body']; } // If user has changed defaults, run filter if (isset($defaults['format'])) { $subject = check_plain($subject); $content = check_markup($content, $defaults['format'], FALSE); } $block = array( 'subject' => $subject, 'content' => $content, ); return $block; } } } function fb_connect_form_alter(&$form, &$form_state, $form_id) { // Add our settings to the fb_app edit form. if (isset($form['fb_app_data'])) { $node = $form['#node']; $fb_app_data = fb_app_get_data($node->fb_app); $fb_connect_data = $fb_app_data['fb_connect']; $form['fb_app_data']['fb_connect'] = array( '#type' => 'fieldset', '#title' => 'Facebook Connect', '#tree' => TRUE, '#collapsible' => TRUE, '#collapsed' => $node->nid ? TRUE : FALSE, ); $form['fb_app_data']['fb_connect']['primary'] = array( '#type' => 'checkbox', '#title' => t('Primary'), '#description' => t('Initialize fbConnect javascript on all (non-canvas) pages. If this site supports multiple Facebook Apps, this may be checked for at most one.'), '#default_value' => $fb_connect_data['primary'], ); if ($primary_apikey = variable_get(FB_CONNECT_VAR_PRIMARY, NULL)) { if ($primary_apikey != $node->fb_app->apikey) { $primary = fb_get_app(array('apikey' => $primary_apikey)); $form['fb_app_data']['fb_connect']['primary']['#description'] .= '
' . t('Note that checking this will replace %app as the primary Facebook Connect app.', array('%app' => $primary ? $primary->title : $primary_apikey)); } } } // Add button to login form. if (($form_id == 'user_login_block' || $form_id == 'user_login') && !fb_is_fbml_canvas()) { if ($apikey = variable_get(FB_CONNECT_VAR_LOGIN, NULL)) { $fb_app = fb_get_app(array('apikey' => $apikey)); // Ensure that facebook javascript is in the page closure. fb_connect_require_feature('XFBML', $fb_app); fb_connect_init_option('ifUserConnected', "{FB_Connect.on_connected}", $fb_app); fb_connect_init_option('ifUserNotConnected', "{FB_Connect.on_not_connected}", $fb_app); // Add button to the form. $form['fb_connect_button'] = array( '#type' => 'markup', '#value' => 'Connect with Facebook', '#prefix' => '
', '#suffix' => '
', '#weight' => 4, // LOG IN button weight is 2. ); } if ($form_id == 'user_login') { // On login, we must leave the user/login page or else get access denied. drupal_add_js(array('fb_connect' => array('destination' => url('user'))), 'setting'); } } } /** * Implementation of hook_nodeapi(). * * Set a variable if this is the primary connect app. */ function fb_connect_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { if (($op == 'insert' || $op == 'update') && $node->type == 'fb_app') { //dpm(func_get_args(), "fb_connect_nodeapi($op)"); // debug if ($node->fb_app_data['fb_connect']['primary']) { variable_set(FB_CONNECT_VAR_PRIMARY, $node->fb_app['apikey']); drupal_set_message(t('!node is the primary Facebook Connect application.', array('!node' => l($node->title, 'node/' . $node->nid)))); } else if ($node->fb_app['apikey'] == variable_get(FB_CONNECT_VAR_PRIMARY)) { // This app was the primary one, but the user has unchecked it. variable_set(FB_CONNECT_VAR_PRIMARY, NULL); } } } /** * Implementation of hook_theme_registry_alter(). * * Override theme functions for things that can be displayed using * XFBML. Currently overriding username and user_picture. We rename * the original entries, as we will use them for users without * javascript enabled. * * This hook is not well documented. Who knows what its supposed to * return? No doubt this will need updating with each new version of * Drupal. */ function fb_connect_theme_registry_alter(&$theme_registry) { // Ideally, we'd do this only on themes which will certainly be used for facebook connect pages. if ($theme_registry['username']['type'] == 'module') { // Re-register the original theme function under a new name. $theme_registry['fb_connect_username_orig'] = $theme_registry['username']; // Override theme username $theme_registry['username'] = array( 'arguments' => array('object' => NULL), 'function' => 'fb_connect_theme_username_override', 'type' => 'module', ); } if ($theme_registry['user_picture']['type'] == 'module') { // Re-register the original theme function under a new name. $theme_registry['fb_connect_user_picture_orig'] = $theme_registry['user_picture']; // Override theme username $theme_registry['user_picture'] = array( 'arguments' => array('account' => NULL), 'function' => 'fb_connect_theme_user_picture_override', 'type' => 'module', ); } } /** * Our replacement for theme('user_picture', ...) */ function fb_connect_theme_user_picture_override($account) { // Markup without fb_connect. $orig = theme('fb_connect_user_picture_orig', $account); // Make no changes to FBML pages. if (fb_is_fbml_canvas()) { return $orig; } // Respect Drupal's profile pic, if uploaded. if (isset($account->picture) && $account->picture) { $output = $orig; } else { // First learn the Facebook id if (isset($account->fbu)) { $fbu = $account->fbu; } else if ($pos = strpos($account->name, '@facebook')) { // One option is to load the user object and get the definitive fbu. But that's expensive, so we rely on the NNNNNN@facebook naming convention. $fbu = substr($account->name, 0, $pos); } else { // Sometimes the $account object is not an account at all. Could be a node for example. // (There may be pages where this code is reached many times, and that could be a performance problem.) $fbu = fb_get_fbu($account->uid); } if (!isset($fbu) || !$fbu) $output = $orig; else { $output = theme('fb_connect_fbml_user_picture', $orig, $account, $fbu); } } return $output; } /** * Our replacement for theme('username', ...) */ function fb_connect_theme_username_override($object) { // TODO: does this function need to account for canvas pages? Or can we assume every canvas page theme will override theme_username on its own? $orig = theme('fb_connect_username_orig', $object); // Make no changes to FBML pages. if (fb_is_fbml_canvas()) { return $orig; } else { // Theme the username with XFBML, using original username as backup. return fb_connect_fbml_username($object, $orig); } } /** * Helper function for themes to display the username. */ function fb_connect_fbml_username($object, $orig_username = NULL) { if (!isset($orig_username)) // What to display if javascript disabled. $orig_username = theme_username($object); // First learn the Facebook id if (isset($object->fbu)) { $fbu = $object->fbu; } else if ($pos = strpos($object->name, '@facebook')) { // One option is to load the user object and get the definitive fbu. But that's expensive, so we rely on the NNNNNN@facebook naming convention. $fbu = substr($object->name, 0, $pos); } else { // Experimental. This can be expensive on pages with many comments or nodes! //$fbu = fb_get_fbu($object->uid); } if (isset($fbu) && is_numeric($fbu)) { // Display both orig and XFBML, to degrade gracefully. return theme('fb_connect_fbml_username', $orig_username, $object, $fbu); } else { return $orig_username; } } /** * Implements hook_theme(). * * We use theme function for XFBML username and picture so that the * markup can be relatively easily customized. */ function fb_connect_theme() { return array( 'fb_connect_fbml_username' => array( 'arguments' => array( 'orig_username' => NULL, 'object' => NULL, 'fbu' => NULL, ), ), 'fb_connect_fbml_user_picture' => array( 'arguments' => array( 'orig' => NULL, 'account' => NULL, 'fbu' => NULL, ), ), 'fb_connect_fbml_popup' => array( 'arguments' => array('elements' => NULL), ), ); } function theme_fb_connect_fbml_username($orig_username, $object, $fbu) { return ''.$orig_username.''; } function theme_fb_connect_fbml_user_picture($orig, $account, $fbu) { $output = ''; // Build the markup. // First, the markup if javascript is disabled, degrade gracefully. if ($orig) $output .= ''.$orig.''; // Next markup that will be made visible by javascript. $output .= ''; return $output; } function theme_fb_connect_fbml_popup($elem) { // Hide this markup until javascript shows it. $t = '\n"; return $t; }