r67130 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r67129‎ | r67130 | r67131 >
Date:07:23, 31 May 2010
Author:tisane
Status:reverted (Comments)
Tags:
Comment:
Modified paths:
  • /trunk/extensions/FBConnect (added) (history)
  • /trunk/extensions/FBConnect/FBConnect.alias.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnect.i18n.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnect.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectAPI.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectDB.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectHooks.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectPushEvent.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectUser.php (added) (history)
  • /trunk/extensions/FBConnect/FBConnectXFBML.php (added) (history)
  • /trunk/extensions/FBConnect/PreferencesExtension.php (added) (history)
  • /trunk/extensions/FBConnect/SpecialConnect.php (added) (history)
  • /trunk/extensions/FBConnect/config.sample.php (added) (history)
  • /trunk/extensions/FBConnect/fbconnect.css (added) (history)
  • /trunk/extensions/FBConnect/fbconnect.js (added) (history)
  • /trunk/extensions/FBConnect/fbconnect.min.js (added) (history)
  • /trunk/extensions/FBConnect/fbconnect_table.pg.sql (added) (history)
  • /trunk/extensions/FBConnect/fbconnect_table.sql (added) (history)
  • /trunk/extensions/FBConnect/php-sdk (added) (history)
  • /trunk/extensions/FBConnect/php-sdk/facebook.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnAddImage.i18n.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnAddImage.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnLargeEdit.i18n.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnLargeEdit.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnWatchArticle.i18n.php (added) (history)
  • /trunk/extensions/FBConnect/pushEvents/FBPush_OnWatchArticle.php (added) (history)

Diff [purge]

Index: trunk/extensions/FBConnect/FBConnect.alias.php
@@ -0,0 +1,21 @@
 2+<?php
 3+/**
 4+ * FBConnect.alias.php - FBConnect for MediaWiki
 5+ *
 6+ * Special Page alias file... for when we actually define some special pages ;-)
 7+ */
 8+
 9+
 10+/*
 11+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 12+ */
 13+if ( !defined( 'MEDIAWIKI' ) ) {
 14+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 15+}
 16+
 17+$aliases = array();
 18+
 19+/** English */
 20+$aliases['en'] = array(
 21+ 'Connect' => array( 'Connect', 'ConnectAccount' ),
 22+);
Index: trunk/extensions/FBConnect/FBConnectXFBML.php
@@ -0,0 +1,202 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class FBConnectXFBML
 30+ *
 31+ * This class allows FBML (Facebook Markup Language, an extension to HTML) to
 32+ * be incorporated into the wiki through XFBML.
 33+ *
 34+ * See: <http://wiki.developers.facebook.com/index.php/XFBML>
 35+ */
 36+class FBConnectXFBML {
 37+ /**
 38+ * This function is the callback that the ParserFirstCallInit hook assigns
 39+ * to the parser whenever a FBML tag is encountered (like <fb:name>).
 40+ * Function createParserHook($tag) creates an anonymous (lambda-style)
 41+ * function that simply redirects to parserHook(), filling in the missing
 42+ * $tag argument with the $tag provided to createParserHook.
 43+ */
 44+ static function parserHook($innertext, $args, &$parser, $tag = '' ) {
 45+ global $fbAllowFacebookImages;
 46+
 47+ // Run hook to allow modifying the default behavior. If $override is
 48+ // set, it is used instead. Return false to disable the tag.
 49+ $override = '';
 50+ if ( !wfRunHooks('XFBMLParseTag',
 51+ array( $tag, &$args, &$innertext, &$override )) ) {
 52+ return '';
 53+ }
 54+ if ( $override != '' ) {
 55+ return $override;
 56+ }
 57+
 58+ switch ($tag) {
 59+ case '':
 60+ break; // Error: We shouldn't be here!
 61+
 62+ // To implement a custom XFBML tag handler, simply case it here like so
 63+ case 'fb:add-profile-tab':
 64+ // Disable these tags by returning an empty string
 65+ break;
 66+ case 'fb:serverfbml':
 67+ // TODO: Is this safe? Does it respect $fbAllowFacebookImages?
 68+ $attrs = self::implodeAttrs( $args );
 69+ // Don't recursively parse $innertext
 70+ return "<fb:serverfbml{$attrs}>$innertext</fb:serverfbml>";
 71+ case 'fb:profile-pic':
 72+ #case 'fb:photo': // Dropped in new JavaScript SDK
 73+ #case 'fb:video': // Dropped in new JavaScript SDK
 74+ if (!$fbAllowFacebookImages) {
 75+ break;
 76+ }
 77+ // Careful - no "break;" if $fbAllowFacebookImages is true
 78+ default:
 79+ // Allow other tags by default
 80+ $attrs = self::implodeAttrs( $args );
 81+ return "<{$tag}{$attrs}>" . $parser->recursiveTagParse( $innertext ) . "</$tag>";
 82+ }
 83+ // Strip the tag entirely
 84+ return '';
 85+ }
 86+
 87+ /**
 88+ * Helper function to create name-value pairs from the list of attributes passed to the
 89+ * parser hook.
 90+ */
 91+ static function implodeAttrs( $args ) {
 92+ $attrs = "";
 93+ // The default action is to strip all event handlers and allow the tag
 94+ foreach( $args as $name => $value ) {
 95+ // Disable all event handlers (e.g. onClick, onligin)
 96+ if ( substr( $name, 0, 2 ) == "on" )
 97+ continue;
 98+ // Otherwise, pass the attribute through htmlspecialchars unmodified
 99+ $attrs .= " $name=\"" . htmlspecialchars( $value ) . '"';
 100+ }
 101+ return $attrs;
 102+ }
 103+
 104+ /**
 105+ * Helper function for parserHook. Originally, all tags were directed to
 106+ * that function, but I had no way of knowing which tag provoked the function.
 107+ */
 108+ static function createParserHook($tag) {
 109+ $args = '$text,$args,&$parser';
 110+ $code = 'return FBConnectXFBML::parserHook($text,$args,$parser,\''.$tag.'\');';
 111+ return create_function($args, $code);
 112+ }
 113+
 114+ /**
 115+ * Returns true if XFBML is enabled (i.e. $fbUseMarkup is not false).
 116+ * Defaults to true if $fbUseMarkup is not set.
 117+ */
 118+ static function isEnabled() {
 119+ global $fbUseMarkup;
 120+ return !isset($fbUseMarkup) || $fbUseMarkup;
 121+ }
 122+
 123+ /**
 124+ * Returns the availabe XFBML tags. For help on including these in your
 125+ * site, please see: <http://developers.facebook.com/plugins>.
 126+ *
 127+ * If Facebook adds a new tag (or you create your own!) then this list can
 128+ * be updated with the XFBMLAvailableTags hook.
 129+ */
 130+ static function availableTags() {
 131+ if (!self::isEnabled()) {
 132+ // If XFBML isn't enabled, then don't report any tags
 133+ return array( );
 134+ }
 135+ // http://github.com/facebook/connect-js/blob/master/src/xfbml/xfbml.js#L237
 136+ $tags = array('fb:activity',
 137+ 'fb:add-profile-tab',
 138+ 'fb:bookmark',
 139+ 'fb:comments',
 140+ 'fb:connect-bar',
 141+ 'fb:fan',
 142+ 'fb:like',
 143+ 'fb:like-box',
 144+ 'fb:live-stream',
 145+ 'fb:login',
 146+ 'fb:login-button',
 147+ 'fb:facepile',
 148+ 'fb:name',
 149+ 'fb:profile-pic',
 150+ 'fb:recommendations',
 151+ 'fb:serverfbml',
 152+ 'fb:share-button',
 153+ 'fb:social-bar',
 154+ /*
 155+ * From the Facebook Developer's Wiki under the old JS library.
 156+ * These may still be possible with a <fb:serverFbml> tag.
 157+ * <http://wiki.developers.facebook.com/index.php/XFBML>
 158+ */
 159+ #'fb:connect-form',
 160+ #'fb:container',
 161+ #'fb:eventlink',
 162+ #'fb:grouplink',
 163+ #'fb:photo',
 164+ #'fb:prompt-permission',
 165+ #'fb:pronoun',
 166+ #'fb:unconnected-friends-count',
 167+ #'fb:user-status'
 168+ /*
 169+ * In 2008 I found these in the deprecated Facebook Connect
 170+ * JavaScript library, connect.js.pkg.php, though no documentation
 171+ * was available for them on the Facebook dev wiki.
 172+ */
 173+ #'fb:add-section-button',
 174+ #'fb:share-button',
 175+ #'fb:userlink',
 176+ #'fb:video',
 177+ );
 178+
 179+ // Reject discarded tags (that return an empty string) from Special:Version
 180+ $tempParser = new DummyParser();
 181+ foreach( $tags as $i => $tag ) {
 182+ if ( self::parserHook('', array(), $tempParser, $tag) == '' ) {
 183+ unset( $tags[$i] );
 184+ }
 185+ }
 186+ // Allow other functions to modify the available XFBML tags
 187+ wfRunHooks( 'XFBMLAvailableTags', array( &$tags ));
 188+ return $tags;
 189+ }
 190+}
 191+
 192+
 193+/**
 194+ * Class DummyParser
 195+ *
 196+ * Allows FBConnectXML::availableTags() to pre-sanatize the list of tags reported to
 197+ * MediaWiki, excluding any tags that result in the tag being replaced by an empty
 198+ * string. Sorry for the confusing summary here, its really late. =)
 199+ */
 200+class DummyParser {
 201+ // We don't pass any text in our testing, so this must return an empty string
 202+ function recursiveTagParse() { return ''; }
 203+}
Index: trunk/extensions/FBConnect/SpecialConnect.php
@@ -0,0 +1,764 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class SpecialConnect
 30+ *
 31+ * This class represents the body class for the page Special:Connect.
 32+ *
 33+ * Currently, this page has one valid subpage at Special:Connect/ChooseName.
 34+ * Visiting the subpage will generate an error; it is only useful when POSTed to.
 35+ */
 36+class SpecialConnect extends SpecialPage {
 37+ private $userNamePrefix;
 38+ static private $availableUserUpdateOptions = array('fullname', 'nickname', 'email', 'language', 'timecorrection');
 39+
 40+ /**
 41+ * Constructor.
 42+ */
 43+ function __construct() {
 44+ global $wgSpecialPageGroups;
 45+ // Initiate SpecialPage's constructor
 46+ parent::__construct( 'Connect' );
 47+ // Add this special page to the "login" group of special pages
 48+ $wgSpecialPageGroups['Connect'] = 'login';
 49+
 50+ wfLoadExtensionMessages( 'FBConnect' );
 51+ $this->userNamePrefix = wfMsg('fbconnect-usernameprefix');
 52+ }
 53+
 54+ /**
 55+ * Allows the prefix to be changed at runtime. This is useful, for example,
 56+ * to generate a username based off of a facebook name.
 57+ */
 58+ public function setUserNamePrefix( $prefix ){
 59+ $this->userNamePrefix = $prefix;
 60+ }
 61+
 62+ /**
 63+ * Returns the list of user options that can be updated by facebook on each login.
 64+ */
 65+ public function getAvailableUserUpdateOptions(){
 66+ return self::$availableUserUpdateOptions;
 67+ }
 68+
 69+ /**
 70+ * Overrides getDescription() in SpecialPage. Looks in a different wiki message
 71+ * for this extension's description.
 72+ */
 73+ function getDescription() {
 74+ return wfMsg( 'fbconnect-title' );
 75+ }
 76+
 77+ /**
 78+ * Performs any necessary execution and outputs the resulting Special page.
 79+ */
 80+ function execute( $par ) {
 81+ global $wgUser, $facebook, $wgRequest;
 82+
 83+ if ( $wgRequest->getVal("action", "") == "disconnect" ) {
 84+ self::disconnectAction();
 85+ }
 86+
 87+ // Check to see if the user is already logged in
 88+ if ( $wgUser->getID() ) {
 89+ $this->sendPage('alreadyLoggedIn');
 90+ return;
 91+ }
 92+
 93+ $fbid = $facebook->getUser();
 94+ /*
 95+ // Connect to the Facebook API
 96+ try {
 97+ $me = $facebook->api('/me');
 98+
 99+ } catch (FacebookApiException $e) {
 100+ error_log($e);
 101+ }
 102+ */
 103+
 104+ // Look at the subpage name to discover where we are in the login process
 105+ switch ( $par ) {
 106+ case 'ChooseName':
 107+ $choice = $wgRequest->getText('wpNameChoice');
 108+ if ($wgRequest->getCheck('wpCancel')) {
 109+ $this->sendError('fbconnect-cancel', 'fbconnect-canceltext');
 110+ }
 111+ else switch ($choice) {
 112+ // Check to see if the user opted to connect an existing account
 113+ case 'existing':
 114+ $this->attachUser($fbid, $wgRequest->getText('wpExistingName'),
 115+ $wgRequest->getText('wpExistingPassword'));
 116+ break;
 117+ // Check to see if the user selected another valid option
 118+ case 'nick':
 119+ case 'first':
 120+ case 'full':
 121+ // Get the username from Facebook (Note: not from the form)
 122+ $username = FBConnectUser::getOptionFromInfo($choice . 'name',
 123+ $facebook->getUserInfo());
 124+ case 'manual':
 125+ if (!isset($username) || !$this->userNameOK($username)) {
 126+ // Use manual name if no username is set, even if manual wasn't chosen
 127+ $username = $wgRequest->getText('wpName2');
 128+ }
 129+ // If no valid username was found, something's not right; ask again
 130+ if (!$this->userNameOK($username)) {
 131+ $this->sendPage('chooseNameForm', 'fbconnect-invalidname');
 132+ } else {
 133+ $this->createUser($fbid, $username);
 134+ }
 135+ break;
 136+ case 'auto':
 137+ // Create a user with a unique generated username
 138+ $this->createUser($fbid, $this->generateUserName());
 139+ break;
 140+ default:
 141+ $this->sendError('fbconnect-invalid', 'fbconnect-invalidtext');
 142+ }
 143+ break;
 144+ default:
 145+ // Main entry point
 146+ #if ( $wgRequest->getText( 'returnto' ) ) {
 147+ # $this->setReturnTo( $wgRequest->getText( 'returnto' ),
 148+ # $wgRequest->getVal( 'returntoquery' ) );
 149+ #}
 150+ if ($fbid) {
 151+ // If the user is connected, log them in
 152+ $this->login($fbid);
 153+ } else {
 154+ // If the user isn't Connected, then show a form with the Connect button
 155+ $this->sendPage('connectForm');
 156+ }
 157+ }
 158+ }
 159+
 160+ /**
 161+ * Logs in the user by their Facebook ID. If the Facebook user doesn't have
 162+ * an account on the wiki, then they are presented with a form prompting
 163+ * them to choose a wiki username.
 164+ */
 165+ protected function login($fb_id) {
 166+ global $wgUser;
 167+
 168+ // Check to see if the Connected user exists in the database
 169+ if ($fb_id) {
 170+ $user = FBConnectDB::getUser($fb_id);
 171+ }
 172+
 173+ // If the user doesn't exist yet locally, allow hooks to check to see if this is a clustered set up
 174+ if ( !(isset($user) && $user instanceof User ) &&
 175+ !wfRunHooks('SpecialConnect::login::notFoundLocally', array(&$this, &$user, $fb_id))) {
 176+ return;
 177+ }
 178+
 179+ if ( isset($user) && $user instanceof User ) {
 180+ $fbUser = new FBConnectUser($user);
 181+ // Update user from facebook (see class FBConnectUser)
 182+ $fbUser->updateFromFacebook();
 183+
 184+ // Setup the session
 185+ global $wgSessionStarted;
 186+ if (!$wgSessionStarted) {
 187+ wfSetupSession();
 188+ }
 189+
 190+ $user->setCookies();
 191+ $wgUser = $user;
 192+
 193+ // Similar to what's done in LoginForm::authenticateUserData().
 194+ // Load $wgUser now. This is necessary because loading $wgUser (say
 195+ // by calling getName()) calls the UserLoadFromSession hook, which
 196+ // potentially creates the user in the local database.
 197+ $sessionUser = User::newFromSession();
 198+ $sessionUser->load();
 199+
 200+ $this->sendPage('displaySuccessLogin');
 201+ } else if ($fb_id) {
 202+ $this->sendPage('chooseNameForm');
 203+ } else {
 204+ // TODO: send an error message saying only Connected users can log in
 205+ // or ask them to Connect.
 206+ $this->sendError('fbconnect-cancel', 'fbconnect-canceltext');
 207+ }
 208+ }
 209+
 210+ protected function createUser($fb_id, $name) {
 211+ global $wgUser, $wgOut, $fbConnectOnly, $wgAuth, $wgRequest, $wgMemc;
 212+ wfProfileIn(__METHOD__);
 213+
 214+ // Handle accidental reposts.
 215+ if ( $wgUser->isLoggedIn() ) {
 216+ $this->sendPage('displaySuccessLogin');
 217+ wfProfileOut(__METHOD__);
 218+ return;
 219+ }
 220+
 221+ // Make sure we're not stealing an existing user account.
 222+ if (!$name || !$this->userNameOK($name)) {
 223+ // TODO: Provide an error message that explains that they need to pick a name or the name is taken.
 224+ wfDebug("FBConnect: Name not OK: '$name'\n");
 225+ $this->sendPage('chooseNameForm');
 226+ return;
 227+ }
 228+
 229+ /// START OF TYPICAL VALIDATIONS AND RESTRICITONS ON ACCOUNT-CREATION. ///
 230+
 231+ // Check the restrictions again to make sure that the user can create this account.
 232+ $titleObj = SpecialPage::getTitleFor( 'Connect' );
 233+ if ( wfReadOnly() ) {
 234+ $wgOut->readOnlyPage();
 235+ return;
 236+ } elseif ( !isset($fbConnectOnly) || !$fbConnectOnly ) {
 237+ // These two permissions don't apply in $fbConnectOnly mode
 238+ if ( $wgUser->isBlockedFromCreateAccount() ) {
 239+ wfDebug("FBConnect: Blocked user was attempting to create account via Facebook Connect.\n");
 240+ $wgOut->showErrorPage('fbconnect-error', 'fbconnect-errortext');
 241+ return;
 242+ } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
 243+ $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
 244+ return;
 245+ }
 246+ }
 247+
 248+ // If we are not allowing users to login locally, we should be checking
 249+ // to see if the user is actually able to authenticate to the authenti-
 250+ // cation server before they create an account (otherwise, they can
 251+ // create a local account and login as any domain user). We only need
 252+ // to check this for domains that aren't local.
 253+ $mDomain = $wgRequest->getText( 'wpDomain' );
 254+ if( 'local' != $mDomain && '' != $mDomain ) {
 255+ if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $name ) ) ) {
 256+ $wgOut->showErrorPage('fbconnect-error', 'wrongpassword');
 257+ return false;
 258+ }
 259+ }
 260+
 261+ // IP-blocking (and open proxy blocking) protection from SpecialUserLogin
 262+ global $wgEnableSorbs, $wgProxyWhitelist;
 263+ $ip = wfGetIP();
 264+ if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
 265+ $wgUser->inSorbsBlacklist( $ip ) )
 266+ {
 267+ $wgOut->showErrorPage('fbconnect-error', 'sorbs_create_account_reason');
 268+ return;
 269+ }
 270+
 271+ /**
 272+ // Test to see if we are denied by $wgAuth or the user can't create an account
 273+ if ( !$wgAuth->autoCreate() || !$wgAuth->userExists( $userName ) ||
 274+ !$wgAuth->authenticate( $userName )) {
 275+ $result = false;
 276+ return true;
 277+ }
 278+ /**/
 279+
 280+ // Run a hook to let custom forms make sure that it is okay to proceed with processing the form.
 281+ // This hook should only check preconditions and should not store values. Values should be stored using the hook at the bottom of this function.
 282+ // Can use 'this' to call sendPage('chooseNameForm', 'SOME-ERROR-MSG-CODE-HERE') if some of the preconditions are invalid.
 283+ if(! wfRunHooks( 'SpecialConnect::createUser::validateForm', array( &$this ) )){
 284+ return;
 285+ }
 286+
 287+ $user = User::newFromName($name);
 288+ if (!$user) {
 289+ wfDebug("FBConnect: Error creating new user.\n");
 290+ $wgOut->showErrorPage('fbconnect-error', 'fbconnect-error-creating-user');
 291+ return;
 292+ }
 293+
 294+ // Let extensions abort the account creation. If you have extensions which are expecting a Real Name or Email, you may need to disable
 295+ // them since these are not requirements of Facebook Connect (so users will not have them).
 296+ // NOTE: Currently this is commented out because it seems that most wikis might have a handful of restrictions that won't be needed on
 297+ // Facebook Connections. For instance, requiring a CAPTCHA or age-verification, etc. Having a Facebook account as a pre-requisitie removes the need for that.
 298+ /*
 299+ $abortError = '';
 300+ if( !wfRunHooks( 'AbortNewAccount', array( $user, &$abortError ) ) ) {
 301+ // Hook point to add extra creation throttles and blocks
 302+ wfDebug( "SpecialConnect::createUser: a hook blocked creation\n" );
 303+ $wgOut->showErrorPage('fbconnect-error', 'fbconnect-error-user-creation-hook-aborted', array($abortError));
 304+ return false;
 305+ }
 306+ /**/
 307+
 308+ // Apply account-creation throttles
 309+ global $wgAccountCreationThrottle;
 310+ if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
 311+ $key = wfMemcKey( 'acctcreate', 'ip', $ip );
 312+ $value = $wgMemc->get( $key );
 313+ if ( !$value ) {
 314+ $wgMemc->set( $key, 0, 86400 );
 315+ }
 316+ if ( $value >= $wgAccountCreationThrottle ) {
 317+ $this->throttleHit( $wgAccountCreationThrottle );
 318+ return false;
 319+ }
 320+ $wgMemc->incr( $key );
 321+ }
 322+
 323+ /// END OF TYPICAL VALIDATIONS AND RESTRICITONS ON ACCOUNT-CREATION. ///
 324+
 325+ // Create the account (locally on main cluster or via wgAuth on other clusters)
 326+ $email = $realName = ""; // The real values will get filled in outside of the scope of this function.
 327+ $pass = null;
 328+ if( !$wgAuth->addUser( $user, $pass, $email, $realName ) ) {
 329+ wfDebug("FBConnect: Error adding new user to database.\n");
 330+ $wgOut->showErrorPage('fbconnect-error', 'fbconnect-errortext');
 331+ return;
 332+ }
 333+
 334+ // Adds the user to the local database (regardless of whether wgAuth was used)
 335+ $user = $this->initUser( $user, true );
 336+
 337+ // Attach the user to their Facebook account in the database
 338+ // This must be done up here so that the data is in the database before copy-to-local is done for sharded setups
 339+ FBConnectDB::addFacebookID($user, $fb_id);
 340+
 341+ wfRunHooks( 'AddNewAccount', array( $user ) );
 342+
 343+ // Mark that the user is a Facebook user
 344+ $user->addGroup('fb-user');
 345+
 346+ // Store which fields should be auto-updated from Facebook when the user logs in.
 347+ $updateFormPrefix = "wpUpdateUserInfo";
 348+ foreach (self::$availableUserUpdateOptions as $option) {
 349+ if($wgRequest->getVal($updateFormPrefix.$option, '') != ""){
 350+ $user->setOption("fbconnect-update-on-login-$option", 1);
 351+ } else {
 352+ $user->setOption("fbconnect-update-on-login-$option", 0);
 353+ }
 354+ }
 355+
 356+ // Process the FBConnectPushEvent preference checkboxes if fbConnectPushEvents are enabled.
 357+ global $fbEnablePushToFacebook;
 358+ if($fbEnablePushToFacebook){
 359+ global $fbPushEventClasses;
 360+ if(!empty($fbPushEventClasses)){
 361+ foreach($fbPushEventClasses as $pushEventClassName){
 362+ $pushObj = new $pushEventClassName;
 363+ $className = get_class();
 364+ $prefName = $pushObj->getUserPreferenceName();
 365+
 366+ $user->setOption($prefName, ($wgRequest->getCheck($prefName)?"1":"0"));
 367+ }
 368+ }
 369+ }
 370+
 371+ // Unfortunately, performs a second database lookup
 372+ $fbUser = new FBConnectUser($user);
 373+ // Update the user with settings from Facebook
 374+ $fbUser->updateFromFacebook();
 375+
 376+ // Start the session if it's not already been started
 377+ global $wgSessionStarted;
 378+ if (!$wgSessionStarted) {
 379+ wfSetupSession();
 380+ }
 381+
 382+ // Log the user in
 383+ $user->setCookies();
 384+ // Store the new user as the global user object
 385+ $wgUser = $user;
 386+
 387+ /*
 388+ * Similar to what's done in LoginForm::authenticateUserData(). Load
 389+ * $wgUser now. This is necessary because loading $wgUser (say by
 390+ * calling getName()) calls the UserLoadFromSession hook, which
 391+ * potentially creates the user in the local database.
 392+ */
 393+ $sessionUser = User::newFromSession();
 394+ $sessionUser->load();
 395+
 396+ // Allow custom form processing to store values since this form submission was successful.
 397+ // This hook should not fail on invalid input, instead check the input using the SpecialConnect::createUser::validateForm hook above.
 398+ wfRunHooks( 'SpecialConnect::createUser::postProcessForm', array( &$this ) );
 399+
 400+ // TODO: Which MediaWiki versions can we call this function in?
 401+ $user->addNewUserLogEntryAutoCreate();
 402+ #$user->addNewUserLogEntry();
 403+
 404+ $this->sendPage('displaySuccessLogin');
 405+
 406+ wfProfileOut(__METHOD__);
 407+ }
 408+
 409+ /**
 410+ * Actually add a user to the database.
 411+ * Give it a User object that has been initialised with a name.
 412+ *
 413+ * This is a custom version of similar code in SpecialUserLogin's LoginForm with differences
 414+ * due to the fact that this code doesn't require a password, etc.
 415+ *
 416+ * @param $u User object.
 417+ * @param $autocreate boolean -- true if this is an autocreation via auth plugin
 418+ * @return User object.
 419+ * @private
 420+ */
 421+ protected function initUser( $u, $autocreate ) {
 422+ global $wgAuth;
 423+
 424+ $u->addToDatabase();
 425+
 426+ // No passwords for FBConnect accounts.
 427+ /**
 428+ if ( $wgAuth->allowPasswordChange() ) {
 429+ $u->setPassword( $this->mPassword );
 430+ }
 431+ /**
 432+ $u->setEmail( $this->mEmail ); // emails aren't required by FBConnect extension (some customizations such as Wikia require it on their own).
 433+ $u->setRealName( $this->mRealName ); // real name isn't required for FBConnect
 434+ /**/
 435+ $u->setToken();
 436+
 437+ $wgAuth->initUser( $u, $autocreate );
 438+ $wgAuth->updateUser($u);
 439+
 440+ //$u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
 441+ $u->setOption('skinoverwrite', 1);
 442+ $u->saveSettings();
 443+
 444+ # Update user count
 445+ $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
 446+ $ssUpdate->doUpdate();
 447+
 448+ return $u;
 449+ }
 450+
 451+ /**
 452+ * Attaches the Facebook ID to an existing wiki account. If the user does
 453+ * not exist, or the supplied password does not match, then an error page
 454+ * is sent. Otherwise, the accounts are matched in the database and the new
 455+ * user object is logged in.
 456+ */
 457+ protected function attachUser($fbid, $name, $password) {
 458+ global $wgOut, $wgUser;
 459+ wfProfileIn(__METHOD__);
 460+
 461+ // The user must be logged into Facebook before choosing a wiki username
 462+ if ( !$fbid ) {
 463+ wfDebug("FBConnect: aborting in attachUser(): no Facebook ID was reported.\n");
 464+ $wgOut->showErrorPage( 'fbconnect-error', 'fbconnect-errortext' );
 465+ return;
 466+ }
 467+ // Look up the user by their name
 468+ $user = new FBConnectUser(User::newFromName($name));
 469+ if (!$user || !$user->checkPassword($password)) {
 470+ $this->sendPage('chooseNameForm', 'wrongpassword');
 471+ return;
 472+ }
 473+ // Attach the user to their Facebook account in the database
 474+ FBConnectDB::addFacebookID($user, $fbid);
 475+ // Update the user with settings from Facebook
 476+ $user->updateFromFacebook();
 477+ // Store the user in the global user object
 478+ $wgUser = $user;
 479+ $this->sendPage('displaySuccessLogin');
 480+
 481+ wfProfileOut(__METHOD__);
 482+ }
 483+
 484+ /**
 485+ * Generates a unique username for a wiki account based on the prefix specified
 486+ * in the message 'fbconnect-usernameprefix'. The number appended is equal to
 487+ * the number of Facebook Connect to user ID associations in the user_fbconnect
 488+ * table, so quite a few numbers will be skipped. However, this approach is
 489+ * more scalable. For smaller wiki installations, uncomment the line $i = 1 to
 490+ * have consecutive usernames starting at 1.
 491+ */
 492+ public function generateUserName() {
 493+ // Because $i is incremented the first time through the while loop
 494+ $i = FBConnectDB::countUsers();
 495+ #$i = 1; // This is the DUMB WAY to do this for large databases
 496+ while ($i < PHP_INT_MAX) {
 497+ $name = $this->userNamePrefix . $i;
 498+ if ($this->userNameOK($name)) {
 499+ return $name;
 500+ }
 501+ ++$i;
 502+ }
 503+ return $prefix;
 504+ }
 505+
 506+ /**
 507+ * Tests whether the name is OK to use as a user name.
 508+ */
 509+ public function userNameOK ($name) {
 510+ global $wgReservedUsernames;
 511+ return ($name && (null == User::idFromName($name)) && !in_array($name, $wgReservedUsernames));
 512+ }
 513+
 514+ /**
 515+ * Displays a simple error page.
 516+ */
 517+ protected function sendError($titleMsg, $textMsg) {
 518+ global $wgOut;
 519+ $wgOut->showErrorPage($titleMsg, $textMsg);
 520+ }
 521+
 522+ public function sendPage($function, $arg = NULL) {
 523+ global $wgOut;
 524+ // Setup the page for rendering
 525+ wfLoadExtensionMessages( 'FBConnect' );
 526+ $this->setHeaders();
 527+ $wgOut->disallowUserJs(); # just in case...
 528+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
 529+ $wgOut->setArticleRelated( false );
 530+ // Call the specified function to continue generating the page
 531+ if (is_null($arg)) {
 532+ $this->$function();
 533+ } else {
 534+ $this->$function($arg);
 535+ }
 536+ }
 537+
 538+ private function alreadyLoggedIn() {
 539+ global $wgOut, $wgUser, $wgRequest, $wgSitename;
 540+ $wgOut->setPageTitle(wfMsg('fbconnect-error'));
 541+ $wgOut->addWikiMsg('fbconnect-alreadyloggedin', $wgUser->getName());
 542+ $wgOut->addWikiMsg('fbconnect-click-to-connect-existing', $wgSitename);
 543+ $wgOut->addHTML('<fb:login-button'.FBConnect::getPermissionsAttribute().FBConnect::getOnLoginAttribute().'></fb:login-button>');
 544+ // Render the "Return to" text retrieved from the URL
 545+ $wgOut->returnToMain(false, $wgRequest->getText('returnto'), $wgRequest->getVal('returntoquery'));
 546+ }
 547+
 548+ private function displaySuccessLogin() {
 549+ global $wgOut, $wgRequest;
 550+ $wgOut->setPageTitle(wfMsg('fbconnect-success'));
 551+ $wgOut->addWikiMsg('fbconnect-successtext');
 552+ // Run any hooks for UserLoginComplete
 553+ $inject_html = '';
 554+ wfRunHooks( 'UserLoginComplete', array( &$wgUser, &$inject_html ) );
 555+ $wgOut->addHtml( $inject_html );
 556+ // Render the "return to" text retrieved from the URL
 557+ $wgOut->returnToMain(false, $wgRequest->getText('returnto'), $wgRequest->getVal('returntoquery'));
 558+ }
 559+
 560+ /**
 561+ * TODO: Add an option to disallow this extension to access your Facebook
 562+ * information. This option could simply point you to your Facebook privacy
 563+ * settings. This is necessary in case the user wants to perpetually browse
 564+ * the wiki anonymously, while still being logged in to Facebook.
 565+ *
 566+ * NOTE: The above might be done now that we have checkboxes for which options
 567+ * to update from fb. Haven't tested it though.
 568+ */
 569+ private function chooseNameForm($messagekey = 'fbconnect-chooseinstructions') {
 570+ // Permissions restrictions.
 571+ global $wgUser, $facebook, $wgOut, $fbConnectOnly;
 572+ $titleObj = SpecialPage::getTitleFor( 'Connect' );
 573+ if ( wfReadOnly() ) {
 574+ // The wiki is in read-only mode
 575+ $wgOut->readOnlyPage();
 576+ return;
 577+ } elseif ( !isset($fbConnectOnly) || !$fbConnectOnly ) {
 578+ // These two permissions don't apply in $fbConnectOnly mode
 579+ if ( $wgUser->isBlockedFromCreateAccount() ) {
 580+ wfDebug("FBConnect: Blocked user was attempting to create account via Facebook Connect.\n");
 581+ $wgOut->showErrorPage('fbconnect-error', 'fbconnect-errortext');
 582+ return;
 583+ } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
 584+ $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
 585+ return;
 586+ }
 587+ }
 588+
 589+
 590+ // Allow other code to have a custom form here (so that this extension can be integrated with existing custom login screens).
 591+ if( !wfRunHooks( 'SpecialConnect::chooseNameForm', array( &$this, &$messagekey ) ) ){
 592+ return;
 593+ }
 594+
 595+ // Connect to the Facebook API
 596+ $userinfo = $facebook->getUserInfo();
 597+
 598+ // Keep track of when the first option visible to the user is checked
 599+ $checked = false;
 600+
 601+ // Outputs the canonical name of the special page at the top of the page
 602+ $this->outputHeader();
 603+ // If a different $messagekey was passed (like 'wrongpassword'), use it instead
 604+ $wgOut->addWikiMsg( $messagekey );
 605+ // TODO: Format the html a little nicer
 606+ $wgOut->addHTML('
 607+ <form action="' . $this->getTitle('ChooseName')->getLocalUrl() . '" method="POST">
 608+ <fieldset id="mw-fbconnect-choosename">
 609+ <legend>' . wfMsg('fbconnect-chooselegend') . '</legend>
 610+ <table>');
 611+ // Let them attach to an existing user if $fbConnectOnly allows it
 612+ if (!$fbConnectOnly) {
 613+ // Grab the UserName from the cookie if it exists
 614+ global $wgCookiePrefix;
 615+ $name = isset($_COOKIE[$wgCookiePrefix . 'UserName']) ?
 616+ trim($_COOKIE[$wgCookiePrefix . 'UserName']) : '';
 617+ // Build an array of attributes to update
 618+ $updateOptions = array();
 619+ foreach (self::$availableUserUpdateOptions as $option) {
 620+ // Translate the MW parameter into a FB parameter
 621+ $value = FBConnectUser::getOptionFromInfo($option, $userinfo);
 622+ // If no corresponding value was received from Facebook, then continue
 623+ if (!$value) {
 624+ continue;
 625+ }
 626+ // Build the list item for the update option
 627+ $updateOptions[] = "<li><input name=\"wpUpdateUserInfo$option\" type=\"checkbox\" " .
 628+ "value=\"1\" id=\"wpUpdateUserInfo$option\" /><label for=\"wpUpdateUserInfo$option\">" .
 629+ wfMsgHtml("fbconnect-$option") . wfMsgExt('colon-separator', array('escapenoentities')) .
 630+ " <i>$value</i></label></li>";
 631+ }
 632+ // Implode the update options into an unordered list
 633+ $updateChoices = count($updateOptions) > 0 ? "<br />\n" . wfMsgHtml('fbconnect-updateuserinfo') .
 634+ "\n<ul>\n" . implode("\n", $updateOptions) . "\n</ul>\n" : '';
 635+ // Create the HTML for the "existing account" option
 636+ $html = '<tr><td class="wm-label"><input name="wpNameChoice" type="radio" ' .
 637+ 'value="existing" id="wpNameChoiceExisting"/></td><td class="mw-input">' .
 638+ '<label for="wnNameChoiceExisting">' . wfMsg('fbconnect-chooseexisting') . '<br/>' .
 639+ wfMsgHtml('fbconnect-chooseusername') . '<input name="wpExistingName" size="16" value="' .
 640+ $name . '" id="wpExistingName"/>' . wfMsgHtml('fbconnect-choosepassword') .
 641+ '<input name="wpExistingPassword" ' . 'size="" value="" type="password"/>' . $updateChoices .
 642+ '</td></tr>';
 643+ $wgOut->addHTML($html);
 644+ }
 645+
 646+ // Add the options for nick name, first name and full name if we can get them
 647+ // TODO: Wikify the usernames (i.e. Full name should have an _ )
 648+ foreach (array('nick', 'first', 'full') as $option) {
 649+ $nickname = FBConnectUser::getOptionFromInfo($option . 'name', $userinfo);
 650+ if ($nickname && $this->userNameOK($nickname)) {
 651+ $wgOut->addHTML('<tr><td class="mw-label"><input name="wpNameChoice" type="radio" value="' .
 652+ $option . ($checked ? '' : '" checked="checked') . '" id="wpNameChoice' . $option .
 653+ '"/></td><td class="mw-input"><label for="wpNameChoice' . $option . '">' .
 654+ wfMsg('fbconnect-choose' . $option, $nickname) . '</label></td></tr>');
 655+ // When the first radio is checked, this flag is set and subsequent options aren't checked
 656+ $checked = true;
 657+ }
 658+ }
 659+
 660+ // The options for auto and manual usernames are always available
 661+ $wgOut->addHTML('<tr><td class="mw-label"><input name="wpNameChoice" type="radio" value="auto" ' .
 662+ ($checked ? '' : 'checked="checked" ') . 'id="wpNameChoiceAuto"/></td><td class="mw-input">' .
 663+ '<label for="wpNameChoiceAuto">' . wfMsg('fbconnect-chooseauto', $this->generateUserName()) .
 664+ '</label></td></tr><tr><td class="mw-label"><input name="wpNameChoice" type="radio" ' .
 665+ 'value="manual" id="wpNameChoiceManual"/></td><td class="mw-input"><label ' .
 666+ 'for="wpNameChoiceManual">' . wfMsg('fbconnect-choosemanual') . '</label>&nbsp;' .
 667+ '<input name="wpName2" size="16" value="" id="wpName2"/></td></tr>' .
 668+ // Finish with two options, "Log in" or "Cancel"
 669+ '<tr><td></td><td class="mw-submit"><input type="submit" value="Log in" name="wpOK"/>' .
 670+ '<input type="submit" value="Cancel" name="wpCancel"/></td></tr></table></fieldset></form>'
 671+ );
 672+ }
 673+
 674+ /**
 675+ * Displays the main connect form.
 676+ */
 677+ private function connectForm() {
 678+ global $facebook, $wgOut, $wgUser, $wgSitename;
 679+ //$fbid = $facebook->getUser();
 680+
 681+ // Outputs the canonical name of the special page at the top of the page
 682+ $this->outputHeader();
 683+
 684+ // Render a humble Facebook Connect button
 685+ $wgOut->addHTML('<h2>' . wfMsg( 'fbconnect' ) . '</h2>
 686+ <div>'.wfMsgExt( 'fbconnect-intro', array('parse', 'content')) . '<br/>' . wfMsg( 'fbconnect-click-to-login', $wgSitename ) .'
 687+ <fb:login-button size="large" background="black" length="long"'.FBConnect::getPermissionsAttribute().FBConnect::getOnLoginAttribute().'></fb:login-button>
 688+ </div>'
 689+ );
 690+ }
 691+
 692+ /**
 693+ * Disconnect from Facebook
 694+ */
 695+ private function disconnectAction() {
 696+ global $wgRequest, $wgOut, $wgUser;
 697+
 698+ if( $wgRequest->wasPosted() ) {
 699+ $id = FBConnectDB::getFacebookIDs($wgUser);
 700+ if( count($id) > 0 ) {
 701+ FBConnectDB::removeFacebookID( $wgUser, $id['user_fbid'] );
 702+ }
 703+ }
 704+
 705+ $title = Title::makeTitle( NS_SPECIAL, "Preferences" );
 706+ $url = $title->getFullUrl("#prefsection-10");
 707+ $wgOut->redirect($url);
 708+ return true;
 709+ }
 710+
 711+ /**
 712+ * Creates a Login Form template object and propogates it with parameters.
 713+ *
 714+ private function createLoginForm() {
 715+ global $wgUser, $wgEnableEmail, $wgEmailConfirmToEdit,
 716+ $wgCookiePrefix, $wgCookieExpiration, $wgAuth;
 717+
 718+ $template = new UserloginTemplate();
 719+
 720+ // Pull the name from $wgUser or cookies
 721+ if( $wgUser->isLoggedIn() )
 722+ $name = $wgUser->getName();
 723+ else if( isset( $_COOKIE[$wgCookiePrefix . 'UserName'] ))
 724+ $name = $_COOKIE[$wgCookiePrefix . 'UserName'];
 725+ else
 726+ $name = null;
 727+ // Alias some common URLs for $action and $link
 728+ $loginTitle = self::getTitleFor( 'Userlogin' );
 729+ $this_href = wfUrlencode( $this->getTitle() );
 730+ // Action URL that gets posted to
 731+ $action = $loginTitle->getLocalUrl('action=submitlogin&type=login&returnto=' . $this_href);
 732+ // Don't show a "create account" link if the user is not allowed to create an account
 733+ if ($wgUser->isAllowed( 'createaccount' )) {
 734+ $link_href = htmlspecialchars($loginTitle->getLocalUrl('type=signup&returnto=' . $this_href));
 735+ $link_text = wfMsgHtml( 'nologinlink' );
 736+ $link = wfMsgWikiHtml( 'nologin', "<a href=\"$link_href\">$link_text</a>" );
 737+ } else {
 738+ $link = '';
 739+ }
 740+
 741+ // Set the appropriate options for this template
 742+ $template->set( 'header', '' );
 743+ $template->set( 'name', $name );
 744+ $template->set( 'action', $action );
 745+ $template->set( 'link', $link );
 746+ $template->set( 'message', '' );
 747+ $template->set( 'messagetype', 'none' );
 748+ $template->set( 'useemail', $wgEnableEmail );
 749+ $template->set( 'emailrequired', $wgEmailConfirmToEdit );
 750+ $template->set( 'canreset', $wgAuth->allowPasswordChange() );
 751+ $template->set( 'canremember', ( $wgCookieExpiration > 0 ) );
 752+ $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) );
 753+ // Look this setting up in SpecialUserLogin.php
 754+ $template->set( 'usedomain', false );
 755+ // Give authentication and captcha plugins a chance to modify the form
 756+ $type = 'login';
 757+ $wgAuth->modifyUITemplate( $template, $type );
 758+ wfRunHooks( 'UserLoginForm', array( &$template ));
 759+
 760+
 761+ // Spit out the form we just made
 762+ return $template;
 763+ }
 764+ /**/
 765+}
Index: trunk/extensions/FBConnect/FBConnectAPI.php
@@ -0,0 +1,255 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class FBConnectAPI
 30+ *
 31+ * This class contains the code used to interface with Facebook via the
 32+ * Facebook Platform API.
 33+ */
 34+class FBConnectAPI extends Facebook {
 35+ // Constructor
 36+ public function __construct() {
 37+ global $fbAppId, $fbSecret, $fbDomain;
 38+ // Check to make sure config.sample.php was renamed properly
 39+ if ( !$this->isConfigSetup() ) {
 40+ exit( 'Please update $fbAppId and $fbSecret in config.php' );
 41+ }
 42+ $config = array(
 43+ 'appId' => $fbAppId,
 44+ 'secret' => $fbSecret,
 45+ 'cookie' => true,
 46+ );
 47+ // Include the optional domain parameter if it has been set
 48+ if ( !empty($fbDomain) && $fbDomain != 'BASE_DOMAIN' ) {
 49+ $config['domain'] = $fbDomain;
 50+ }
 51+ parent::__construct( $config );
 52+ }
 53+
 54+ /**
 55+ * Check to make sure config.sample.php was properly renamed to config.php
 56+ * and the instructions to fill out the first two important variables were
 57+ * followed correctly.
 58+ */
 59+ public function isConfigSetup() {
 60+ global $fbAppId, $fbSecret;
 61+ $isSetup = isset($fbAppId) && $fbAppId != 'YOUR_APP_KEY' &&
 62+ isset($fbSecret) && $fbSecret != 'YOUR_SECRET';
 63+ if(!$isSetup) {
 64+ // Check to see if they are still using the old variables
 65+ global $fbApiKey, $fbApiSecret;
 66+ if ( isset($fbApiKey) ) {
 67+ $fbAppId = $fbApiKey;
 68+ }
 69+ if ( isset($fbApiSecret) ) {
 70+ $fbSecret= $fbApiSecret;
 71+ }
 72+ $isSetup = isset($fbAppId) && $fbAppId != 'YOUR_APP_KEY' &&
 73+ isset($fbSecret) && $fbSecret != 'YOUR_SECRET';
 74+ }
 75+ return $isSetup;
 76+ }
 77+
 78+ /**
 79+ * Calls users.getInfo. Requests information about the user from Facebook.
 80+ */
 81+ public function getUserInfo() {
 82+ // First check to see if we have a session (if not, return null)
 83+ $user = $this->getUser();
 84+ if ( !$user ) {
 85+ return null;
 86+ }
 87+ try {
 88+ // Cache information about users to reduce the number of Facebook hits
 89+ static $userinfo = array();
 90+
 91+ if ( !isset($userinfo[$user]) ) {
 92+ // Query the Facebook API with the users.getInfo method
 93+ $query = array(
 94+ 'method' => 'users.getInfo',
 95+ 'uids' => $user,
 96+ 'fields' => join(',', array(
 97+ 'first_name', 'name', 'sex', 'timezone', 'locale',
 98+ /*'profile_url',*/
 99+ 'username', 'proxied_email', 'contact_email',
 100+ )),
 101+ );
 102+ $user_details = $this->api( $query );
 103+ // Cache the data in the $userinfo array
 104+ $userinfo[$user] = $user_details[0];
 105+ }
 106+ return isset($userinfo[$user]) ? $userinfo[$user] : null;
 107+ } catch (FacebookApiException $e) {
 108+ error_log( 'Failure in the api when requesting users.getInfo ' .
 109+ "on uid $user: " . $e->getMessage());
 110+ }
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Retrieves group membership data from Facebook.
 116+ */
 117+ public function getGroupRights( $user = null ) {
 118+ global $fbUserRightsFromGroup;
 119+
 120+ // Groupies can be members, officers or admins (the latter two infer the former)
 121+ $rights = array('member' => false,
 122+ 'officer' => false,
 123+ 'admin' => false);
 124+
 125+ $gid = !empty($fbUserRightsFromGroup) ? $fbUserRightsFromGroup : false;
 126+ if (// If no group ID is specified, then there's no group to belong to
 127+ !$gid ||
 128+ // If $user wasn't specified, set it to the logged in user
 129+ !$user ||
 130+ // If there is no logged in user
 131+ !($user = $this->getUser())
 132+ ) {
 133+ return $rights;
 134+ }
 135+
 136+ // If a User object was provided, translate it into a Facebook ID
 137+ if ($user instanceof User) {
 138+ // TODO: Does this call for a special api call without access_token?
 139+ $users = FBConnectDB::getFacebookIDs($user);
 140+ if (count($users)) {
 141+ $user = $users[0];
 142+ } else {
 143+ // Not a Connected user, can't be in a group
 144+ return $rights;
 145+ }
 146+ }
 147+
 148+ // Cache the rights for an individual user to prevent hitting Facebook for duplicate info
 149+ static $rights_cache = array();
 150+ if ( array_key_exists( $user, $rights_cache )) {
 151+ // Retrieve the rights from the cache
 152+ return $rights_cache[$user];
 153+ }
 154+
 155+ // This can contain up to 500 IDs, avoid requesting this info twice
 156+ static $members = false;
 157+ // Get a random 500 group members, along with officers, admins and not_replied's
 158+ if ($members === false) {
 159+ try {
 160+ // Check to make sure our session is still valid
 161+ $members = $this->api(array(
 162+ 'method' => 'groups.getMembers',
 163+ 'gid' => $gid
 164+ ));
 165+ } catch (FacebookApiException $e) {
 166+ // Invalid session; we're not going to be able to get the rights
 167+ error_log($e);
 168+ $rights_cache[$user] = $rights;
 169+ return $rights;
 170+ }
 171+ }
 172+
 173+ if ( $members ) {
 174+ // Check to see if the user is an officer
 175+ if (array_key_exists('officers', $members) && in_array($user, $members['officers'])) {
 176+ $rights['member'] = $rights['officer'] = true;
 177+ }
 178+ // Check to see if the user is an admin of the group
 179+ if (array_key_exists('admins', $members) && in_array($user, $members['admins'])) {
 180+ $rights['member'] = $rights['admin'] = true;
 181+ }
 182+ // Because the latter two rights infer the former, this step isn't always necessary
 183+ if( !$rights['member'] ) {
 184+ // Check to see if we are one of the (up to 500) random users
 185+ if ((array_key_exists('not_replied', $members) && is_array($members['not_replied']) &&
 186+ in_array($user, $members['not_replied'])) || in_array($user, $members['members'])) {
 187+ $rights['member'] = true;
 188+ } else {
 189+ // For groups of over 500ish, we must use this extra API call
 190+ // Notice that it occurs last, because we can hopefully avoid having to call it
 191+ try {
 192+ $group = $this->api(array(
 193+ 'method' => 'groups.get',
 194+ 'uid' => $user,
 195+ 'gids' => $gid
 196+ ));
 197+ } catch (FacebookApiException $e) {
 198+ error_log($e);
 199+ }
 200+ if (is_array($group) && is_array($group[0]) && $group[0]['gid'] == "$gid") {
 201+ $rights['member'] = true;
 202+ }
 203+ }
 204+ }
 205+ }
 206+ // Cache the rights
 207+ $rights_cache[$user] = $rights;
 208+ return $rights;
 209+ }
 210+
 211+ /*
 212+ * Publish message on Facebook wall.
 213+ */
 214+ public function publishStream($message, $link = "", $link_title ) {
 215+ /*
 216+ $attachment = array(
 217+ 'name' => $message,
 218+ 'href' => $link,
 219+ 'caption' => $caption,
 220+ 'description' => $description
 221+ );
 222+
 223+ if( count($media) > 0 ) {
 224+ foreach ( $media as $value ) {
 225+ $attachment[ 'media' ][] = $value;
 226+ }
 227+ }
 228+ /**/
 229+ // $api_error_descriptions
 230+ $attachment = array();
 231+
 232+ $query = array(
 233+ 'method' => 'stream.publish',
 234+ 'attachment' => json_encode($attachment),
 235+ 'action_links' => json_encode(array(
 236+ 'text' => $link_title,
 237+ 'href' => $link
 238+ )),
 239+ );
 240+
 241+ $result = json_decode( $this->api( $query ) );
 242+
 243+ if ( is_array( $result ) ) {
 244+ // Error
 245+ #error_log(FacebookAPIErrorCodes::$api_error_descriptions[$result]);
 246+ error_log("stream.publish returned error code $result->error_code");
 247+ return false;
 248+ } else if ( is_string( $result ) ) {
 249+ // Success! Return value is "$UserId_$PostId"
 250+ return true;
 251+ } else {
 252+ error_log("stream.publish: Unknown return type: " . gettype($result));
 253+ return false;
 254+ }
 255+ }
 256+}
Index: trunk/extensions/FBConnect/PreferencesExtension.php
@@ -0,0 +1,290 @@
 2+<?php
 3+
 4+/**
 5+ * See: http://www.mediawiki.org/wiki/Extension:PreferencesExtension
 6+ *
 7+ * This allows other extensions to add their own preferences to the default preferences display
 8+ *
 9+ * Author: Austin Che <http://openwetware.org/wiki/User:Austin_J._Che>
 10+ */
 11+
 12+$wgExtensionFunctions[] = "wfPreferencesExtension";
 13+$wgExtensionCredits['specialpage'][] = array(
 14+ 'name' => 'PreferencesExtension',
 15+ 'version' => '2008/02/10.2',
 16+ 'author' => 'Austin Che',
 17+ 'url' => 'http://openwetware.org/wiki/User:Austin_J._Che/Extensions/PreferencesExtension',
 18+ 'description' => 'Enables extending user preferences',
 19+);
 20+
 21+// constants for pref types
 22+define('PREF_USER_T', 1);
 23+define('PREF_TOGGLE_T', 2);
 24+define('PREF_TEXT_T', 3);
 25+define('PREF_PASSWORD_T', 4);
 26+define('PREF_INT_T', 5);
 27+define('PREF_DROPDOWN_T', 6);
 28+define('PREF_CUSTOM_HTML_T', 6);
 29+
 30+// each element of the following should be an array that can have keys:
 31+// name, section, type, size, validate, load, save, html, min, max, default
 32+if (!isset($wgExtensionPreferences))
 33+ $wgExtensionPreferences = array();
 34+
 35+function wfPreferencesExtension()
 36+{
 37+ wfProfileIn(__METHOD__);
 38+
 39+// wfUseMW('1.7');
 40+ global $wgHooks;
 41+ $wgHooks['SpecialPage_initList'][] = 'wfOverridePreferences';
 42+
 43+ wfProfileOut(__METHOD__);
 44+}
 45+
 46+/**
 47+ * Adds an array of prefs to be displayed in the user preferences
 48+ *
 49+ * @param array $prefs
 50+ */
 51+function wfAddPreferences($prefs)
 52+{
 53+ global $wgExtensionPreferences;
 54+ wfProfileIn(__METHOD__);
 55+
 56+ foreach ($prefs as $pref)
 57+ {
 58+ $wgExtensionPreferences[] = $pref;
 59+ }
 60+ wfProfileOut(__METHOD__);
 61+}
 62+
 63+function wfOverridePreferences(&$list)
 64+{
 65+ wfProfileIn(__METHOD__);
 66+
 67+ // we 'override' the default preferences special page with our own
 68+ $list["Preferences"] = array("SpecialPage", "Preferences", "", true, "wfSpecialPreferencesExtension");
 69+
 70+ wfProfileOut(__METHOD__);
 71+ return true;
 72+}
 73+
 74+function wfSpecialPreferencesExtension()
 75+{
 76+ wfProfileIn(__METHOD__);
 77+
 78+ global $IP;
 79+ require_once($IP . '/includes/specials/SpecialPreferences.php');
 80+
 81+ // override the default preferences form
 82+ class SpecialPreferencesExtension extends PreferencesForm
 83+ {
 84+ // overload to add new field by hook
 85+ function __construct( &$request ) {
 86+ global $wgExtensionPreferences, $wgUser;
 87+ parent::__construct($request);
 88+ wfRunHooks( 'initPreferencesExtensionForm', array( $wgUser, &$wgExtensionPreferences ) );
 89+ }
 90+ // unlike parent, we don't load in posted form values in constructor
 91+ // until savePreferences when we need it
 92+ // we also don't need resetPrefs, instead loading the newest values when displaying the form
 93+ // finally parent's execute function doesn't need overriding
 94+ // this leaves only two functions to override
 95+ // one for displaying the form and one for saving the values
 96+
 97+ function savePreferences()
 98+ {
 99+ // handle extension prefs first
 100+ global $wgUser, $wgRequest;
 101+ global $wgExtensionPreferences;
 102+ wfProfileIn(__METHOD__);
 103+
 104+ foreach ($wgExtensionPreferences as $p)
 105+ {
 106+ $name = isset($p['name']) ? $p['name'] : "";
 107+ if ( !$name ) {
 108+ continue;
 109+ }
 110+
 111+ $value = $wgRequest->getVal($name);
 112+ $type = isset($p['type']) ? $p['type'] : PREF_USER_T;
 113+
 114+ if ( !empty($p['int-type']) ) {
 115+ $type = $p['int-type'];
 116+ }
 117+
 118+ switch ($type)
 119+ {
 120+ case PREF_TOGGLE_T:
 121+ if (isset($p['save']))
 122+ $p['save']($name, $value);
 123+ else
 124+ $wgUser->setOption($name, $wgRequest->getCheck("wpOp{$name}"));
 125+ break;
 126+
 127+ case PREF_INT_T:
 128+ $min = isset($p['min']) ? $p['min'] : 0;
 129+ $max = isset($p['max']) ? $p['max'] : 0x7fffffff;
 130+ if (isset($p['save']))
 131+ $p['save']($name, $value);
 132+ else
 133+ $wgUser->setOption($name, $this->validateIntOrNull($value, $min, $max));
 134+ break;
 135+
 136+ case PREF_DROPDOWN_T:
 137+ if (isset($p['save']))
 138+ $p['save']($name, $value);
 139+ else
 140+ $wgUser->setOption($name, $value);
 141+ break;
 142+
 143+ case PREF_PASSWORD_T:
 144+ case PREF_TEXT_T:
 145+ case PREF_USER_T:
 146+ default:
 147+ if (isset($p['validate']))
 148+ $value = $p['validate']($value);
 149+ if (isset($p['save']))
 150+ $p['save']($name, $value);
 151+ else
 152+ $wgUser->setOption($name, $value);
 153+ break;
 154+ }
 155+ }
 156+
 157+ // call parent's function which saves the normal prefs and writes to the db
 158+ parent::savePreferences();
 159+
 160+ wfProfileOut(__METHOD__);
 161+ }
 162+
 163+ function mainPrefsForm( $status , $message = '' )
 164+ {
 165+ global $wgOut, $wgRequest, $wgUser;
 166+ global $wgExtensionPreferences;
 167+ wfProfileIn(__METHOD__);
 168+
 169+ // first get original form, then hack into it new options
 170+ parent::mainPrefsForm($status, $message);
 171+ $html = $wgOut->getHTML();
 172+ $wgOut->clearHTML();
 173+ $sections = array();
 174+ foreach ($wgExtensionPreferences as $p)
 175+ {
 176+ if (! isset($p['section']) || ! $p['section'])
 177+ continue;
 178+ $section = $p['section'];
 179+
 180+ $name = isset($p['name']) ? $p['name'] : "";
 181+
 182+ $value = "";
 183+ if ($name)
 184+ {
 185+ if (isset($p['load']))
 186+ $value = $p['load']($name);
 187+ else
 188+ $value = $wgUser->getOption($name);
 189+ }
 190+ if ($value === '' && isset($p['default']))
 191+ $value = $p['default'];
 192+
 193+ $sectext = htmlspecialchars(wfMsg($section));
 194+ $regex = "/(<fieldset>\s*<legend>\s*" . preg_quote($sectext) .
 195+ "\s*<\/legend>.*?)(<\/fieldset>)/s";
 196+
 197+ // check if $section exists in prefs yet
 198+ // cache the existence of sections
 199+ if (!isset($sections[$section]))
 200+ {
 201+ $sections[$section] = true;
 202+
 203+ if (! preg_match($regex, $html, $m))
 204+ {
 205+ // doesn't exist so add an empty section to end
 206+ $addhtml = "<fieldset><legend>$sectext</legend></fieldset>\n";
 207+ $html = preg_replace("/(<(div|table) id='prefsubmit'.*)/s", "$addhtml $1", $html);
 208+ }
 209+
 210+ }
 211+
 212+ $type = isset($p['type']) ? $p['type'] : PREF_USER_T;
 213+
 214+ if ( !empty($p['int-type']) ) {
 215+ $type = $p['int-type'];
 216+ }
 217+ $pos = isset($p['pos']) ? $p['pos'] : '';
 218+ switch ($type)
 219+ {
 220+ case PREF_CUSTOM_HTML_T:
 221+ $addhtml = $p['html'];
 222+ break;
 223+
 224+ case PREF_TOGGLE_T:
 225+ $addhtml = $this->getToggle($name);
 226+ break;
 227+
 228+ case PREF_INT_T:
 229+ case PREF_TEXT_T:
 230+ case PREF_PASSWORD_T:
 231+ $size = isset($p['size']) && $p['size'] ? "size=\"{$p['size']}\"" : "";
 232+ $caption = isset($p['caption']) && $p['caption'] ? $p['caption'] : wfMsg($name);
 233+ if ($type == PREF_PASSWORD_T)
 234+ $type = "password";
 235+ else
 236+ $type = "text";
 237+
 238+ if ($pos == 'first' || $pos == '')
 239+ $addhtml = "\n<table>\n";
 240+ else
 241+ $addhtml = '';
 242+ $addhtml .= $this->addRow("<label for=\"{$name}\">$caption</label>",
 243+ "<input type=\"$type\" id=\"{$name}\" name=\"{$name}\" value=\"{$value}\" $size />")."\n";
 244+ if ($pos == 'last' || $pos == '')
 245+ $addhtml .="</table>\n";
 246+ break;
 247+
 248+ case PREF_DROPDOWN_T:
 249+ $caption = isset($p['caption']) && $p['caption'] ? $p['caption'] : wfMsg($name);
 250+ $onchange = isset($p['onchange']) && $p['onchange'] ? (" onchange='" . $p['onchange'] . "'") : '';
 251+ $row = "\n <select id=\"{$name}\"$onchange name=\"{$name}\">\n";
 252+ $options = is_array($p['options']) ? $p['options'] : array();
 253+ foreach($options as $option)
 254+ {
 255+ $row .= " <option>$option</option>\n";
 256+ }
 257+ $row .= " </select>";
 258+
 259+ if ($pos == 'first' || $pos == '')
 260+ $addhtml = "\n<table>\n";
 261+ else
 262+ $addhtml = '';
 263+ $addhtml .= $this->addRow("<label for=\"{$name}\">$caption</label>", $row)."\n";
 264+ if ($pos == 'last' || $pos == '')
 265+ $addhtml .="</table>\n";
 266+ break;
 267+
 268+ case PREF_USER_T:
 269+ default:
 270+ $addhtml = preg_replace("/@VALUE@/", $value, isset($p['html']) ? $p['html'] : "");
 271+ break;
 272+ }
 273+
 274+ // the section exists
 275+ $html = preg_replace($regex, "$1 $addhtml $2", $html);
 276+ }
 277+
 278+ $wgOut->addHTML($html);
 279+
 280+ // debugging
 281+ //$wgOut->addHTML($wgUser->encodeOptions());
 282+ wfProfileOut(__METHOD__);
 283+ }
 284+ }
 285+
 286+ global $wgRequest;
 287+ $prefs = new SpecialPreferencesExtension($wgRequest);
 288+ $prefs->execute();
 289+
 290+ wfProfileOut(__METHOD__);
 291+}
Index: trunk/extensions/FBConnect/FBConnectUser.php
@@ -0,0 +1,162 @@
 2+<?php
 3+/*
 4+ * Copyright � 2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class FBConnectUser
 30+ *
 31+ * Extends the User class.
 32+ */
 33+class FBConnectUser extends User {
 34+ /**
 35+ * Constructor: Create this object from an existing User. Bonus points if
 36+ * the existing User was created form an ID and has not yet been loaded!
 37+ */
 38+ function __construct($user) {
 39+ $this->mId = $user->getId();
 40+ $this->mFrom = 'id';
 41+ }
 42+
 43+ /**
 44+ * Update a user's settings with the values retrieved from the current
 45+ * logged-in Facebook user. Settings are only updated if a different value
 46+ * is returned from Facebook and the user's settings allow an update on
 47+ * login.
 48+ */
 49+ function updateFromFacebook() {
 50+ wfProfileIn(__METHOD__);
 51+ global $facebook;
 52+
 53+ // Keep track of whether any settings were modified
 54+ $mod = false;
 55+ // Connect to the Facebook API and retrieve the user's info
 56+ $userinfo = $facebook->getUserInfo();
 57+ // Update the following options if the user's settings allow it
 58+ $updateOptions = array('nickname', 'fullname', 'language',
 59+ 'timecorrection', 'email');
 60+ foreach ($updateOptions as $option) {
 61+ // Translate Facebook parameters into MediaWiki parameters
 62+ $value = self::getOptionFromInfo($option, $userinfo);
 63+ if ($value && ($this->getOption("fbconnect-update-on-login-$option", "0") == "1")) {
 64+ switch ($option) {
 65+ case 'fullname':
 66+ $this->setRealName($value);
 67+ break;
 68+ default:
 69+ $this->setOption($option, $value);
 70+ }
 71+ $mod = true;
 72+ }
 73+ }
 74+ // Only save the updated settings if something was changed
 75+ if ($mod) {
 76+ $this->saveSettings();
 77+ }
 78+
 79+ wfProfileOut(__METHOD__);
 80+ }
 81+
 82+ /**
 83+ * Helper function for updateFromFacebook(). Takes an array of info from
 84+ * Facebook, and looks up the corresponding MediaWiki parameter.
 85+ */
 86+ static function getOptionFromInfo($option, $userinfo) {
 87+ // Lookup table for the names of the settings
 88+ $params = array('nickname' => 'username',
 89+ 'fullname' => 'name',
 90+ 'firstname' => 'first_name',
 91+ 'gender' => 'sex',
 92+ 'language' => 'locale',
 93+ 'timecorrection' => 'timezone',
 94+ 'email' => 'contact_email');
 95+ if (empty($userinfo)) {
 96+ return null;
 97+ }
 98+ $value = array_key_exists($params[$option], $userinfo) ? $userinfo[$params[$option]] : '';
 99+ // Special handling of several settings
 100+ switch ($option) {
 101+ case 'fullname':
 102+ case 'firstname':
 103+ // If real names aren't allowed, then simply ignore the parameter from Facebook
 104+ global $wgAllowRealName;
 105+ if (!$wgAllowRealName) {
 106+ $value = '';
 107+ }
 108+ break;
 109+ case 'gender':
 110+ // Unfortunately, Facebook genders are localized (but this might change)
 111+ if ($value != 'male' || $value != 'female') {
 112+ $value = '';
 113+ }
 114+ break;
 115+ case 'language':
 116+ /**
 117+ * Convert Facebook's locale into a MediaWiki language code.
 118+ * For an up-to-date list of Facebook locales, see:
 119+ * <http://wiki.developers.facebook.com/index.php/Facebook_Locales>.
 120+ * For an up-to-date list of MediaWiki languages, see:
 121+ * <http://svn.wikimedia.org/svnroot/mediawiki/trunk/phase3/languages/Names.php>.
 122+ */
 123+ if ($value == '') {
 124+ break;
 125+ }
 126+ // These regional languages get special treatment
 127+ $locales = array('en_PI' => 'en', # Pirate English
 128+ 'en_GB' => 'en-gb', # British English
 129+ 'en_UD' => 'en', # Upside Down English
 130+ 'fr_CA' => 'fr', # Canadian French
 131+ 'fb_LT' => 'en', # Leet Speak
 132+ 'pt_BR' => 'pt-br', # Brazilian Portuguese
 133+ 'zh_CN' => 'zh-cn', # Simplified Chinese
 134+ 'es_ES' => 'es', # European Spanish
 135+ 'zh_HK' => 'zh-hk', # Traditional Chinese (Hong Kong)
 136+ 'zh_TW' => 'zh-tw'); # Traditional Chinese (Taiwan)
 137+ if (array_key_exists($value, $locales)) {
 138+ $value = $locales[$value];
 139+ } else {
 140+ // No special regional treatment exists in MW; chop it off
 141+ $value = substr($value, 0, 2);
 142+ }
 143+ break;
 144+ case 'timecorrection':
 145+ // Convert the timezone into a local timezone correction
 146+ // TODO: $value = TimezoneToOffset($value);
 147+ $value = '';
 148+ break;
 149+ case 'email':
 150+ // If a contact email isn't available, then use a proxied email
 151+ if ($value == '') {
 152+ // TODO: update the user's email from $userinfo['proxied_email']
 153+ // instead (the address must stay hidden from the user)
 154+ $value = '';
 155+ }
 156+ // TODO: if the user's email is updated from Facebook, then
 157+ // automatically authenticate the email address
 158+ #$user->mEmailAuthenticated = wfTimestampNow();
 159+ }
 160+ // If an appropriate value was found, return it
 161+ return $value == '' ? null : $value;
 162+ }
 163+}
Index: trunk/extensions/FBConnect/FBConnect.php
@@ -0,0 +1,215 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+/**
 20+ * FBConnect plugin. Integrates Facebook Connect into MediaWiki.
 21+ *
 22+ * Facebook Connect single sign on (SSO) experience and XFBML are currently available.
 23+ * Please rename config.sample.php to config.php, follow the instructions inside and
 24+ * customize variables as you like to set up your Facebook Connect extension.
 25+ *
 26+ * Info is available at http://www.mediawiki.org/wiki/Extension:FBConnect
 27+ * and at http://wiki.developers.facebook.com/index.php/MediaWiki
 28+ *
 29+ * Limited support is available at http://www.mediawiki.org/wiki/Extension_talk:FBConnect
 30+ * and at http://wiki.developers.facebook.com/index.php/Talk:MediaWiki
 31+ *
 32+ * @author Garrett Bruin
 33+ * @copyright Copyright � 2008 Garrett Brown
 34+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
 35+ * @addtogroup Extensions
 36+ */
 37+
 38+
 39+/*
 40+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 41+ */
 42+if ( !defined( 'MEDIAWIKI' )) {
 43+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 44+}
 45+
 46+/*
 47+ * FBConnect version. Note: this is not necessarily the most recent SVN revision number.
 48+ */
 49+define( 'MEDIAWIKI_FBCONNECT_VERSION', 'r203, May 22, 2010' );
 50+
 51+/*
 52+ * Add information about this extension to Special:Version.
 53+ */
 54+$wgExtensionCredits['specialpage'][] = array(
 55+ 'name' => 'Facebook Connect Plugin',
 56+ 'author' => 'Garrett Brown',
 57+ 'url' => 'http://www.mediawiki.org/wiki/Extension:FBConnect',
 58+ 'descriptionmsg' => 'fbconnect-desc',
 59+ 'version' => MEDIAWIKI_FBCONNECT_VERSION,
 60+);
 61+
 62+/*
 63+ * Initialization of the autoloaders and special extension pages.
 64+ */
 65+$dir = dirname(__FILE__) . '/';
 66+require_once $dir . 'config.php';
 67+require_once $dir . 'php-sdk/facebook.php';
 68+if (!empty( $fbIncludePreferencesExtension ) && version_compare($wgVersion, '1.16', '<')) {
 69+ require_once $dir . 'PreferencesExtension.php';
 70+}
 71+
 72+$wgExtensionFunctions[] = 'FBConnect::init';
 73+
 74+if( !empty( $fbEnablePushToFacebook ) ){
 75+ // Need to include it explicitly instead of autoload since it has initialization code of its own.
 76+ // This should be done after FBConnect::init is added to wgExtensionFunctions so that FBConnect
 77+ // gets fully initialized first.
 78+ require_once $dir . 'FBConnectPushEvent.php';
 79+}
 80+
 81+$wgExtensionMessagesFiles['FBConnect'] = $dir . 'FBConnect.i18n.php';
 82+$wgExtensionAliasesFiles['FBConnect'] = $dir . 'FBConnect.alias.php';
 83+
 84+$wgAutoloadClasses['FBConnectAPI'] = $dir . 'FBConnectAPI.php';
 85+$wgAutoloadClasses['FBConnectDB'] = $dir . 'FBConnectDB.php';
 86+$wgAutoloadClasses['FBConnectHooks'] = $dir . 'FBConnectHooks.php';
 87+$wgAutoloadClasses['FBConnectUser'] = $dir . 'FBConnectUser.php';
 88+$wgAutoloadClasses['FBConnectXFBML'] = $dir . 'FBConnectXFBML.php';
 89+$wgAutoloadClasses['SpecialConnect'] = $dir . 'SpecialConnect.php';
 90+
 91+$wgSpecialPages['Connect'] = 'SpecialConnect';
 92+
 93+// Define new autopromote condition (use quoted text, numbers can cause collisions)
 94+define( 'APCOND_FB_INGROUP', 'fb*g' );
 95+define( 'APCOND_FB_ISOFFICER', 'fb*o' );
 96+define( 'APCOND_FB_ISADMIN', 'fb*a' );
 97+
 98+// Create a new group for Facebook users
 99+$wgGroupPermissions['fb-user'] = $wgGroupPermissions['user'];
 100+
 101+// If we are configured to pull group info from Facebook, then create the group permissions
 102+if ($fbUserRightsFromGroup) {
 103+ $wgGroupPermissions['fb-groupie'] = $wgGroupPermissions['user'];
 104+ $wgGroupPermissions['fb-officer'] = $wgGroupPermissions['bureaucrat'];
 105+ $wgGroupPermissions['fb-admin'] = $wgGroupPermissions['sysop'];
 106+ $wgImplictGroups[] = 'fb-groupie';
 107+ $wgImplictGroups[] = 'fb-officer';
 108+ $wgImplictGroups[] = 'fb-admin';
 109+ $wgAutopromote['fb-groupie'] = APCOND_FB_INGROUP;
 110+ $wgAutopromote['fb-officer'] = APCOND_FB_ISOFFICER;
 111+ $wgAutopromote['fb-admin'] = APCOND_FB_ISADMIN;
 112+}
 113+
 114+/**
 115+$wgAutopromote['autoconfirmed'] = array( '&', array( APCOND_EDITCOUNT, &$wgAutoConfirmCount ),
 116+ array( APCOND_AGE, &$wgAutoConfirmAge ),
 117+ array( APCOND_FB_INGROUP ));
 118+/**/
 119+
 120+
 121+/**
 122+ * Class FBConnect
 123+ *
 124+ * This class initializes the extension, and contains the core non-hook,
 125+ * non-authentification code.
 126+ */
 127+class FBConnect {
 128+ static private $fbOnLoginJs;
 129+
 130+ /**
 131+ * Initializes and configures the extension.
 132+ */
 133+ public static function init() {
 134+ global $wgXhtmlNamespaces, $wgSharedTables, $facebook, $wgHooks;
 135+ global $fbOnLoginJsOverride;
 136+
 137+ // The xmlns:fb attribute is required for proper rendering on IE
 138+ $wgXhtmlNamespaces['fb'] = 'http://www.facebook.com/2008/fbml';
 139+
 140+ // Facebook/username associations should be shared when $wgSharedDB is enabled
 141+ $wgSharedTables[] = 'user_fbconnect';
 142+
 143+ // Create our Facebook instance and make it available through $facebook
 144+ $facebook = new FBConnectAPI();
 145+
 146+ // Install all public static functions in class FBConnectHooks as MediaWiki hooks
 147+ $hooks = self::enumMethods('FBConnectHooks');
 148+ foreach( $hooks as $hookName ) {
 149+ $wgHooks[$hookName][] = "FBConnectHooks::$hookName";
 150+ }
 151+
 152+ // Allow configurable over-riding of the onLogin handler.
 153+ if(!empty($fbOnLoginJsOverride)){
 154+ self::$fbOnLoginJs = $fbOnLoginJsOverride;
 155+ } else {
 156+ self::$fbOnLoginJs = "window.location.reload(true);";
 157+ }
 158+
 159+ // ParserFirstCallInit was introduced in modern (1.12+) MW versions so as to
 160+ // avoid unstubbing $wgParser on setHook() too early, as per r35980
 161+ if (!defined( 'MW_SUPPORTS_PARSERFIRSTCALLINIT' )) {
 162+ global $wgParser;
 163+ wfRunHooks( 'ParserFirstCallInit', $wgParser );
 164+ }
 165+ }
 166+
 167+ /**
 168+ * Returns an array with the names of all public static functions
 169+ * in the specified class.
 170+ */
 171+ public static function enumMethods( $className ) {
 172+ $hooks = array();
 173+ try {
 174+ $class = new ReflectionClass( $className );
 175+ foreach( $class->getMethods( ReflectionMethod::IS_PUBLIC ) as $method ) {
 176+ if ( $method->isStatic() ) {
 177+ $hooks[] = $method->getName();
 178+ }
 179+ }
 180+ } catch( Exception $e ) {
 181+ // If PHP's version doesn't support the Reflection API, then exit
 182+ die( 'PHP version (' . phpversion() . ') must be great enough to support the Reflection API' );
 183+ // Or list the extensions here manually...
 184+ $hooks = array('AuthPluginSetup', 'UserLoadFromSession',
 185+ 'RenderPreferencesForm', 'PersonalUrls',
 186+ 'ParserAfterTidy', 'BeforePageDisplay', /*...*/ );
 187+ }
 188+ return $hooks;
 189+ }
 190+
 191+ /**
 192+ * Return the code for the permissions attribute (with leading space) to use on all fb:login-buttons.
 193+ */
 194+ public static function getPermissionsAttribute(){
 195+ global $fbExtendedPermissions;
 196+ $attr = "";
 197+ if(!empty($fbExtendedPermissions)){
 198+ $attr = " perms=\"".implode(",", $fbExtendedPermissions)."\"";
 199+ }
 200+ return $attr;
 201+ } // end getPermissionsAttribute()
 202+
 203+ /**
 204+ * Return the code for the onlogin attribute which should be appended to all fb:login-button's in this
 205+ * extension.
 206+ *
 207+ * TODO: Generate the entire fb:login-button in a function in this class. We have numerous buttons now.
 208+ */
 209+ public static function getOnLoginAttribute(){
 210+ $attr = "";
 211+ if(!empty(self::$fbOnLoginJs)){
 212+ $attr = " onlogin=\"".self::$fbOnLoginJs."\"";
 213+ }
 214+ return $attr;
 215+ } // end getOnLoginAttribute()
 216+}
Index: trunk/extensions/FBConnect/fbconnect.css
@@ -0,0 +1,78 @@
 2+/* Info about Facebook Connect on page Special:Connect */
 3+#specialconnect_info, #specialconnect_box {
 4+ font-family: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif;
 5+}
 6+
 7+#specialconnect_info p {
 8+ font-size: 14px;
 9+ color: #888;
 10+ margin: 16px 0;
 11+}
 12+
 13+#specialconnect_info th, #specialconnect_info td {
 14+ color: #333;
 15+ text-align: left;
 16+ vertical-align: top;
 17+ padding: 0 10px 0 0;
 18+}
 19+
 20+/* Login and Connect boxes */
 21+#specialconnect_boxarea {
 22+ border-collapse: collapse;
 23+ width: 92%;
 24+ margin: 28px 0;
 25+}
 26+
 27+#specialconnect_boxarea .box_left, #specialconnect_boxarea .box_right {
 28+ vertical-align: top;
 29+ width: 50%;
 30+ padding: 0 14px;
 31+}
 32+
 33+.box_left #specialconnect_box {
 34+ float: right;
 35+}
 36+
 37+.box_right #specialconnect_box {
 38+ float: left;
 39+}
 40+
 41+/* Facebook-styled box */
 42+#specialconnect_box .box_content {
 43+ color: #333;
 44+ font-size: 12px;
 45+ font-weight: normal;
 46+ margin: 0;
 47+ padding: 0;
 48+}
 49+
 50+#specialconnect_box h1 {
 51+ color: #333;
 52+ font-size: 13px;
 53+ font-weight: bold;
 54+ border-top: 1px solid #94A3C4;
 55+ border-bottom: 1px solid #CCCCCC;
 56+ background-color: #D8DFEA;
 57+}
 58+
 59+#specialconnect_box h1 p {
 60+ margin-top: 0;
 61+ margin-bottom: 0;
 62+}
 63+
 64+#specialconnect_box h1, #specialconnect_box .box_content {
 65+ margin-bottom: 2px;
 66+ padding: 1px 5px;
 67+}
 68+
 69+.box_left #specialconnect_box h1, .box_left #specialconnect_box .box_content {
 70+ padding-right: 120px;
 71+}
 72+
 73+.box_left #specialconnect_box img {
 74+ float: right;
 75+ border-collapse: collapse;
 76+ margin: 7px;
 77+ padding: 0;
 78+ width: 100px;
 79+}
Index: trunk/extensions/FBConnect/FBConnectPushEvent.php
@@ -0,0 +1,184 @@
 2+<?php
 3+/**
 4+ * @author Sean Colombo
 5+ *
 6+ * This class is an extendable superclass for events to push to a facebook news-feed.
 7+ *
 8+ * To create a push event, override this class, then add it to config.php in the way
 9+ * defined in config.sample.php.
 10+ */
 11+
 12+$wgExtensionFunctions[] = 'FBConnectPushEvent::initExtension';
 13+
 14+// PreferencesExtension is needed up until 1.16, then the needed functionality is built in.
 15+$wgHooks['GetPreferences'][] = 'FBConnectPushEvent::addPreferencesToggles';
 16+$wgHooks['initPreferencesExtensionForm'][] = 'FBConnectPushEvent::addPreferencesToggles';
 17+
 18+class FBConnectPushEvent {
 19+ protected $isAllowedUserPreferenceName = ''; // implementing classes MUST override this with their own value.
 20+
 21+ // This must correspond to the name of the message for the text on the tab itself.
 22+ static protected $PREFERENCES_TAB_NAME = "fbconnect-prefstext";
 23+
 24+ /**
 25+ * Accessor for the user preference to which (if set to 1) allows this type of event
 26+ * to be used.
 27+ */
 28+ public function getUserPreferenceName(){
 29+ return $this->isAllowedUserPreferenceName;
 30+ }
 31+
 32+ /**
 33+ * Initialize the extension itself. This includes creating the user-preferences for
 34+ * the push events.
 35+ */
 36+ static public function initExtension(){
 37+ wfProfileIn(__METHOD__);
 38+
 39+ // The push feature of the extension requires the publish_stream extended permission.
 40+ global $fbExtendedPermissions;
 41+ $PERMISSION_TO_PUBLISH = 'publish_stream';
 42+ if(empty($fbExtendedPermissions) || !is_array($fbExtendedPermissions)){
 43+ $fbExtendedPermissions = array($PERMISSION_TO_PUBLISH);
 44+ } else if(!in_array($PERMISSION_TO_PUBLISH, $fbExtendedPermissions)){
 45+ $fbExtendedPermissions[] = $PERMISSION_TO_PUBLISH;
 46+ }
 47+
 48+ // Make sure that all of the push events were configured correctly.
 49+ self::initAll();
 50+
 51+ // TODO: This initialization should only be run if the user is fb-connected. Otherwise, the same Connect form as Special:Connect should be shown.
 52+
 53+ // TODO: Can we detect if this is Special:Preferences and only add the checkboxes if that is the case? Can't think of anything else that would break.
 54+
 55+ wfProfileOut(__METHOD__);
 56+ } // end initExtension()
 57+
 58+ /**
 59+ * Adds enable/disable toggles to the Preferences form for controlling all push events.
 60+ *
 61+ */
 62+ static public function addPreferencesToggles( $user, &$preferences ){
 63+ global $fbPushEventClasses;
 64+
 65+ // TODO: PREF_TOGGLE_T is not defined in v1.16
 66+ /**
 67+ if( !defined('PREF_TOGGLE_T') ) {
 68+ return true;
 69+ }
 70+ /**/
 71+
 72+ if( !empty($fbPushEventClasses) ){
 73+ foreach($fbPushEventClasses as $pushEventClassName){
 74+ $pushObj = new $pushEventClassName;
 75+ $className = get_class();
 76+ $prefName = $pushObj->getUserPreferenceName();
 77+
 78+ $preferences[$prefName] = array(
 79+ 'type' => 'toggle',
 80+ 'label-message' => $prefName,
 81+ 'section' => self::$PREFERENCES_TAB_NAME,
 82+ 'default' => '1',
 83+ );
 84+
 85+ // Prior to v1.16
 86+ if( defined('PREF_TOGGLE_T') ) {
 87+ $preferences[$prefName]['int-type'] = PREF_TOGGLE_T;
 88+ $preferences[$prefName]['name'] = $prefName;
 89+ }
 90+ }
 91+ }
 92+
 93+ return true;
 94+ } // end addPreferencesToggles()
 95+
 96+ /**
 97+ * This function returns HTML which contains toggles (in a list) for setting the push
 98+ * Preferences. It is designed to be used inside of a form (such as on Special:Connect).
 99+ *
 100+ * This is not used by the code which adds the form to Special:Preferences.
 101+ *
 102+ * If firstTime is set to true, the checkboxes will default to being checked, otherwise
 103+ * they will default to the current user-option setting for the user.
 104+ */
 105+ static public function createPreferencesToggles($firstTime = false){
 106+ global $wgUser, $wgLang, $fbPushEventClasses;
 107+ wfProfileIn(__METHOD__);
 108+
 109+ $html = "";
 110+ if(!empty($fbPushEventClasses)){
 111+ foreach($fbPushEventClasses as $pushEventClassName){
 112+ $pushObj = new $pushEventClassName;
 113+ $className = get_class();
 114+ $prefName = $pushObj->getUserPreferenceName();
 115+
 116+ $prefText = $wgLang->getUserToggle( $prefName );
 117+ if($firstTime){
 118+ $checked = ' checked="checked"';
 119+ } else {
 120+ $checked = $wgUser->getOption( $prefName ) == 1 ? ' checked="checked"' : '';
 121+ }
 122+ $html .= "<div class='toggle'>";
 123+ $html .= "<input type='checkbox' value='1' id=\"$prefName\" name=\"$prefName\"$checked />";
 124+ $html .= "<label for=\"$prefName\">$prefText</label>";
 125+ $html .= "</div>\n";
 126+ }
 127+ }
 128+
 129+ wfProfileOut(__METHOD__);
 130+ return $html;
 131+ } // end createPreferencesToggles()
 132+
 133+ /**
 134+ * This static function is called by the FBConnect extension if push events are enabled. It checks
 135+ * to make sure that the configured push-events are valid and then gives them each a chance to initialize.
 136+ */
 137+ static public function initAll(){
 138+ global $fbPushEventClasses, $wgUser;
 139+ wfProfileIn(__METHOD__);
 140+
 141+ if( !empty( $fbPushEventClasses ) ) {
 142+ // Fail fast (and hard) if a push event was coded incorrectly.
 143+ foreach($fbPushEventClasses as $pushEventClassName){
 144+ $pushObj = new $pushEventClassName;
 145+ $className = get_class();
 146+ $prefName = $pushObj->getUserPreferenceName();
 147+ if(empty($prefName)){
 148+ $dirName = dir( __FILE__ );
 149+ $msg = "FATAL ERROR: The push event class <strong>\"$pushEventClassName\"</strong> does not return a valid user preference name! ";
 150+ $msg.= " It was probably written incorrectly. Either fix the class or remove it from being used in <strong>$dirName/config.php</strong>";
 151+ die($msg);
 152+ } else if(!is_subclass_of($pushObj, $className)){
 153+ $msg = "FATAL ERROR: The push event class <strong>\"$pushEventClassName\"</strong> is not a subclass of <strong>$className</strong>! ";
 154+ $msg.= " It was probably written incorrectly. Either fix the class or remove it from being used in <strong>$dirName/config.php</strong>";
 155+ die($msg);
 156+ }
 157+
 158+ // The push event is valid, let it initialize itself if needed.
 159+ if( $wgUser->getOption($prefName) ) {
 160+ $pushObj->init();
 161+ }
 162+ }
 163+ }
 164+
 165+ wfProfileOut(__METHOD__);
 166+ }
 167+
 168+ /**
 169+ * Overridable function to do any initialization needed by the push event.
 170+ *
 171+ * This is only called if this particular push-event is enabled in config.php
 172+ * and the getUserPreferenceName() call checks out (the result must be non-empty).
 173+ */
 174+ public function init(){}
 175+
 176+ /**
 177+ * Put Facebook message.
 178+ */
 179+ public function pushEvent($message, $link = null, $link_title = null) {
 180+ global $facebook;
 181+
 182+ return $facebook->publishStream($message, $link, $link_title);
 183+ }
 184+
 185+} // end FBConnectPushEvent class
Index: trunk/extensions/FBConnect/FBConnectHooks.php
@@ -0,0 +1,596 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class FBConnectHooks
 30+ *
 31+ * This class contains all the hooks used in this extension. HOOKS DO NOT NEED
 32+ * TO BE EXPLICITLY ADDED TO $wgHooks. Simply write a public static function
 33+ * with the same name as the hook that provokes it, place it inside this class
 34+ * and let FBConnect::init() do its magic. Helper functions should be private,
 35+ * because only public static methods are added as hooks.
 36+ */
 37+class FBConnectHooks {
 38+ /**
 39+ * Hook is called whenever an article is being viewed... Currently, figures
 40+ * out the Facebook ID of the user that the userpage belongs to.
 41+ */
 42+ public static function ArticleViewHeader( &$article, &$outputDone, &$pcache ) {
 43+ global $wgOut;
 44+ // Get the article title
 45+ $nt = $article->getTitle();
 46+ // If the page being viewed is a user page
 47+ if ($nt && $nt->getNamespace() == NS_USER && strpos($nt->getText(), '/') === false) {
 48+ $user = User::newFromName($nt->getText());
 49+ if (!$user || $user->getID() == 0) {
 50+ return true;
 51+ }
 52+ $fb_id = FBConnectDB::getFacebookIDs($user->getId());
 53+ if (!count($fb_id) || !($fb_id = $fb_id[0])) {
 54+ return true;
 55+ }
 56+ // TODO: Something with the Facebook ID stored in $fb_id
 57+ return true;
 58+ }
 59+ return true;
 60+ }
 61+
 62+ /**
 63+ * Checks the autopromote condition for a user.
 64+ */
 65+ static function AutopromoteCondition( $cond_type, $args, $user, &$result ) {
 66+ global $fbUserRightsFromGroup;
 67+
 68+ // Probably a redundant check, but with PHP you can never be too sure...
 69+ if (!$fbUserRightsFromGroup) {
 70+ // No group to pull rights from, so the user can't be a member
 71+ $result = false;
 72+ return true;
 73+ }
 74+ $types = array(APCOND_FB_INGROUP => 'member',
 75+ APCOND_FB_ISOFFICER => 'officer',
 76+ APCOND_FB_ISADMIN => 'admin');
 77+ $type = $types[$cond_type];
 78+ switch( $type ) {
 79+ case 'member':
 80+ case 'officer':
 81+ case 'admin':
 82+ global $facebook;
 83+ // Connect to the Facebook API and ask if the user is in the group
 84+ $rights = $facebook->getGroupRights($user);
 85+ $result = $rights[$type];
 86+ }
 87+
 88+ return true;
 89+ }
 90+
 91+ /**
 92+ * Injects some important CSS and Javascript into the <head> of the page.
 93+ */
 94+ public static function BeforePageDisplay( &$out, &$sk ) {
 95+ global $wgVersion, $fbLogo, $fbScript, $fbExtensionScript, $fbIncludeJquery,
 96+ $wgScriptPath, $wgJsMimeType, $wgStyleVersion;
 97+
 98+ // Asynchronously load the Facebook Connect JavaScript SDK before the page's content
 99+ if(!empty($fbScript)){
 100+ $out->prependHTML('
 101+ <div id="fb-root"></div>
 102+ <script>
 103+ (function(){var e=document.createElement("script");e.type="' .
 104+ $wgJsMimeType . '";e.src="' . $fbScript .
 105+ '";e.async=true;document.getElementById("fb-root").appendChild(e)})();
 106+ </script>' . "\n"
 107+ );
 108+ }
 109+
 110+ // Inserts list of global JavaScript variables if necessary
 111+ if (self::MGVS_hack( $mgvs_script )) {
 112+ $out->addInlineScript( $mgvs_script );
 113+ }
 114+
 115+ // Add a Facebook logo to the class .mw-fblink
 116+ $style = empty($fbLogo) ? '' : <<<STYLE
 117+ /* Add a pretty logo to Facebook links */
 118+ .mw-fblink {
 119+ background: url($fbLogo) top left no-repeat !important;
 120+ padding-left: 17px !important;
 121+ }
 122+STYLE;
 123+
 124+ // Things get a little simpler in 1.16...
 125+ if (version_compare($wgVersion, '1.16', '>=')) {
 126+ // Add a pretty Facebook logo if $fbLogo is set
 127+ if ( !empty( $fbLogo) ) {
 128+ $out->addInlineStyle($style);
 129+ }
 130+
 131+ // Don't include jQuery if it's already in use on the site
 132+ #$out->includeJQuery();
 133+ // Temporary workaround until until MW is bundled with jQuery 1.4.2:
 134+ $out->addScriptFile('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
 135+
 136+ // Add the script file specified by $url
 137+ if(!empty($fbExtensionScript)){
 138+ $out->addScriptFile($fbExtensionScript);
 139+ }
 140+ } else {
 141+ // Add a pretty Facebook logo if $fbLogo is set
 142+ if ( !empty( $fbLogo) ) {
 143+ $out->addScript('<style type="text/css">' . $style . '</style>');
 144+ }
 145+
 146+ // Don't include jQuery if it's already in use on the site
 147+ if (!empty($fbIncludeJquery)){
 148+ $out->addScriptFile("http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js");
 149+ }
 150+
 151+ // Add the script file specified by $url
 152+ if(!empty($fbExtensionScript)){
 153+ $out->addScript("<script type=\"$wgJsMimeType\" src=\"$fbExtensionScript?$wgStyleVersion\"></script>\n");
 154+ }
 155+ }
 156+ return true;
 157+ }
 158+
 159+ /**
 160+ * Fired when MediaWiki is updated to allow FBConnect to update the database.
 161+ * If the database type is supported, then a new tabled named 'user_fbconnect'
 162+ * is created. For the table's layout, see fbconnect_table.sql. If $wgDBprefix
 163+ * is set, then the table 'user_fbconnect' will be prefixed accordingly. Make
 164+ * sure that fbconnect_table.sql is updated with the database prefix beforehand.
 165+ */
 166+ static function LoadExtensionSchemaUpdates() {
 167+ global $wgDBtype, $wgDBprefix, $wgExtNewTables;
 168+ $base = dirname( __FILE__ );
 169+ if ( $wgDBtype == 'mysql' ) {
 170+ $wgExtNewTables[] = array("{$wgDBprefix}user_fbconnect", "$base/fbconnect_table.sql");
 171+ } else if ( $wgDBtype == 'postgres' ) {
 172+ $wgExtNewTables[] = array("{$wgDBprefix}user_fbconnect", "$base/fbconnect_table.pg.sql");
 173+ }
 174+ return true;
 175+ }
 176+
 177+ /**
 178+ * Adds several Facebook Connect variables to the page:
 179+ *
 180+ * fbAppId The application ID (see $fbAppId in config.php)
 181+ * fbSession Assist the JavaScript SDK with loading the session
 182+ * fbUseMarkup Should XFBML tags be rendered (see $fbUseMarkup in config.php)
 183+ * fbLogo Facebook logo (see $fbLogo in config.php)
 184+ * fbLogoutURL The URL to be redirected to on a disconnect
 185+ *
 186+ * This hook was added in MediaWiki version 1.14. See:
 187+ * http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Skin.php?view=log&pathrev=38397
 188+ * If we are not at revision 38397 or later, this function is called from BeforePageDisplay
 189+ * to retain backward compatability.
 190+ */
 191+ public static function MakeGlobalVariablesScript( &$vars ) {
 192+ global $fbAppId, $facebook, $fbUseMarkup, $fbLogo, $wgTitle, $wgRequest;
 193+ $vars['fbAppId'] = $fbAppId;
 194+ $vars['fbSession'] = $facebook->getSession();
 195+ $vars['fbUseMarkup'] = $fbUseMarkup;
 196+ $vars['fbLogo'] = $fbLogo ? true : false;
 197+ $vars['fbLogoutURL'] = Skin::makeSpecialUrl('Userlogout',
 198+ $wgTitle->isSpecial('Preferences') ? '' :
 199+ "returnto={$wgTitle->getPrefixedURL()}");
 200+ $query = $wgRequest->getValues();
 201+ $vars['wgPagequery'] = wfUrlencode( wfArrayToCGI( $query ) );
 202+ return true;
 203+ }
 204+
 205+ /**
 206+ * Hack: Run MakeGlobalVariablesScript for backwards compatability.
 207+ * The MakeGlobalVariablesScript hook was added to MediaWiki 1.14 in revision 38397:
 208+ * http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/Skin.php?view=log&pathrev=38397
 209+ */
 210+ private static function MGVS_hack( &$script ) {
 211+ global $wgVersion, $IP;
 212+ if (version_compare($wgVersion, '1.14', '<')) {
 213+ $svn = SpecialVersion::getSvnRevision($IP);
 214+ // if !$svn, then we must be using 1.13.x (as opposed to 1.14alpha+)
 215+ if (!$svn || $svn < 38397)
 216+ {
 217+ $script = "";
 218+ $vars = array();
 219+ wfRunHooks('MakeGlobalVariablesScript', array(&$vars));
 220+ foreach( $vars as $name => $value ) {
 221+ $script .= "\t\tvar $name = " . json_encode($value) . ";\n";
 222+ }
 223+ return true;
 224+ }
 225+ }
 226+ return false;
 227+ }
 228+
 229+ /**
 230+ * Installs a parser hook for every tag reported by FBConnectXFBML::availableTags().
 231+ * Accomplishes this by asking FBConnectXFBML to create a hook function that then
 232+ * redirects to FBConnectXFBML::parserHook().
 233+ */
 234+ public static function ParserFirstCallInit( &$parser ) {
 235+ $pHooks = FBConnectXFBML::availableTags();
 236+ foreach( $pHooks as $tag ) {
 237+ $parser->setHook( $tag, FBConnectXFBML::createParserHook( $tag ));
 238+ }
 239+ return true;
 240+ }
 241+
 242+ /**
 243+ * Modify the user's persinal toolbar (in the upper right).
 244+ *
 245+ * TODO: Better 'returnto' code
 246+ */
 247+ public static function PersonalUrls( &$personal_urls, &$wgTitle ) {
 248+ global $facebook, $wgUser, $wgLang, $wgShowIPinHeader;
 249+ global $fbPersonalUrls, $fbConnectOnly;
 250+ wfLoadExtensionMessages('FBConnect');
 251+
 252+ /*
 253+ * Personal URLs option: remove_user_talk_link
 254+ */
 255+ if ($fbPersonalUrls['remove_user_talk_link'] &&
 256+ array_key_exists('mytalk', $personal_urls)) {
 257+ unset($personal_urls['mytalk']);
 258+ }
 259+
 260+ // If the user is logged in and connected
 261+ if ( $wgUser->isLoggedIn() && $facebook->getSession() ) {
 262+ /*
 263+ * Personal URLs option: use_real_name_from_fb
 264+ */
 265+ if ( !empty( $fbPersonalUrls['use_real_name_from_fb'] ) ) {
 266+ // Start with the real name in the database
 267+ $name = $wgUser->getRealName();
 268+ // Ask Facebook for the real name
 269+ if (!$name || $name == '') {
 270+ try {
 271+ // This might fail if we load a stale session from cookies
 272+ $fbUser = $facebook->api('/me');
 273+ $name = $fbUser['name'];
 274+ } catch (FacebookApiException $e) {
 275+ error_log($e);
 276+ }
 277+ }
 278+ // Make sure we were able to get a name from the database or Facebook
 279+ if ($name && $name != '') {
 280+ $personal_urls['userpage']['text'] = $name;
 281+ }
 282+ }
 283+ // Replace logout link with a button to disconnect from Facebook Connect
 284+ if( empty( $fbPersonalUrls['hide_logout_of_fb'] ) ){
 285+ unset( $personal_urls['logout'] );
 286+ $personal_urls['fblogout'] = array(
 287+ 'text' => wfMsg( 'fbconnect-logout' ),
 288+ 'href' => '#',
 289+ 'active' => false );
 290+ }
 291+
 292+ /*
 293+ * Personal URLs option: link_back_to_facebook
 294+ */
 295+ if ($fbPersonalUrls['link_back_to_facebook']) {
 296+ try {
 297+ $fbUser = $facebook->api('/me');
 298+ $link = $fbUser['link'];
 299+ } catch (FacebookApiException $e) {
 300+ $link = 'http://www.facebook.com/profile.php?id=' .
 301+ $facebook->getUser();
 302+ }
 303+ $personal_urls['fblink'] = array(
 304+ 'text' => wfMsg( 'fbconnect-link' ),
 305+ 'href' => $link,
 306+ 'active' => false
 307+ );
 308+ }
 309+ }
 310+ // User is logged in but not Connected
 311+ else if ($wgUser->isLoggedIn()) {
 312+ /*
 313+ * Personal URLs option: hide_convert_button
 314+ */
 315+ if (!$fbPersonalUrls['hide_convert_button']) {
 316+ $personal_urls['fbconvert'] = array(
 317+ 'text' => wfMsg( 'fbconnect-convert' ),
 318+ 'href' => SpecialConnect::getTitleFor('Connect', 'Convert')->getLocalURL(
 319+ 'returnto=' . $wgTitle->getPrefixedURL()),
 320+ 'active' => $wgTitle->isSpecial( 'Connect' )
 321+ );
 322+ }
 323+ }
 324+ // User is not logged in
 325+ else {
 326+ /*
 327+ * Personal URLs option: hide_connect_button
 328+ */
 329+ if (!$fbPersonalUrls['hide_connect_button']) {
 330+ // Add an option to connect via Facebook Connect
 331+ $personal_urls['fbconnect'] = array(
 332+ 'text' => wfMsg( 'fbconnect-connect' ),
 333+ 'href' => SpecialPage::getTitleFor( 'Connect' )->
 334+ getLocalUrl( 'returnto=' . $wgTitle->getPrefixedURL() ),
 335+ 'active' => $wgTitle->isSpecial('Connect')
 336+ );
 337+ }
 338+
 339+ // Remove other personal toolbar links
 340+ if ( !empty( $fbConnectOnly ) ) {
 341+ foreach (array('login', 'anonlogin') as $k) {
 342+ if (array_key_exists($k, $personal_urls)) {
 343+ unset($personal_urls[$k]);
 344+ }
 345+ }
 346+ }
 347+ }
 348+ return true;
 349+ }
 350+
 351+ /**
 352+ * Modify the preferences form. At the moment, we simply turn the user name
 353+ * into a link to the user's facebook profile.
 354+ *
 355+ * TODO!
 356+ */
 357+ public static function RenderPreferencesForm( $form, $output ) {
 358+ global $facebook, $wgUser;
 359+
 360+ // This hook no longer seems to work...
 361+ return true;
 362+
 363+ if( $facebook->getSession() ) {
 364+ $html = $output->getHTML();
 365+ $name = $wgUser->getName();
 366+ $i = strpos( $html, $name );
 367+ if ($i !== FALSE) {
 368+ // If the user has a valid Facebook ID, link to the Facebook profile
 369+ try {
 370+ $fbUser = $facebook->api('/me');
 371+ // Replace the old output with the new output
 372+ $html = substr( $html, 0, $i ) .
 373+ preg_replace("/$name/", "$name (<a href=\"$fbUser[link]\" " .
 374+ "class='mw-userlink mw-fbconnectuser'>" .
 375+ wfMsg('fbconnect-link-to-profile') . "</a>",
 376+ substr( $html, $i ), 1);
 377+ $output->clearHTML();
 378+ $output->addHTML( $html );
 379+ } catch (FacebookApiException $e) {
 380+ error_log($e);
 381+ }
 382+ }
 383+ }
 384+ return true;
 385+ }
 386+
 387+ /**
 388+ * Adds the class "mw-userlink" to links belonging to Connect accounts on
 389+ * the page Special:ListUsers.
 390+ */
 391+ static function SpecialListusersFormatRow( &$item, $row ) {
 392+ global $fbSpecialUsers;
 393+
 394+ // Only modify Facebook Connect users
 395+ if (!$fbSpecialUsers ||
 396+ !count(FBConnectDB::getFacebookIDs(User::newFromName($row->user_name)))) {
 397+ return true;
 398+ }
 399+
 400+ // Look to see if class="..." appears in the link
 401+ $regs = array();
 402+ preg_match( '/^([^>]*?)class=(["\'])([^"]*)\2(.*)/', $item, $regs );
 403+ if (count( $regs )) {
 404+ // If so, append " mw-userlink" to the end of the class list
 405+ $item = $regs[1] . "class=$regs[2]$regs[3] mw-userlink$regs[2]" . $regs[4];
 406+ } else {
 407+ // Otherwise, stick class="mw-userlink" into the link just before the '>'
 408+ preg_match( '/^([^>]*)(.*)/', $item, $regs );
 409+ $item = $regs[1] . ' class="mw-userlink"' . $regs[2];
 410+ }
 411+ return true;
 412+ }
 413+
 414+ /**
 415+ * Adds some info about the governing Facebook group to the header form of
 416+ * Special:ListUsers.
 417+ */
 418+ static function SpecialListusersHeaderForm( &$pager, &$out ) {
 419+ global $fbUserRightsFromGroup, $facebook, $fbLogo;
 420+ if ( empty($fbUserRightsFromGroup) ) {
 421+ return true;
 422+ }
 423+
 424+ // TODO: Do we need to verify the Facebook session here?
 425+
 426+ $gid = $fbUserRightsFromGroup;
 427+ // Connect to the API and get some info about the group
 428+ try {
 429+ $group = $facebook->api('/' . $gid);
 430+ } catch (FacebookApiException $e) {
 431+ error_log($e);
 432+ }
 433+ $out .= '
 434+ <table style="border-collapse: collapse;">
 435+ <tr>
 436+ <td>
 437+ ' . wfMsgWikiHtml( 'fbconnect-listusers-header',
 438+ wfMsg( 'group-bureaucrat-member' ), wfMsg( 'group-sysop-member' ),
 439+ "<a href=\"http://www.facebook.com/group.php?gid=$gid\">$group[name]</a>",
 440+ "<a href=\"http://www.facebook.com/profile.php?id={$group[owner][id]}\" " .
 441+ "class=\"mw-userlink\">{$group[owner][name]}</a>") . "
 442+ </td>
 443+ <td>
 444+ <img src=\"https://graph.facebook.com/$gid/picture?type=large\" title=\"$group[name]\" alt=\"$group[name]\">
 445+ </td>
 446+ </tr>
 447+ </table>";
 448+ return true;
 449+ }
 450+
 451+ /**
 452+ * Removes Special:UserLogin and Special:CreateAccount from the list of
 453+ * special pages if $fbConnectOnly is set to true.
 454+ */
 455+ static function SpecialPage_initList( &$aSpecialPages ) {
 456+ global $fbConnectOnly;
 457+ if ( !empty( $fbConnectOnly) ) {
 458+ $aSpecialPages['Userlogin'] = array('SpecialRedirectToSpecial', 'UserLogin',
 459+ 'Connect', false, array('returnto', 'returntoquery'));
 460+ // Used in 1.12.x and above
 461+ $aSpecialPages['CreateAccount'] = array('SpecialRedirectToSpecial',
 462+ 'CreateAccount', 'Connect');
 463+ }
 464+ return true;
 465+ }
 466+
 467+ /**
 468+ * HACK: Please someone fix me or explain why this is necessary!
 469+ *
 470+ * Unstub $wgUser to avoid race conditions and stop returning stupid false
 471+ * negatives!
 472+ *
 473+ * This might be due to a bug in User::getRights() [called from
 474+ * User::isAllowed('read'), called from Title::userCanRead()], where mRights
 475+ * is retrieved from an uninitialized user. From my probing, it seems that
 476+ * the user is uninitialized with almost all members blank except for mFrom,
 477+ * equal to 'session'. The second time around, $user seems to point to the
 478+ * User object after being loaded from the session. After the user is loaded
 479+ * it has all the appropriate groups. However, before being loaded it seems
 480+ * that instead of being null, mRights is equal to the array
 481+ * (createaccount, createpage, createtalk, writeapi).
 482+ */
 483+ static function userCan (&$title, &$user, $action, &$result) {
 484+ // Unstub $wgUser (is there a more succinct way to do this?)
 485+ $user->getId();
 486+ return true;
 487+ }
 488+
 489+ /**
 490+ * Removes the 'createaccount' right from users if $fbConnectOnly is true.
 491+ */
 492+ static function UserGetRights( &$user, &$aRights ) {
 493+ global $fbConnectOnly;
 494+ if ( !empty( $fbConnectOnly ) ) {
 495+ foreach ( $aRights as $i => $right ) {
 496+ if ( $right == 'createaccount' ) {
 497+ unset( $aRights[$i] );
 498+ break;
 499+ }
 500+ }
 501+ }
 502+ return true;
 503+ }
 504+
 505+ /**
 506+ * If the user isn't logged in, try to auto-authenticate via Facebook
 507+ * Connect. The Single Sign On magic of FBConnect happens in this function.
 508+ */
 509+ static function UserLoadFromSession( $user, &$result ) {
 510+ global $facebook, $wgCookiePrefix, $wgTitle, $wgOut, $wgUser;
 511+
 512+ // Check to see if the user can be logged in from Facebook
 513+ $fbId = $facebook->getSession() ? $facebook->getUser() : 0;
 514+ // Check to see if the user can be loaded from the session
 515+ $localId = isset($_COOKIE["{$wgCookiePrefix}UserID"]) ?
 516+ intval($_COOKIE["{$wgCookiePrefix}UserID"]) :
 517+ (isset($_SESSION['wsUserID']) ? $_SESSION['wsUserID'] : 0);
 518+
 519+ // Case: Not logged into Facebook, but logged into the wiki
 520+ if (!$fbId && $localId) {
 521+ $mwUser = User::newFromId($localId);
 522+ // If the user was Connected, the JS should have logged them out...
 523+ // TODO: test to see if they logged in normally (with a password)
 524+ #if (FBConnectDB::userLoggedInWithPassword($mwUser)) return true;
 525+ if (count(FBConnectDB::getFacebookIDs($mwUser))) {
 526+ // Oh well, they shouldn't be here anyways; silently log them out
 527+ $mwUser->logout();
 528+ // Defaults have just been loaded, so save some time
 529+ $result = false;
 530+ }
 531+ }
 532+ // Case: Logged into Facebook, not logged into the wiki
 533+ else if ($fbId && !$localId) {
 534+ // Look up the MW ID of the Facebook user
 535+ $mwUser = FBConnectDB::getUser($fbId);
 536+ $id = $mwUser ? $mwUser->getId() : 0;
 537+ // If the user doesn't exist, ask them to name their new account
 538+ if (!$id) {
 539+ // TODO: $wgTitle was empty for some strange reason...
 540+ if (!empty( $wgTitle )) {
 541+ $returnto = $wgTitle->isSpecial('Userlogout') || $wgTitle->isSpecial('Connect') ?
 542+ '' : 'returnto=' . $wgTitle->getPrefixedURL();
 543+ } else {
 544+ $returnto = '';
 545+ }
 546+ // Don't redirect if we're on certain special pages
 547+ if ($returnto != '') {
 548+ // Redirect to Special:Connect so the Facebook user can choose a nickname
 549+ $wgOut->redirect($wgUser->getSkin()->makeSpecialUrl('Connect', $returnto));
 550+ }
 551+ } else {
 552+ // TODO: To complete the SSO experience, this should log the user on
 553+ /*
 554+ // Load the user from their ID
 555+ $user->mId = $id;
 556+ $user->mFrom = 'id';
 557+ $user->load();
 558+ // Update user's info from Facebook
 559+ $fbUser = new FBConnectUser($mwUser);
 560+ $fbUser->updateFromFacebook();
 561+ // Authentification okay, no need to continue with User::loadFromSession()
 562+ $result = true;
 563+ /**/
 564+ }
 565+ }
 566+ // Case: Not logged into Facebook or the wiki
 567+ // Case: Logged into Facebook, logged into the wiki
 568+ return true;
 569+ }
 570+
 571+ /**
 572+ *
 573+ *
 574+ */
 575+ static function initPreferencesExtensionForm($user, &$wgExtensionPreferences) {
 576+ $id = FBConnectDB::getFacebookIDs($user);
 577+ if( count($id) > 0 ) {
 578+ //action="/index.php?title=TechTeam_QA_8_Wiki&amp;action=submit" method="post"
 579+ $action = Title::makeTitle(NS_SPECIAL,"Connect");
 580+ $action = $action->getFullURL("action=disconnect");
 581+ $html = Xml::openElement("div");
 582+ $html .= Xml::openElement( "form", array("submit" => "post", "action" => $action) );
 583+ $html .= Xml::element( "input", array("type" => "submit", "value" => "Disconent"), '', true );
 584+ $html .= Xml::closeElement( "form" );
 585+ $html .= Xml::closeElement( "div" );
 586+ $wgExtensionPreferences = array_merge(
 587+ array(
 588+ array(
 589+ 'name' => 'ssasasas',
 590+ 'section' => 'fbconnect-prefstext',
 591+ 'html' => $html,
 592+ 'type' => PREF_CUSTOM_HTML_T) ), $wgExtensionPreferences);
 593+ }
 594+
 595+ return true;
 596+ }
 597+}
Index: trunk/extensions/FBConnect/FBConnectDB.php
@@ -0,0 +1,162 @@
 2+<?php
 3+/*
 4+ * Copyright � 2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * Class FBConnectDB
 30+ *
 31+ * This class abstracts the manipulation of the custom table used by this
 32+ * extension. If $wgDBprefix is set, this class will pull from the translated
 33+ * tables. If the table 'users_fbconnect' does not exist in your database
 34+ * you will receive errors like this:
 35+ *
 36+ * Database error from within function "FBConnectDB::getUser". Database
 37+ * returned error "Table 'user_fbconnect' doesn't exist".
 38+ *
 39+ * In this case, you will need to fix this by running the MW updater:
 40+ * >php maintenance/update.php
 41+ */
 42+class FBConnectDB {
 43+ /**
 44+ * Find the Facebook IDs of the given user, if any, using the database connection provided.
 45+ *
 46+ */
 47+ public static function getFacebookIDs( $user ) {
 48+ $dbr = wfGetDB( DB_SLAVE, array(), self::sharedDB() );
 49+ $fbid = array();
 50+ if ( $user instanceof User && $user->getId() != 0 ) {
 51+ $prefix = self::getPrefix();
 52+ $res = $dbr->select(
 53+ array( "{$prefix}user_fbconnect" ),
 54+ array( 'user_fbid' ),
 55+ array( 'user_id' => $user->getId() ),
 56+ __METHOD__
 57+ );
 58+ foreach( $res as $row ) {
 59+ $fbid[] = $row->user_fbid;
 60+ }
 61+ $res->free();
 62+ }
 63+ return $fbid;
 64+ }
 65+
 66+ /**
 67+ * Find the user by their Facebook ID.
 68+ * If there is no user found for the given id, returns null.
 69+ */
 70+ public static function getUser( $fbid ) {
 71+ $prefix = self::getPrefix();
 72+ // NOTE: Do not just pass this dbr into getUserByDB since that function prevents
 73+ // rewriting of the database name for shared tables.
 74+ $dbr = & wfGetDB( DB_SLAVE, array(), self::sharedDB() );
 75+ $id = $dbr->selectField(
 76+ array( "{$prefix}user_fbconnect" ),
 77+ array( 'user_id' ),
 78+ array( 'user_fbid' => $fbid ),
 79+ __METHOD__
 80+ );
 81+
 82+ if ( $id ) {
 83+ return User::newFromId( $id );
 84+ } else {
 85+ return null;
 86+ }
 87+ }
 88+
 89+ /**
 90+ * Add a User <-> Facebook ID association to the database.
 91+ */
 92+ public static function addFacebookID( $user, $fbid ) {
 93+ wfProfileIn(__METHOD__);
 94+
 95+ $prefix = self::getPrefix();
 96+ $dbw = wfGetDB( DB_MASTER, array(), self::sharedDB() );
 97+ $dbw->insert(
 98+ "{$prefix}user_fbconnect",
 99+ array(
 100+ 'user_id' => $user->getId(),
 101+ 'user_fbid' => $fbid
 102+ ),
 103+ __METHOD__
 104+ );
 105+ wfProfileOut(__METHOD__);
 106+ }
 107+
 108+ /**
 109+ * Remove a User <-> Facebook ID association from the database.
 110+ */
 111+ public static function removeFacebookID( $user, $fbid ) {
 112+ $prefix = self::getPrefix();
 113+ $dbw = wfGetDB( DB_MASTER, array(), self::sharedDB() );
 114+ $dbw->delete(
 115+ "{$prefix}user_fbconnect",
 116+ array(
 117+ 'user_id' => $user->getId(),
 118+ 'user_fbid' => $fbid
 119+ ),
 120+ __METHOD__
 121+ );
 122+ return (bool) $dbw->affectedRows();
 123+ }
 124+
 125+ /**
 126+ * Estimates the total number of User <-> Facebook ID associations in the
 127+ * database. If there are no users, then the estimate will probably be 1.
 128+ */
 129+ public static function countUsers() {
 130+ $prefix = self::getPrefix();
 131+ $dbr = wfGetDB( DB_SLAVE, array(), self::sharedDB() );
 132+ // An estimate is good enough for choosing a unique nickname
 133+ $count = $dbr->estimateRowCount("{$prefix}user_fbconnect");
 134+ // Avoid returning 0 or -1
 135+ return $count >= 1 ? $count : 1;
 136+ }
 137+
 138+ /**
 139+ * Returns the name of the shared database, if one is in use for the Facebook
 140+ * Connect users table. Note that 'user_fbconnect' (without respecting
 141+ * $wgSharedPrefix) is added to $wgSharedTables in FBConnect::init by default.
 142+ * This function can also be used as a test for whether a shared database for
 143+ * Facebook Connect users is in use.
 144+ *
 145+ * See also: <http://www.mediawiki.org/wiki/Manual:Shared_database>
 146+ */
 147+ private static function sharedDB() {
 148+ global $wgExternalSharedDB;
 149+ if (!empty($wgExternalSharedDB)) {
 150+ return $wgExternalSharedDB;
 151+ }
 152+ return false;
 153+ }
 154+
 155+ /**
 156+ * Returns the table prefix name, either $wgDBprefix, $wgSharedPrefix
 157+ * depending on whether a shared database is in use.
 158+ */
 159+ private static function getPrefix() {
 160+ global $wgDBprefix, $wgSharedPrefix;
 161+ return self::sharedDB() ? $wgSharedPrefix : $wgDBprefix;
 162+ }
 163+}
Index: trunk/extensions/FBConnect/fbconnect.min.js
@@ -0,0 +1,2 @@
 2+window.fbAsyncInit=function(){FB.init({appId:window.fbAppId,session:window.fbSession,status:true,cookie:true,xfbml:window.fbUseMarkup});FB.Event.subscribe("auth.logout",function(){if(confirm("Not logged in.\n\nYou have been loggout out of Facebook. Press OK to log in via Facebook Connect again, or press Cancel to stay on the current page."))window.location=window.wgArticlePath.replace(/\$1/,"Special:Connect")})};
 3+$(document).ready(function(){$("#pt-fbconnect,#pt-fblink,#pt-fbconvert").addClass("mw-fblink");$("#pt-fblogout").click(function(){confirm("You are logging out of both this site and Facebook.")&&FB.logout(function(){window.location=window.fbLogoutURL})})});function sendToConnectOnLogin(){window.location.href=wgServer+wgScript+"?title=Special:Connect&returnto="+wgPageName+"&returntoquery="+wgPagequery};
\ No newline at end of file
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnWatchArticle.i18n.php
@@ -0,0 +1,5 @@
 2+<?php
 3+
 4+$messages['en'] = array(
 5+ 'tog-fbconnect-push-allow-OnWatchArticle' => "Post to my Facebook News Feed when I follow (watchlist) an article.",
 6+);
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnAddImage.i18n.php
@@ -0,0 +1,5 @@
 2+<?php
 3+
 4+$messages['en'] = array(
 5+ 'tog-fbconnect-push-allow-OnAddImage' => "Post to my Facebook News Feed when I add an image.",
 6+);
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnWatchArticle.php
@@ -0,0 +1,35 @@
 2+<?php
 3+/**
 4+ * @author Sean Colombo
 5+ *
 6+ * Pushes an item to Facebook News Feed when the user adds an article to their watchlist.
 7+ */
 8+
 9+global $wgExtensionMessagesFiles;
 10+$pushDir = dirname(__FILE__) . '/';
 11+$wgExtensionMessagesFiles['FBPush_OnWatchArticle'] = $pushDir . "FBPush_OnWatchArticle.i18n.php";
 12+
 13+class FBPush_OnWatchArticle extends FBConnectPushEvent {
 14+ protected $isAllowedUserPreferenceName = 'fbconnect-push-allow-OnWatchArticle'; // must correspond to an i18n message that is 'tog-[the value of the string on this line]'.
 15+
 16+ public function init(){
 17+ global $wgHooks;
 18+ wfProfileIn(__METHOD__);
 19+
 20+ $wgHooks['ArticleSaveComplete'][] = 'FBPush_OnWatchArticle::articleCountPagesAddedInLastHour';
 21+ wfLoadExtensionMessages('FBPush_OnWatchArticle');
 22+
 23+ wfProfileOut(__METHOD__);
 24+ }
 25+
 26+
 27+ public static function articleCountPagesAddedInLastHour(&$article, &$user, $text, $summary,$flag, $fake1, $fake2, &$flags, $revision, &$status, $baseRevId){
 28+ global $wgContentNamespaces;
 29+ wfProfileIn(__METHOD__);
 30+ if( in_array($article->getTitle()->getNamespace(), $wgContentNamespaces) ) {
 31+ self::pushEvent($article->getTitle()->getText(), $article->getTitle()->getFullURL(), "Read more");
 32+ }
 33+ wfProfileOut(__METHOD__);
 34+ return true;
 35+ }
 36+}
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnAddImage.php
@@ -0,0 +1,22 @@
 2+<?php
 3+/**
 4+ * @author Sean Colombo
 5+ *
 6+ * Pushes an item to Facebook News Feed when the user adds an Image to the site.
 7+ */
 8+
 9+global $wgExtensionMessagesFiles;
 10+$pushDir = dirname(__FILE__) . '/';
 11+$wgExtensionMessagesFiles['FBPush_OnAddImage'] = $pushDir . "FBPush_OnAddImage.i18n.php";
 12+
 13+class FBPush_OnAddImage extends FBConnectPushEvent {
 14+ protected $isAllowedUserPreferenceName = 'fbconnect-push-allow-OnAddImage'; // must correspond to an i18n message that is 'tog-[the value of the string on this line]'.
 15+
 16+ public function init(){
 17+ wfProfileIn(__METHOD__);
 18+
 19+ wfLoadExtensionMessages('FBPush_OnAddImage');
 20+
 21+ wfProfileOut(__METHOD__);
 22+ }
 23+}
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnLargeEdit.i18n.php
@@ -0,0 +1,5 @@
 2+<?php
 3+
 4+$messages['en'] = array(
 5+ 'tog-fbconnect-push-allow-OnLargeEdit' => "Post to my Facebook News Feed when I make an edit over ".FBPush_OnLargeEdit::getMinCharsToPush()." characters.",
 6+);
Index: trunk/extensions/FBConnect/pushEvents/FBPush_OnLargeEdit.php
@@ -0,0 +1,27 @@
 2+<?php
 3+/**
 4+ * @author Sean Colombo
 5+ *
 6+ * Pushes an item to Facebook News Feed in the event of a large edit.
 7+ */
 8+
 9+global $wgExtensionMessagesFiles;
 10+$pushDir = dirname(__FILE__) . '/';
 11+$wgExtensionMessagesFiles['FBPush_OnLargeEdit'] = $pushDir . "FBPush_OnLargeEdit.i18n.php";
 12+
 13+class FBPush_OnLargeEdit extends FBConnectPushEvent {
 14+ protected $isAllowedUserPreferenceName = 'fbconnect-push-allow-OnLargeEdit'; // must correspond to an i18n message that is 'tog-[the value of the string on this line]'.
 15+ static private $MIN_CHARS_TO_PUSH = 300; // number of chars that need to be changed
 16+
 17+ static public function getMinCharsToPush(){
 18+ return self::$MIN_CHARS_TO_PUSH;
 19+ }
 20+
 21+ public function init(){
 22+ wfProfileIn(__METHOD__);
 23+
 24+ wfLoadExtensionMessages('FBPush_OnLargeEdit');
 25+
 26+ wfProfileOut(__METHOD__);
 27+ }
 28+}
Index: trunk/extensions/FBConnect/FBConnect.i18n.php
@@ -0,0 +1,176 @@
 2+<?php
 3+/*
 4+ * Copyright � 2008-2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 5+ * This program is free software; you can redistribute it and/or modify
 6+ * it under the terms of the GNU General Public License as published by
 7+ * the Free Software Foundation; either version 2 of the License, or
 8+ * (at your option) any later version.
 9+ *
 10+ * This program is distributed in the hope that it will be useful,
 11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 13+ * GNU General Public License for more details.
 14+ *
 15+ * You should have received a copy of the GNU General Public License along
 16+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 17+ */
 18+
 19+
 20+/*
 21+ * Not a valid entry point, skip unless MEDIAWIKI is defined.
 22+ */
 23+if ( !defined( 'MEDIAWIKI' ) ) {
 24+ die( 'This file is a MediaWiki extension, it is not a valid entry point' );
 25+}
 26+
 27+
 28+/**
 29+ * FBConnect.i18n.php
 30+ *
 31+ * Internationalization file for FBConnect.
 32+ */
 33+
 34+
 35+$messages = array();
 36+
 37+// Shorthand to make my life more sane
 38+if (!defined( 'fb' )) {
 39+ define( 'fb', 'fbconnect-' );
 40+}
 41+
 42+/** English */
 43+$messages['en'] = array(
 44+// Extension name
 45+'fbconnect' => 'Facebook Connect',
 46+fb.'desc' => 'Enables users to [[Special:Connect|Connect]] with their [http://www.facebook.com Facebook] ' .
 47+ 'accounts. Offers authentification based on Facebook groups and the use of FBML in wiki text.',
 48+// Group containing Facebook Connect users
 49+'group-fb-user' => 'Facebook Connect users',
 50+'group-fb-user-member' => 'Facebook Connect user',
 51+'grouppage-fb-user' => '{{ns:project}}:Facebook Connect users',
 52+// Group for Facebook Connect users beloning to the group specified by $fbUserRightsFromGroup
 53+'group-fb-groupie' => 'Group members',
 54+'group-fb-groupie-member' => 'Group member',
 55+'grouppage-fb-groupie' => '{{ns:project}}:Group members',
 56+// Officers of the Facebook group
 57+'group-fb-officer' => 'Group officers',
 58+'group-fb-officer-member' => 'Group officer',
 59+'grouppage-fb-officer' => '{{ns:project}}:Group officers',
 60+// Admins of the Facebook group
 61+'group-fb-admin' => 'Group admins',
 62+'group-fb-admin-member' => 'Group administrator',
 63+'grouppage-fb-admin' => '{{ns:project}}:Group admins',
 64+// Personal toolbar
 65+fb.'connect' => 'Log in with Facebook Connect',
 66+fb.'convert' => 'Connect this account with Facebook',
 67+fb.'logout' => 'Logout of Facebook',
 68+fb.'link' => 'Back to facebook.com',
 69+
 70+// Special:Connect
 71+fb.'title' => 'Connect account with Facebook',
 72+fb.'intro' => 'This wiki is enabled with Facebook Connect, the next evolution of Facebook Platform. This means ' .
 73+ 'that when you are Connected, in addition to the normal [[Wikipedia:Help:Logging in#Why log in?' .
 74+ '|benefits]] you see when logging in, you will be able to take advantage of some extra features...',
 75+fb.'click-to-login' => 'Click this button to login to this site via facebook',
 76+fb.'click-to-connect-existing' => 'Click this button to connect your facebook account to $1',
 77+fb.'conv' => 'Convenience',
 78+fb.'convdesc' => 'Connected users are automatically logged you in. If permission is given, then this wiki can even ' .
 79+ 'use Facebook as an email proxy so you can continue to receive important notifications without ' .
 80+ 'revealing your email address.',
 81+fb.'fbml' => 'Facebook Markup Language',
 82+fb.'fbmldesc' => 'Facebook has provided a bunch of built-in tags that will render dynamic data. Many of these tags ' .
 83+ 'can be included in wiki text, and will be rendered differently depending on which Connected user ' .
 84+ 'they are being viewed by.',
 85+fb.'comm' => 'Communication',
 86+fb.'commdesc' => 'Facebook Connect ushers in a whole new level of networking. See which of your friends are using ' .
 87+ 'the wiki, and optionally share your actions with your friends through the Facebook News Feed.',
 88+fb.'welcome' => 'Welcome, Facebook Connect user!',
 89+fb.'loginbox' => "Or '''login''' with Facebook:\n\n$1",
 90+fb.'merge' => 'Merge your wiki account with your Facebook ID',
 91+fb.'mergebox' => 'This feature has not yet been implemented. Accounts can be merged manually with [[Special:' .
 92+ 'Renameuser]] if it is installed. For more information, please visit [[MediaWikiWiki:Extension:' .
 93+ "Renameuser|Extension:Renameuser]].\n\n\n$1\n\n\nNote: This can be undone by a sysop.",
 94+fb.'logoutbox'=> "$1\n\nThis will also log you out of Facebook and all Connected sites, including this wiki.",
 95+fb.'listusers-header'
 96+ => '$1 and $2 privileges are automatically transfered from the Officer and Admin titles of the ' .
 97+ "Facebook group $3.\n\nFor more info, please contact the group creator $4.",
 98+// Prefix to use for automatically-generated usernames
 99+fb.'usernameprefix' => 'FacebookUser',
 100+// Special:Connect
 101+fb.'error' => 'Verification error',
 102+fb.'errortext' => 'An error occured during verification with Facebook Connect.',
 103+fb.'cancel' => 'Action cancelled',
 104+fb.'canceltext' => 'The previous action was cancelled by the user.',
 105+fb.'invalid' => 'Invalid option',
 106+fb.'invalidtext' => 'The selection made on the previous page was invalid.',
 107+fb.'success' => 'Facebook verification succeeded',
 108+fb.'successtext' => 'You have been successfully logged in with Facebook Connect.',
 109+#fb.'optional' => 'Optional',
 110+#fb.'required' => 'Required',
 111+fb.'nickname' => 'Nickname',
 112+fb.'fullname' => 'Fullname',
 113+fb.'email' => 'E-mail address',
 114+fb.'language' => 'Language',
 115+fb.'timecorrection' => 'Time zone correction (hours)',
 116+fb.'chooselegend' => 'Username choice',
 117+fb.'chooseinstructions' => 'All users need a nickname; you can choose one from the options below.',
 118+fb.'invalidname' => 'The nickname you chose is already taken or not a valid nickname. Please chose a different one.',
 119+fb.'choosenick' => 'Your Facebook profile name ($1)',
 120+fb.'choosefirst' => 'Your first name ($1)',
 121+fb.'choosefull' => 'Your full name ($1)',
 122+fb.'chooseauto' => 'An auto-generated name ($1)',
 123+fb.'choosemanual' => 'A name of your choice:',
 124+fb.'chooseexisting' => 'An existing account on this wiki',
 125+fb.'chooseusername' => 'Username:',
 126+fb.'choosepassword' => 'Password:',
 127+fb.'updateuserinfo' => 'Update the following personal information:',
 128+fb.'alreadyloggedin' => "'''You are already logged in, $1!'''\n\nIf you want to use Facebook " .
 129+ 'Connect to log in in the future, you can [[Special:Connect/Convert|' .
 130+ 'convert your account to use Facebook Connect]].',
 131+/*
 132+fb.'convertinstructions' => 'This form lets you change your user account to use an OpenID URL or add more OpenID URLs',
 133+fb.'convertoraddmoreids' => 'Convert to OpenID or add another OpenID URL',
 134+fb.'convertsuccess' => 'Successfully converted to OpenID',
 135+fb.'convertsuccesstext' => 'You have successfully converted your OpenID to $1.',
 136+fb.'convertyourstext' => 'That is already your OpenID.',
 137+fb.'convertothertext' => 'That is someone else\'s OpenID.',
 138+*/
 139+
 140+fb.'error-creating-user' => "Error creating the user in the local database.",
 141+fb.'error-user-creation-hook-aborted' => "A hook (extension) aborted the account creation with the message: $1",
 142+
 143+
 144+ 'fbconnect-prefstext' => 'Facebook Connect',
 145+ 'fbconnect-link-to-profile' => 'Facebook profile',
 146+ 'fbconnect-prefsheader' => "To control which events will push an item to your Facebook News Feed,
 147+ <a id='fbConnectPushEventBar_show' href='#'>show preferences</a>
 148+ <a id='fbConnectPushEventBar_hide' href='#' style='display:none'>hide preferences</a>",
 149+ 'fbconnect-prefs-can-be-updated' => "You can update these any time by visiting the '$1' tab of your Preferences page.",
 150+);
 151+
 152+/**
 153+ * Message documentation (Message documentation)
 154+ * What is this used for? Answer: This is shown to translators to help them know what the string is for.
 155+ * @author Garrett Brown
 156+ */
 157+$messages['qqq'] = array(
 158+ 'fbconnect-desc' => 'Short description of the FBConnect extension, shown in [[Special:Version]]. Do not translate or change links.',
 159+ 'fbconnect-trustinstructions' => '* $1 is a trust root. A trust root looks much like a normal URL, but is used to describe a set of URLs. Trust roots are used by OpenID to verify that a user has approved the OpenID enabled website.',
 160+ 'fbconnect-optional' => '{{Identical|Optional}}',
 161+ 'fbconnect-email' => '{{Identical|E-mail address}}',
 162+ 'fbconnect-language' => '{{Identical|Language}}',
 163+ 'fbconnect-timezone' => '{{Identical|Time zone}}',
 164+ 'fbconnect-choosepassword' => '{{Identical|Password}}',
 165+ 'fbconnect-alreadyloggedin' => '$1 is a user name.',
 166+ 'fbconnect-autosubmit' => '{{doc-important|"Continue" will never be localised. It is hardcoded in a PHP extension. Translations could be made like ""Continue" (translation)"}}',
 167+ 'fbconnect-delete-button' => '{{Identical|Confirm}}',
 168+ 'prefs-fbconnect' => '{{optional}}
 169+OpenID preferences tab title',
 170+ 'fbconnect-prefstext' => 'FBConnect preferences tab text above the list of preferences',
 171+ 'fbconnect-pref-hide' => 'FBConnect preference label (Hide your FBConnect URL on your user page, if you log in with FBConnect)',
 172+ 'fbconnect-pref-update-userinfo-on-login' => 'FBConnect preference label for updating from Facebook account upon login',
 173+ 'fbconnect-urls-action' => '{{Identical|Action}}',
 174+ 'fbconnect-urls-delete' => '{{identical|Delete}}',
 175+ 'fbconnect-link-to-profile' => 'Appears next to the user\s name in their Preferences page and this text is made into link to the profile of that user if they are connected.',
 176+);
 177+/**/
Index: trunk/extensions/FBConnect/php-sdk/facebook.php
@@ -0,0 +1,775 @@
 2+<?php
 3+
 4+if (!function_exists('curl_init')) {
 5+ throw new Exception('Facebook needs the CURL PHP extension.');
 6+}
 7+if (!function_exists('json_decode')) {
 8+ throw new Exception('Facebook needs the JSON PHP extension.');
 9+}
 10+
 11+/**
 12+ * Thrown when an API call returns an exception.
 13+ *
 14+ * @author Naitik Shah <naitik@facebook.com>
 15+ */
 16+class FacebookApiException extends Exception
 17+{
 18+ /**
 19+ * The result from the API server that represents the exception information.
 20+ */
 21+ protected $result;
 22+
 23+ /**
 24+ * Make a new API Exception with the given result.
 25+ *
 26+ * @param Array $result the result from the API server
 27+ */
 28+ public function __construct($result) {
 29+ $this->result = $result;
 30+
 31+ $code = isset($result['error_code']) ? $result['error_code'] : 0;
 32+ $msg = isset($result['error'])
 33+ ? $result['error']['message'] : $result['error_msg'];
 34+ parent::__construct($msg, $code);
 35+ }
 36+
 37+ /**
 38+ * Return the associated result object returned by the API server.
 39+ *
 40+ * @returns Array the result from the API server
 41+ */
 42+ public function getResult() {
 43+ return $this->result;
 44+ }
 45+
 46+ /**
 47+ * Returns the associated type for the error. This will default to
 48+ * 'Exception' when a type is not available.
 49+ *
 50+ * @return String
 51+ */
 52+ public function getType() {
 53+ return
 54+ isset($this->result['error']) && isset($this->result['error']['type'])
 55+ ? $this->result['error']['type']
 56+ : 'Exception';
 57+ }
 58+
 59+ /**
 60+ * To make debugging easier.
 61+ *
 62+ * @returns String the string representation of the error
 63+ */
 64+ public function __toString() {
 65+ $str = $this->getType() . ': ';
 66+ if ($this->code != 0) {
 67+ $str .= $this->code . ': ';
 68+ }
 69+ return $str . $this->message;
 70+ }
 71+}
 72+
 73+/**
 74+ * Provides access to the Facebook Platform.
 75+ *
 76+ * @author Naitik Shah <naitik@facebook.com>
 77+ */
 78+class Facebook
 79+{
 80+ /**
 81+ * Version.
 82+ */
 83+ const VERSION = '2.0.3';
 84+
 85+ /**
 86+ * Default options for curl.
 87+ */
 88+ public static $CURL_OPTS = array(
 89+ CURLOPT_CONNECTTIMEOUT => 10,
 90+ CURLOPT_RETURNTRANSFER => true,
 91+ CURLOPT_TIMEOUT => 60,
 92+ CURLOPT_USERAGENT => 'facebook-php-2.0',
 93+ );
 94+
 95+ /**
 96+ * List of query parameters that get automatically dropped when rebuilding
 97+ * the current URL.
 98+ */
 99+ protected static $DROP_QUERY_PARAMS = array(
 100+ 'session',
 101+ );
 102+
 103+ /**
 104+ * Maps aliases to Facebook domains.
 105+ */
 106+ public static $DOMAIN_MAP = array(
 107+ 'api' => 'https://api.facebook.com/',
 108+ 'api_read' => 'https://api-read.facebook.com/',
 109+ 'graph' => 'https://graph.facebook.com/',
 110+ 'www' => 'https://www.facebook.com/',
 111+ );
 112+
 113+ /**
 114+ * The Application ID.
 115+ */
 116+ protected $appId;
 117+
 118+ /**
 119+ * The Application API Secret.
 120+ */
 121+ protected $apiSecret;
 122+
 123+ /**
 124+ * The active user session, if one is available.
 125+ */
 126+ protected $session;
 127+
 128+ /**
 129+ * Indicates that we already loaded the session as best as we could.
 130+ */
 131+ protected $sessionLoaded = false;
 132+
 133+ /**
 134+ * Indicates if Cookie support should be enabled.
 135+ */
 136+ protected $cookieSupport = false;
 137+
 138+ /**
 139+ * Base domain for the Cookie.
 140+ */
 141+ protected $baseDomain = '';
 142+
 143+ /**
 144+ * Initialize a Facebook Application.
 145+ *
 146+ * The configuration:
 147+ * - appId: the application ID
 148+ * - secret: the application secret
 149+ * - cookie: (optional) boolean true to enable cookie support
 150+ * - domain: (optional) domain for the cookie
 151+ *
 152+ * @param Array $config the application configuration
 153+ */
 154+ public function __construct($config) {
 155+ $this->setAppId($config['appId']);
 156+ $this->setApiSecret($config['secret']);
 157+ if (isset($config['cookie'])) {
 158+ $this->setCookieSupport($config['cookie']);
 159+ }
 160+ if (isset($config['domain'])) {
 161+ $this->setBaseDomain($config['domain']);
 162+ }
 163+ }
 164+
 165+ /**
 166+ * Set the Application ID.
 167+ *
 168+ * @param String $appId the Application ID
 169+ */
 170+ public function setAppId($appId) {
 171+ $this->appId = $appId;
 172+ return $this;
 173+ }
 174+
 175+ /**
 176+ * Get the Application ID.
 177+ *
 178+ * @return String the Application ID
 179+ */
 180+ public function getAppId() {
 181+ return $this->appId;
 182+ }
 183+
 184+ /**
 185+ * Set the API Secret.
 186+ *
 187+ * @param String $appId the API Secret
 188+ */
 189+ public function setApiSecret($apiSecret) {
 190+ $this->apiSecret = $apiSecret;
 191+ return $this;
 192+ }
 193+
 194+ /**
 195+ * Get the API Secret.
 196+ *
 197+ * @return String the API Secret
 198+ */
 199+ public function getApiSecret() {
 200+ return $this->apiSecret;
 201+ }
 202+
 203+ /**
 204+ * Set the Cookie Support status.
 205+ *
 206+ * @param Boolean $cookieSupport the Cookie Support status
 207+ */
 208+ public function setCookieSupport($cookieSupport) {
 209+ $this->cookieSupport = $cookieSupport;
 210+ return $this;
 211+ }
 212+
 213+ /**
 214+ * Get the Cookie Support status.
 215+ *
 216+ * @return Boolean the Cookie Support status
 217+ */
 218+ public function useCookieSupport() {
 219+ return $this->cookieSupport;
 220+ }
 221+
 222+ /**
 223+ * Set the base domain for the Cookie.
 224+ *
 225+ * @param String $domain the base domain
 226+ */
 227+ public function setBaseDomain($domain) {
 228+ $this->baseDomain = $domain;
 229+ return $this;
 230+ }
 231+
 232+ /**
 233+ * Get the base domain for the Cookie.
 234+ *
 235+ * @return String the base domain
 236+ */
 237+ public function getBaseDomain() {
 238+ return $this->baseDomain;
 239+ }
 240+
 241+ /**
 242+ * Set the Session.
 243+ *
 244+ * @param Array $session the session
 245+ * @param Boolean $write_cookie indicate if a cookie should be written. this
 246+ * value is ignored if cookie support has been disabled.
 247+ */
 248+ public function setSession($session=null, $write_cookie=true) {
 249+ $session = $this->validateSessionObject($session);
 250+ $this->sessionLoaded = true;
 251+ $this->session = $session;
 252+ if ($write_cookie) {
 253+ $this->setCookieFromSession($session);
 254+ }
 255+ return $this;
 256+ }
 257+
 258+ /**
 259+ * Get the session object. This will automatically look for a signed session
 260+ * sent via the Cookie or Query Parameters if needed.
 261+ *
 262+ * @return Array the session
 263+ */
 264+ public function getSession() {
 265+ if (!$this->sessionLoaded) {
 266+ $session = null;
 267+ $write_cookie = true;
 268+
 269+ // try loading session from $_REQUEST
 270+ if (isset($_REQUEST['session'])) {
 271+ $session = json_decode(
 272+ get_magic_quotes_gpc()
 273+ ? stripslashes($_REQUEST['session'])
 274+ : $_REQUEST['session'],
 275+ true
 276+ );
 277+ $session = $this->validateSessionObject($session);
 278+ }
 279+
 280+ // try loading session from cookie if necessary
 281+ if (!$session && $this->useCookieSupport()) {
 282+ $cookieName = $this->getSessionCookieName();
 283+ if (isset($_COOKIE[$cookieName])) {
 284+ $session = array();
 285+ parse_str(trim(
 286+ get_magic_quotes_gpc()
 287+ ? stripslashes($_COOKIE[$cookieName])
 288+ : $_COOKIE[$cookieName],
 289+ '"'
 290+ ), $session);
 291+ $session = $this->validateSessionObject($session);
 292+ // write only if we need to delete a invalid session cookie
 293+ $write_cookie = empty($session);
 294+ }
 295+ }
 296+
 297+ $this->setSession($session, $write_cookie);
 298+ }
 299+
 300+ return $this->session;
 301+ }
 302+
 303+ /**
 304+ * Get the UID from the session.
 305+ *
 306+ * @return String the UID if available
 307+ */
 308+ public function getUser() {
 309+ $session = $this->getSession();
 310+ return $session ? $session['uid'] : null;
 311+ }
 312+
 313+ /**
 314+ * Get a Login URL for use with redirects. By default, full page redirect is
 315+ * assumed. If you are using the generated URL with a window.open() call in
 316+ * JavaScript, you can pass in display=popup as part of the $params.
 317+ *
 318+ * The parameters:
 319+ * - next: the url to go to after a successful login
 320+ * - cancel_url: the url to go to after the user cancels
 321+ * - req_perms: comma separated list of requested extended perms
 322+ * - display: can be "page" (default, full page) or "popup"
 323+ *
 324+ * @param Array $params provide custom parameters
 325+ * @return String the URL for the login flow
 326+ */
 327+ public function getLoginUrl($params=array()) {
 328+ $currentUrl = $this->getCurrentUrl();
 329+ return $this->getUrl(
 330+ 'www',
 331+ 'login.php',
 332+ array_merge(array(
 333+ 'api_key' => $this->getAppId(),
 334+ 'cancel_url' => $currentUrl,
 335+ 'display' => 'page',
 336+ 'fbconnect' => 1,
 337+ 'next' => $currentUrl,
 338+ 'return_session' => 1,
 339+ 'session_version' => 3,
 340+ 'v' => '1.0',
 341+ ), $params)
 342+ );
 343+ }
 344+
 345+ /**
 346+ * Get a Logout URL suitable for use with redirects.
 347+ *
 348+ * The parameters:
 349+ * - next: the url to go to after a successful logout
 350+ *
 351+ * @param Array $params provide custom parameters
 352+ * @return String the URL for the logout flow
 353+ */
 354+ public function getLogoutUrl($params=array()) {
 355+ $session = $this->getSession();
 356+ return $this->getUrl(
 357+ 'www',
 358+ 'logout.php',
 359+ array_merge(array(
 360+ 'api_key' => $this->getAppId(),
 361+ 'next' => $this->getCurrentUrl(),
 362+ 'session_key' => $session['session_key'],
 363+ ), $params)
 364+ );
 365+ }
 366+
 367+ /**
 368+ * Get a login status URL to fetch the status from facebook.
 369+ *
 370+ * The parameters:
 371+ * - ok_session: the URL to go to if a session is found
 372+ * - no_session: the URL to go to if the user is not connected
 373+ * - no_user: the URL to go to if the user is not signed into facebook
 374+ *
 375+ * @param Array $params provide custom parameters
 376+ * @return String the URL for the logout flow
 377+ */
 378+ public function getLoginStatusUrl($params=array()) {
 379+ return $this->getUrl(
 380+ 'www',
 381+ 'extern/login_status.php',
 382+ array_merge(array(
 383+ 'api_key' => $this->getAppId(),
 384+ 'no_session' => $this->getCurrentUrl(),
 385+ 'no_user' => $this->getCurrentUrl(),
 386+ 'ok_session' => $this->getCurrentUrl(),
 387+ 'session_version' => 3,
 388+ ), $params)
 389+ );
 390+ }
 391+
 392+ /**
 393+ * Make an API call.
 394+ *
 395+ * @param Array $params the API call parameters
 396+ * @return the decoded response
 397+ */
 398+ public function api(/* polymorphic */) {
 399+ $args = func_get_args();
 400+ if (is_array($args[0])) {
 401+ return $this->_restserver($args[0]);
 402+ } else {
 403+ return call_user_func_array(array($this, '_graph'), $args);
 404+ }
 405+ }
 406+
 407+ /**
 408+ * Invoke the old restserver.php endpoint.
 409+ *
 410+ * @param Array $params method call object
 411+ * @return the decoded response object
 412+ * @throws FacebookApiException
 413+ */
 414+ protected function _restserver($params) {
 415+ // generic application level parameters
 416+ $params['api_key'] = $this->getAppId();
 417+ $params['format'] = 'json-strings';
 418+
 419+ $result = json_decode($this->_oauthRequest(
 420+ $this->getApiUrl($params['method']),
 421+ $params
 422+ ), true);
 423+
 424+ // results are returned, errors are thrown
 425+ if (is_array($result) && isset($result['error_code'])) {
 426+ throw new FacebookApiException($result);
 427+ }
 428+ return $result;
 429+ }
 430+
 431+ /**
 432+ * Invoke the Graph API.
 433+ *
 434+ * @param String $path the path (required)
 435+ * @param String $method the http method (default 'GET')
 436+ * @param Array $params the query/post data
 437+ * @return the decoded response object
 438+ * @throws FacebookApiException
 439+ */
 440+ protected function _graph($path, $method='GET', $params=array()) {
 441+ if (is_array($method) && empty($params)) {
 442+ $params = $method;
 443+ $method = 'GET';
 444+ }
 445+ $params['method'] = $method; // method override as we always do a POST
 446+
 447+ $result = json_decode($this->_oauthRequest(
 448+ $this->getUrl('graph', $path),
 449+ $params
 450+ ), true);
 451+
 452+ // results are returned, errors are thrown
 453+ if (is_array($result) && isset($result['error'])) {
 454+ $e = new FacebookApiException($result);
 455+ if ($e->getType() === 'OAuthException') {
 456+ $this->setSession(null);
 457+ }
 458+ throw $e;
 459+ }
 460+ return $result;
 461+ }
 462+
 463+ /**
 464+ * Make a OAuth Request
 465+ *
 466+ * @param String $path the path (required)
 467+ * @param Array $params the query/post data
 468+ * @return the decoded response object
 469+ * @throws FacebookApiException
 470+ */
 471+ protected function _oauthRequest($url, $params) {
 472+ if (!isset($params['access_token'])) {
 473+ $session = $this->getSession();
 474+ // either user session signed, or app signed
 475+ if ($session) {
 476+ $params['access_token'] = $session['access_token'];
 477+ } else {
 478+ $params['access_token'] = $this->getAppId() .'|'. $this->getApiSecret();
 479+ }
 480+ }
 481+
 482+ // json_encode all params values that are not strings
 483+ foreach ($params as $key => $value) {
 484+ if (!is_string($value)) {
 485+ $params[$key] = json_encode($value);
 486+ }
 487+ }
 488+ return $this->makeRequest($url, $params);
 489+ }
 490+
 491+ /**
 492+ * Makes an HTTP request. This method can be overriden by subclasses if
 493+ * developers want to do fancier things or use something other than curl to
 494+ * make the request.
 495+ *
 496+ * @param String $url the URL to make the request to
 497+ * @param Array $params the parameters to use for the POST body
 498+ * @param CurlHandler $ch optional initialized curl handle
 499+ * @return String the response text
 500+ */
 501+ protected function makeRequest($url, $params, $ch=null) {
 502+ if (!$ch) {
 503+ $ch = curl_init();
 504+ }
 505+
 506+ $opts = self::$CURL_OPTS;
 507+ $opts[CURLOPT_POSTFIELDS] = $params;
 508+ $opts[CURLOPT_URL] = $url;
 509+ curl_setopt_array($ch, $opts);
 510+ $result = curl_exec($ch);
 511+ if ($result === false) {
 512+ $e = new FacebookApiException(array(
 513+ 'error_code' => curl_errno($ch),
 514+ 'error' => array(
 515+ 'message' => curl_error($ch),
 516+ 'type' => 'CurlException',
 517+ ),
 518+ ));
 519+ curl_close($ch);
 520+ throw $e;
 521+ }
 522+ curl_close($ch);
 523+ return $result;
 524+ }
 525+
 526+ /**
 527+ * The name of the Cookie that contains the session.
 528+ *
 529+ * @return String the cookie name
 530+ */
 531+ protected function getSessionCookieName() {
 532+ return 'fbs_' . $this->getAppId();
 533+ }
 534+
 535+ /**
 536+ * Set a JS Cookie based on the _passed in_ session. It does not use the
 537+ * currently stored session -- you need to explicitly pass it in.
 538+ *
 539+ * @param Array $session the session to use for setting the cookie
 540+ */
 541+ protected function setCookieFromSession($session=null) {
 542+ if (!$this->useCookieSupport()) {
 543+ return;
 544+ }
 545+
 546+ $cookieName = $this->getSessionCookieName();
 547+ $value = 'deleted';
 548+ $expires = time() - 3600;
 549+ $domain = $this->getBaseDomain();
 550+ if ($session) {
 551+ $value = '"' . http_build_query($session, null, '&') . '"';
 552+ if (isset($session['base_domain'])) {
 553+ $domain = $session['base_domain'];
 554+ }
 555+ $expires = $session['expires'];
 556+ }
 557+
 558+ // prepend dot if a domain is found
 559+ if ($domain) {
 560+ $domain = '.' . $domain;
 561+ }
 562+
 563+ // if an existing cookie is not set, we dont need to delete it
 564+ if ($value == 'deleted' && empty($_COOKIE[$cookieName])) {
 565+ return;
 566+ }
 567+
 568+ if (headers_sent()) {
 569+ // disable error log if we are running in a CLI environment
 570+ // @codeCoverageIgnoreStart
 571+ if (php_sapi_name() != 'cli') {
 572+ error_log('Could not set cookie. Headers already sent.');
 573+ }
 574+ // @codeCoverageIgnoreEnd
 575+
 576+ // ignore for code coverage as we will never be able to setcookie in a CLI
 577+ // environment
 578+ // @codeCoverageIgnoreStart
 579+ } else {
 580+ setcookie($cookieName, $value, $expires, '/', $domain);
 581+ }
 582+ // @codeCoverageIgnoreEnd
 583+ }
 584+
 585+ /**
 586+ * Validates a session_version=3 style session object.
 587+ *
 588+ * @param Array $session the session object
 589+ * @return Array the session object if it validates, null otherwise
 590+ */
 591+ protected function validateSessionObject($session) {
 592+ // make sure some essential fields exist
 593+ if (is_array($session) &&
 594+ isset($session['uid']) &&
 595+ isset($session['session_key']) &&
 596+ isset($session['secret']) &&
 597+ isset($session['access_token']) &&
 598+ isset($session['sig'])) {
 599+ // validate the signature
 600+ $session_without_sig = $session;
 601+ unset($session_without_sig['sig']);
 602+ $expected_sig = self::generateSignature(
 603+ $session_without_sig,
 604+ $this->getApiSecret()
 605+ );
 606+ if ($session['sig'] != $expected_sig) {
 607+ // disable error log if we are running in a CLI environment
 608+ // @codeCoverageIgnoreStart
 609+ if (php_sapi_name() != 'cli') {
 610+ error_log('Got invalid session signature in cookie.');
 611+ }
 612+ // @codeCoverageIgnoreEnd
 613+ $session = null;
 614+ }
 615+ // check expiry time
 616+ } else {
 617+ $session = null;
 618+ }
 619+ return $session;
 620+ }
 621+
 622+ /**
 623+ * Build the URL for api given parameters.
 624+ *
 625+ * @param $method String the method name.
 626+ * @return String the URL for the given parameters
 627+ */
 628+ protected function getApiUrl($method) {
 629+ static $READ_ONLY_CALLS =
 630+ array('admin.getallocation' => 1,
 631+ 'admin.getappproperties' => 1,
 632+ 'admin.getbannedusers' => 1,
 633+ 'admin.getlivestreamvialink' => 1,
 634+ 'admin.getmetrics' => 1,
 635+ 'admin.getrestrictioninfo' => 1,
 636+ 'application.getpublicinfo' => 1,
 637+ 'auth.getapppublickey' => 1,
 638+ 'auth.getsession' => 1,
 639+ 'auth.getsignedpublicsessiondata' => 1,
 640+ 'comments.get' => 1,
 641+ 'connect.getunconnectedfriendscount' => 1,
 642+ 'dashboard.getactivity' => 1,
 643+ 'dashboard.getcount' => 1,
 644+ 'dashboard.getglobalnews' => 1,
 645+ 'dashboard.getnews' => 1,
 646+ 'dashboard.multigetcount' => 1,
 647+ 'dashboard.multigetnews' => 1,
 648+ 'data.getcookies' => 1,
 649+ 'events.get' => 1,
 650+ 'events.getmembers' => 1,
 651+ 'fbml.getcustomtags' => 1,
 652+ 'feed.getappfriendstories' => 1,
 653+ 'feed.getregisteredtemplatebundlebyid' => 1,
 654+ 'feed.getregisteredtemplatebundles' => 1,
 655+ 'fql.multiquery' => 1,
 656+ 'fql.query' => 1,
 657+ 'friends.arefriends' => 1,
 658+ 'friends.get' => 1,
 659+ 'friends.getappusers' => 1,
 660+ 'friends.getlists' => 1,
 661+ 'friends.getmutualfriends' => 1,
 662+ 'gifts.get' => 1,
 663+ 'groups.get' => 1,
 664+ 'groups.getmembers' => 1,
 665+ 'intl.gettranslations' => 1,
 666+ 'links.get' => 1,
 667+ 'notes.get' => 1,
 668+ 'notifications.get' => 1,
 669+ 'pages.getinfo' => 1,
 670+ 'pages.isadmin' => 1,
 671+ 'pages.isappadded' => 1,
 672+ 'pages.isfan' => 1,
 673+ 'permissions.checkavailableapiaccess' => 1,
 674+ 'permissions.checkgrantedapiaccess' => 1,
 675+ 'photos.get' => 1,
 676+ 'photos.getalbums' => 1,
 677+ 'photos.gettags' => 1,
 678+ 'profile.getinfo' => 1,
 679+ 'profile.getinfooptions' => 1,
 680+ 'stream.get' => 1,
 681+ 'stream.getcomments' => 1,
 682+ 'stream.getfilters' => 1,
 683+ 'users.getinfo' => 1,
 684+ 'users.getloggedinuser' => 1,
 685+ 'users.getstandardinfo' => 1,
 686+ 'users.hasapppermission' => 1,
 687+ 'users.isappuser' => 1,
 688+ 'users.isverified' => 1,
 689+ 'video.getuploadlimits' => 1);
 690+ $name = 'api';
 691+ if (isset($READ_ONLY_CALLS[strtolower($method)])) {
 692+ $name = 'api_read';
 693+ }
 694+ return self::getUrl($name, 'restserver.php');
 695+ }
 696+
 697+ /**
 698+ * Build the URL for given domain alias, path and parameters.
 699+ *
 700+ * @param $name String the name of the domain
 701+ * @param $path String optional path (without a leading slash)
 702+ * @param $params Array optional query parameters
 703+ * @return String the URL for the given parameters
 704+ */
 705+ protected function getUrl($name, $path='', $params=array()) {
 706+ $url = self::$DOMAIN_MAP[$name];
 707+ if ($path) {
 708+ if ($path[0] === '/') {
 709+ $path = substr($path, 1);
 710+ }
 711+ $url .= $path;
 712+ }
 713+ if ($params) {
 714+ $url .= '?' . http_build_query($params);
 715+ }
 716+ return $url;
 717+ }
 718+
 719+ /**
 720+ * Returns the Current URL, stripping it of known FB parameters that should
 721+ * not persist.
 722+ *
 723+ * @return String the current URL
 724+ */
 725+ protected function getCurrentUrl() {
 726+ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
 727+ ? 'https://'
 728+ : 'http://';
 729+ $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
 730+ $parts = parse_url($currentUrl);
 731+
 732+ // drop known fb params
 733+ $query = '';
 734+ if (!empty($parts['query'])) {
 735+ $params = array();
 736+ parse_str($parts['query'], $params);
 737+ foreach(self::$DROP_QUERY_PARAMS as $key) {
 738+ unset($params[$key]);
 739+ }
 740+ if (!empty($params)) {
 741+ $query = '?' . http_build_query($params);
 742+ }
 743+ }
 744+
 745+ // use port if non default
 746+ $port =
 747+ isset($parts['port']) &&
 748+ (($protocol === 'http://' && $parts['port'] !== 80) ||
 749+ ($protocol === 'https://' && $parts['port'] !== 443))
 750+ ? ':' . $parts['port'] : '';
 751+
 752+ // rebuild
 753+ return $protocol . $parts['host'] . $port . $parts['path'] . $query;
 754+ }
 755+
 756+ /**
 757+ * Generate a signature for the given params and secret.
 758+ *
 759+ * @param Array $params the parameters to sign
 760+ * @param String $secret the secret to sign with
 761+ * @return String the generated signature
 762+ */
 763+ protected static function generateSignature($params, $secret) {
 764+ // work with sorted data
 765+ ksort($params);
 766+
 767+ // generate the base string
 768+ $base_string = '';
 769+ foreach($params as $key => $value) {
 770+ $base_string .= $key . '=' . $value;
 771+ }
 772+ $base_string .= $secret;
 773+
 774+ return md5($base_string);
 775+ }
 776+}
Index: trunk/extensions/FBConnect/fbconnect_table.sql
@@ -0,0 +1,10 @@
 2+--
 3+-- SQL schema for FBConnect extension
 4+--
 5+
 6+CREATE TABLE /*$wgDBprefix*/user_fbconnect (
 7+ user_fbid BIGINT unsigned NOT NULL PRIMARY KEY,
 8+ user_id int(10) unsigned NOT NULL
 9+) /*$wgDBTableOptions*/;
 10+
 11+CREATE INDEX /*$wgDBprefix*/user_fbconnect_user ON /*$wgDBprefix*/user_fbconnect(user_id);
Index: trunk/extensions/FBConnect/fbconnect_table.pg.sql
@@ -0,0 +1,9 @@
 2+
 3+-- Schema for the FBConnect extension (Postgres version)
 4+
 5+CREATE TABLE user_fbconnect (
 6+ user_fbid BIGINT NOT NULL PRIMARY KEY,
 7+ user_id INTEGER NOT NULL REFERENCES mwuser(user_id)
 8+);
 9+
 10+CREATE INDEX user_fbconnect_user ON user_fbconnect(user_id);
Index: trunk/extensions/FBConnect/config.sample.php
@@ -0,0 +1,198 @@
 2+<?php
 3+/*
 4+ * To install:
 5+ * 1. Copy this file to config.php (remove the .sample part).
 6+ * 2. Follow the instructions below to make the extension work.
 7+ */
 8+
 9+
 10+### FBCONNECT CONFIGURATION VARIABLES ###
 11+
 12+/**
 13+ * To use Facebook Connect you will first need to create a Facebook application:
 14+ * 1. Visit the "Create an Application" setup wizard:
 15+ * http://developers.facebook.com/setup/
 16+ * 2. Enter a descriptive name for your wiki in the Site Name field.
 17+ * This will be seen by users when they sign up for your site.
 18+ * 3. Enter the Site URL and Locale, then click "Create application".
 19+ * 4. Copy the displayed App ID and Secret into this config file.
 20+ *
 21+ * Optionally, you may customize your application:
 22+ * A. Click "developer dashboard" link on the previous screen or visit:
 23+ * http://www.facebook.com/developers/apps.php
 24+ * B. Select your application and click "Edit Settings".
 25+ * C. Upload icon and logo images. The icon appears in Stream stories.
 26+ * D. In the "Connect" section, set your Connect Logo and base domain
 27+ * (which should also be copied into this config file).
 28+ */
 29+$fbAppId = 'YOUR_APP_ID'; # Change this!
 30+$fbSecret = 'YOUR_SECRET'; # Change this!
 31+#$fbDomain = 'BASE_DOMAIN'; # Optional
 32+
 33+/**
 34+ * Disables the creation of new accounts and prevents old accounts from being
 35+ * used to log in if they aren't associated with a Facebook Connect account.
 36+ */
 37+$fbConnectOnly = false;
 38+
 39+/**
 40+ * Allow the use of XFBML in wiki text.
 41+ * For more info, see http://wiki.developers.facebook.com/index.php/XFBML.
 42+ */
 43+$fbUseMarkup = true;
 44+
 45+/**
 46+ * If XFBML is enabled, then <fb:photo> maybe be used as a replacement for
 47+ * $wgAllowExternalImages with the added benefit that all photos are screened
 48+ * against Facebook's Code of Conduct <http://www.facebook.com/codeofconduct.php>
 49+ * and subject to dynamic privacy. To disable just <fb:photo> tags, set this to false.
 50+ *
 51+ * Disabled until the JavaScript SDK supports <fb:photo> tags.
 52+ */
 53+#$fbAllowFacebookImages = true;
 54+
 55+/**
 56+ * For easier wiki rights management, create a group on Facebook and place the
 57+ * group ID here. Three new implicit groups will be created:
 58+ *
 59+ * fb-groupie A member of the specified group
 60+ * fb-officer A group member with an officer title
 61+ * fb-admin An administrator of the Facebook group
 62+ *
 63+ * By default, they map to User, Bureaucrat and Sysop privileges, respectively.
 64+ * Users will automatically be promoted or demoted when their membership, title
 65+ * or admin status is modified from the group page within Facebook.
 66+ */
 67+$fbUserRightsFromGroup = false; # Or a group ID
 68+
 69+// Not used (yet...)
 70+#$fbRestrictToGroup = true;
 71+#$fbRestrictToNotReplied = false;
 72+
 73+/**
 74+ * Options regarding the personal toolbar (in the upper right).
 75+ *
 76+ * == Key == == Effect ==
 77+ * hide_connect_button Hides the "Log in with Facebook Connect" button.
 78+ * hide_convert_button Hides "Connect this account with Facebook" for non-
 79+ * Connected users.
 80+ * hide_logout_of_fb Hides the "logout of facebook" button and leaves only
 81+ * the button to log out of the current MediaWiki.
 82+ * link_back_to_facebook Shows a handy "Back to facebook.com" link for Connected
 83+ * users. This helps enforce the idea that this wiki is
 84+ * "in front" of Facebook.
 85+ * remove_user_talk_link Remove link to user's talk page
 86+ * use_real_name_from_fb Show the real name for all Connected users
 87+ *
 88+ * Additionally, use $wgShowIPinHeader to hide the IP and its talk link.
 89+ * For more information, see <http://www.mediawiki.org/wiki/Manual:$wgShowIPinHeader>.
 90+ */
 91+$fbPersonalUrls = array(
 92+ 'hide_connect_button' => false,
 93+ 'hide_convert_button' => false,
 94+ 'hide_logout_of_fb' => false,
 95+ 'link_back_to_facebook' => true,
 96+ 'remove_user_talk_link' => false,
 97+ 'use_real_name_from_fb' => false,
 98+);
 99+#$wgShowIPinHeader = false;
 100+
 101+/**
 102+ * The Facebook icon. You can copy this image to your server if you want, or
 103+ * set to false to disable.
 104+ */
 105+$fbLogo = 'http://static.ak.fbcdn.net/images/icons/favicon.gif';
 106+
 107+/**
 108+ * URL of the Facebook Connect JavaScript SDK. Because this library is currently
 109+ * a beta release, changes to the APIs may be made on a regular basis. If you
 110+ * use FBConnect on your production website, you may wish to insulate yourself
 111+ * from these changes by downloading and hosting your own copy of the library.
 112+ *
 113+ * For more info, see <http://developers.facebook.com/docs/reference/javascript/>
 114+ */
 115+$fbScript = 'http://connect.facebook.net/en_US/all.js';
 116+#$fbScript = 'https://connect.facebook.net/en_US/all.js';
 117+
 118+/**
 119+ * Path to the extension's client-side JavaScript
 120+ */
 121+global $wgScriptPath;
 122+#$fbExtensionScript = "$wgScriptPath/extensions/FBConnect/fbconnect.js"; // for development
 123+$fbExtensionScript = "$wgScriptPath/extensions/FBConnect/fbconnect.min.js";
 124+
 125+/**
 126+ * Whether to include jQuery. This option is for backwards compatibility and is
 127+ * ignored in version 1.16. Otherwise, if you already have jQuery included on
 128+ * your site, you can safely set this to false.
 129+ */
 130+$fbIncludeJquery = true;
 131+
 132+/**
 133+ * Optionally override the default javascript handling which occurs when a user logs in.
 134+ *
 135+ * This will generally not be needed unless you are doing heavy customization of this extension.
 136+ *
 137+ * NOTE: This will be put inside of double-quotes, so any single-quotes should be used inside
 138+ * of any JS in this variable.
 139+ */
 140+//$fbOnLoginJsOverride = "sendToConnectOnLogin();";
 141+
 142+/**
 143+ * Optionally turn off the inclusion of the PreferencesExtension. Since this
 144+ * is an extension that you may already have installed in your instance of
 145+ * MediaWiki, there is the option to turn off FBConnect's inclusion of it (which
 146+ * will require you to already have PreferencesExtension enabled elsewhere).
 147+ *
 148+ * When running on MediaWiki v1.16 and above, the extension won't be included anyway.
 149+ */
 150+$fbIncludePreferencesExtension = true;
 151+
 152+/**
 153+ * An array of extended permissions to request from the user while they are
 154+ * signing up.
 155+ *
 156+ * NOTE: If fbEnablePushToFacebook is true, then publish_stream will automatically be
 157+ * added to this array.
 158+ *
 159+ * For more details see: http://developers.facebook.com/docs/authentication/permissions
 160+ */
 161+$fbExtendedPermissions = array(
 162+ //'publish_stream',
 163+ //'read_stream',
 164+ //'email',
 165+ //'read_mailbox',
 166+ //'offline_access',
 167+ //'create_event',
 168+ //'rsvp_event',
 169+ //'sms',
 170+ //'xmpp_login',
 171+);
 172+
 173+/**
 174+ * PUSH EVENTS
 175+ *
 176+ * This section allows controlling of whether push events are enabled, and which
 177+ * of the push events to use.
 178+ */
 179+$fbEnablePushToFacebook = false;
 180+if(!empty($fbEnablePushToFacebook)){
 181+ $fbPushDir = dirname(__FILE__) . '/pushEvents/';
 182+
 183+ // Convenience loop for push event classes in the fbPushDir directory
 184+ // whose file-name corresponds to the class-name. To add a push event
 185+ // which does not meet these criteria, just explicitly add it below.
 186+ $pushEventClassNames = array(
 187+ 'FBPush_OnAddImage',
 188+ 'FBPush_OnLargeEdit',
 189+ 'FBPush_OnWatchArticle',
 190+ );
 191+ foreach($pushEventClassNames as $pClassName){
 192+ $fbPushEventClasses[] = $pClassName;
 193+ $wgAutoloadClasses[$pClassName] = $fbPushDir . "$pClassName.php";
 194+ }
 195+
 196+ // Example of explicitly adding a push event which doesn't meet the criteria above.
 197+ // $fbPushEventClasses[] = 'FBPush_OnEXAMPLE_CLASS';
 198+ // $wgAutoloadClasses['FBPush_OnEXAMPLE_CLASS'] = $fbPushDir . 'FBPush_OnEXAMPLE_version_1.php';
 199+}
Index: trunk/extensions/FBConnect/fbconnect.js
@@ -0,0 +1,102 @@
 2+/*
 3+ * Copyright � 2010 Garrett Brown <http://www.mediawiki.org/wiki/User:Gbruin>
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program. If not, see <http://www.gnu.org/licenses/>.
 16+ */
 17+
 18+/**
 19+ * fbconnect.js and fbconnect-min.js
 20+ *
 21+ * FBConnect relies on several different libraries and frameworks for its
 22+ * JavaScript code. Each framework has its own method to verify that the proper
 23+ * code won't be called before it's ready. (Below, lambda represents a named or
 24+ * anonymous function.)
 25+ *
 26+ * MediaWiki: addOnloadHook(lambda);
 27+ * This function manages an array of window.onLoad event handlers to be
 28+ * called be called by a MediaWiki script when the window is fully loaded.
 29+ * Because the DOM may be ready before the window (due to large images to
 30+ * be downloaded), a faster alternative is JQuery's document-ready function.
 31+ *
 32+ * Facebook JavaScript SDK: window.fbAsyncInit = lambda;
 33+ * This global variable is called when the JavaScript SDK is fully
 34+ * initialized asynchronously to the document's state. This might be long
 35+ * after the document is finished rendering the first time the script is
 36+ * downloaded. Subsequently, it may even be called before the DOM is ready.
 37+ *
 38+ * jQuery: $(document).ready(lambda);
 39+ * Self-explanatory; to be called when the DOM is ready to be manipulated.
 40+ * Typically this should occur sooner than MediaWiki's addOnloadHook
 41+ * function is called.
 42+ */
 43+
 44+/**
 45+ * After the Facebook JavaScript SDK has been asynchronously loaded,
 46+ * it looks for the global fbAsyncInit and executes the function when found.
 47+ */
 48+window.fbAsyncInit = function() {
 49+ // Initialize the library with the API key
 50+ FB.init({
 51+ appId : window.fbAppId, // See $fbAppId in config.php
 52+ session: window.fbSession, // Don't re-fetch the session if PHP provides it
 53+ status : true, // Check login status
 54+ cookie : true, // Enable cookies to allow the server to access the session
 55+ xfbml : window.fbUseMarkup // Whether XFBML should be automatically parsed
 56+ });
 57+
 58+ // NOTE: Auth.login doesn't appear to work anymore.
 59+ // The onlogin attribute of the fb:login-buttons is being used instead.
 60+
 61+ // Register a function for when the user logs out of Facebook
 62+ FB.Event.subscribe('auth.logout', function(response) {
 63+ // TODO: Internationalize
 64+ var login = confirm("Not logged in.\n\nYou have been loggout out of " +
 65+ "Facebook. Press OK to log in via Facebook Connect " +
 66+ "again, or press Cancel to stay on the current page.");
 67+ if (login) {
 68+ window.location = window.wgArticlePath.replace(/\$1/, "Special:Connect");
 69+ }
 70+ });
 71+};
 72+
 73+/**
 74+ * jQuery code to be run when the DOM is ready to be manhandled.
 75+ */
 76+$(document).ready(function() {
 77+ // Add a pretty logo to Facebook links
 78+ $('#pt-fbconnect,#pt-fblink,#pt-fbconvert').addClass('mw-fblink');
 79+
 80+ // Add the logout behavior to the "Logout of Facebook" button
 81+ $('#pt-fblogout').click(function() {
 82+ // TODO: Where did the fancy DHTML window go? Maybe consider jQuery Alert Dialogs:
 83+ // http://abeautifulsite.net/2008/12/jquery-alert-dialogs/
 84+ var logout = confirm("You are logging out of both this site and Facebook.");
 85+ if (logout) {
 86+ FB.logout(function(response) {
 87+ window.location = window.fbLogoutURL;
 88+ });
 89+ }
 90+ });
 91+});
 92+
 93+/**
 94+ * An optional handler to use in fbOnLoginJsOverride for when a user logs in
 95+ * via Facebook Connect. This will redirect to Special:Connect with the
 96+ * returnto variables configured properly.
 97+ *
 98+ * TODO: Also set the value for 'returntoquery'!!
 99+ */
 100+function sendToConnectOnLogin(){
 101+ var destUrl = wgServer + wgScript + "?title=Special:Connect&returnto=" + wgPageName + "&returntoquery=" + wgPagequery;
 102+ window.location.href = destUrl;
 103+}

Follow-up revisions

RevisionCommit summaryAuthorDate
r77742Deleting FBConnect extension added in r67130. We're needlessly forking code t...demon21:30, 4 December 2010

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r205array_key_exists() doesn't exist in the running version of PHPvibber04:47, 26 February 2002

Comments

#Comment by Nikerabbit (talk | contribs)   08:17, 31 May 2010
  • Need to check encoding:
+ * Copyright \xA9
  • i18n file isn't in standard format
  • You can't do the following. Variables always need to be declared before use.
+               if(!empty($fbOnLoginJsOverride)){
  • Don't use & for assignments in PHP5
+               $dbr = & wfGetDB( DB_SLAVE, array(),  self::sharedDB() );
  • EstimateRowCount usually reports random numbers. Is that what you want?
+               $count = $dbr->estimateRowCount("{$prefix}user_fbconnect");
  • There is some manual html construction that looks unsafe


#Comment by 😂 (talk | contribs)   21:15, 4 December 2010

Suggest removal, this is becoming an unmaintained fork, which is now ~100 revs ahead of this.

If the SF maintainers want to move here, that's cool, but let's not needlessly fork their work.

#Comment by Nikerabbit (talk | contribs)   21:25, 4 December 2010

And just throw all the work away?

Status & tagging log