summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/Authentication/AuthenticationPlugin.php217
-rw-r--r--plugins/Authentication/User_username.php64
-rw-r--r--plugins/Authorization/AuthorizationPlugin.php108
-rw-r--r--plugins/Autocomplete/autocomplete.php19
-rw-r--r--plugins/BitlyUrl/BitlyUrlPlugin.php54
-rw-r--r--plugins/BlogspamNetPlugin.php15
-rw-r--r--plugins/EmailAuthentication/EmailAuthenticationPlugin.php54
-rw-r--r--plugins/EmailAuthentication/README7
-rw-r--r--plugins/FBConnect/README76
-rw-r--r--plugins/Facebook/FBCLoginGroupNav.php (renamed from plugins/FBConnect/FBCLoginGroupNav.php)0
-rw-r--r--plugins/Facebook/FBCSettingsNav.php (renamed from plugins/FBConnect/FBCSettingsNav.php)0
-rw-r--r--plugins/Facebook/FBC_XDReceiver.php (renamed from plugins/FBConnect/FBC_XDReceiver.php)0
-rw-r--r--plugins/Facebook/FBConnect.css (renamed from plugins/FBConnect/FBConnectPlugin.css)0
-rw-r--r--plugins/Facebook/FBConnectAuth.php (renamed from plugins/FBConnect/FBConnectAuth.php)2
-rw-r--r--plugins/Facebook/FBConnectLogin.php (renamed from plugins/FBConnect/FBConnectLogin.php)3
-rw-r--r--plugins/Facebook/FBConnectSettings.php (renamed from plugins/FBConnect/FBConnectSettings.php)0
-rw-r--r--plugins/Facebook/FacebookPlugin.php (renamed from plugins/FBConnect/FBConnectPlugin.php)378
-rw-r--r--plugins/Facebook/README129
-rw-r--r--plugins/Facebook/facebook/facebook.php598
-rw-r--r--plugins/Facebook/facebook/facebook_desktop.php104
-rwxr-xr-xplugins/Facebook/facebook/facebookapi_php5_restlib.php3618
-rw-r--r--plugins/Facebook/facebook/jsonwrapper/JSON/JSON.php806
-rw-r--r--plugins/Facebook/facebook/jsonwrapper/JSON/LICENSE21
-rw-r--r--plugins/Facebook/facebook/jsonwrapper/jsonwrapper.php6
-rw-r--r--plugins/Facebook/facebook/jsonwrapper/jsonwrapper_inner.php23
-rw-r--r--plugins/Facebook/facebookaction.php648
-rw-r--r--plugins/Facebook/facebookapp.css115
-rw-r--r--plugins/Facebook/facebookhome.php275
-rw-r--r--plugins/Facebook/facebookinvite.php146
-rw-r--r--plugins/Facebook/facebooklogin.php99
-rwxr-xr-xplugins/Facebook/facebookqueuehandler.php73
-rw-r--r--plugins/Facebook/facebookremove.php69
-rw-r--r--plugins/Facebook/facebooksettings.php159
-rw-r--r--plugins/Facebook/facebookutil.php295
-rw-r--r--plugins/Facebook/fbfavicon.ico (renamed from plugins/FBConnect/fbfavicon.ico)bin1150 -> 1150 bytes
-rw-r--r--plugins/GeoURLPlugin.php119
-rw-r--r--plugins/GeonamesPlugin.php329
-rw-r--r--plugins/InfiniteScroll/infinitescroll.js6
-rw-r--r--plugins/LdapAuthentication/LdapAuthenticationPlugin.php331
-rw-r--r--plugins/LdapAuthentication/README82
-rw-r--r--plugins/LdapAuthorization/LdapAuthorizationPlugin.php211
-rw-r--r--plugins/LdapAuthorization/README91
-rw-r--r--plugins/LilUrl/LilUrlPlugin.php61
-rw-r--r--plugins/LinkbackPlugin.php23
-rw-r--r--plugins/Mapstraction/MapstractionPlugin.php245
-rw-r--r--plugins/Mapstraction/allmap.php125
-rw-r--r--plugins/Mapstraction/js/mxn.(provider).core.js289
-rw-r--r--plugins/Mapstraction/js/mxn.cloudmade.core.js357
-rw-r--r--plugins/Mapstraction/js/mxn.core.js1758
-rw-r--r--plugins/Mapstraction/js/mxn.geocommons.core.js233
-rw-r--r--plugins/Mapstraction/js/mxn.google.core.js519
-rw-r--r--plugins/Mapstraction/js/mxn.google.geocoder.js179
-rw-r--r--plugins/Mapstraction/js/mxn.googlev3.core.js443
-rw-r--r--plugins/Mapstraction/js/mxn.js505
-rw-r--r--plugins/Mapstraction/js/mxn.microsoft.core.js402
-rw-r--r--plugins/Mapstraction/js/mxn.openlayers.core.js513
-rw-r--r--plugins/Mapstraction/js/mxn.yahoo.core.js391
-rw-r--r--plugins/Mapstraction/usermap.js45
-rw-r--r--plugins/Mapstraction/usermap.php126
-rw-r--r--plugins/Mobile/WAP20Plugin.php56
-rw-r--r--plugins/MobileProfile/MobileProfilePlugin.php425
-rw-r--r--plugins/MobileProfile/mp-handheld.css1
-rw-r--r--plugins/MobileProfile/mp-screen.css248
-rw-r--r--plugins/OpenID/OpenIDPlugin.php295
-rw-r--r--plugins/OpenID/User_openid.php54
-rw-r--r--plugins/OpenID/User_openid_trustroot.php45
-rw-r--r--plugins/OpenID/doc-src/openid11
-rw-r--r--plugins/OpenID/finishaddopenid.php185
-rw-r--r--plugins/OpenID/finishopenidlogin.php495
-rw-r--r--plugins/OpenID/openid.php293
-rw-r--r--plugins/OpenID/openidlogin.php137
-rw-r--r--plugins/OpenID/openidserver.php151
-rw-r--r--plugins/OpenID/openidsettings.php240
-rw-r--r--plugins/OpenID/openidtrust.php142
-rw-r--r--plugins/Orbited/OrbitedPlugin.php154
-rw-r--r--plugins/Orbited/orbitedextra.js2
-rw-r--r--plugins/Orbited/orbitedupdater.js24
-rw-r--r--plugins/PtitUrl/PtitUrlPlugin.php57
-rw-r--r--plugins/PubSubHubBub/PubSubHubBubPlugin.php121
-rw-r--r--plugins/PubSubHubBub/publisher.php86
-rw-r--r--plugins/Realtime/RealtimePlugin.php13
-rw-r--r--plugins/Realtime/icon_pause.gifbin0 -> 75 bytes
-rw-r--r--plugins/Realtime/icon_play.gifbin0 -> 75 bytes
-rw-r--r--plugins/Realtime/realtimeupdate.css49
-rw-r--r--plugins/Realtime/realtimeupdate.js186
-rw-r--r--plugins/Recaptcha/LICENSE (renamed from plugins/recaptcha/LICENSE)0
-rw-r--r--plugins/Recaptcha/README28
-rw-r--r--plugins/Recaptcha/RecaptchaPlugin.php (renamed from plugins/recaptcha/recaptcha.php)18
-rw-r--r--plugins/Recaptcha/recaptchalib.php (renamed from plugins/recaptcha/recaptchalib.php)0
-rw-r--r--plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php52
-rw-r--r--plugins/ReverseUsernameAuthentication/README26
-rw-r--r--plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php58
-rw-r--r--plugins/SimpleUrl/SimpleUrlPlugin.php51
-rw-r--r--plugins/SphinxSearch/README45
-rw-r--r--plugins/SphinxSearch/SphinxSearchPlugin.php100
-rwxr-xr-xplugins/SphinxSearch/scripts/gen_config.php126
-rwxr-xr-xplugins/SphinxSearch/scripts/index_update.php61
-rw-r--r--plugins/SphinxSearch/scripts/sphinx-utils.php63
-rwxr-xr-xplugins/SphinxSearch/scripts/sphinx.sh15
-rw-r--r--plugins/SphinxSearch/sphinx.conf.sample71
-rw-r--r--plugins/SphinxSearch/sphinxsearch.php96
-rw-r--r--plugins/TemplatePlugin.php2
-rw-r--r--plugins/TightUrl/TightUrlPlugin.php57
-rw-r--r--plugins/TwitterBridge/README85
-rw-r--r--plugins/TwitterBridge/TwitterBridgePlugin.php187
-rwxr-xr-xplugins/TwitterBridge/daemons/synctwitterfriends.php281
-rwxr-xr-xplugins/TwitterBridge/daemons/twitterqueuehandler.php73
-rwxr-xr-xplugins/TwitterBridge/daemons/twitterstatusfetcher.php570
-rw-r--r--plugins/TwitterBridge/twitter.php351
-rw-r--r--plugins/TwitterBridge/twitterauthorization.php224
-rw-r--r--plugins/TwitterBridge/twitterbasicauthclient.php204
-rw-r--r--plugins/TwitterBridge/twitteroauthclient.php229
-rw-r--r--plugins/TwitterBridge/twittersettings.php272
-rw-r--r--plugins/UrlShortener/UrlShortenerPlugin.php103
-rw-r--r--plugins/UserFlag/UserFlagPlugin.php148
-rw-r--r--plugins/UserFlag/User_flag_profile.php85
-rw-r--r--plugins/UserFlag/adminprofileflag.php201
-rw-r--r--plugins/UserFlag/flag.gifbin0 -> 80 bytes
-rw-r--r--plugins/UserFlag/flagprofile.php141
-rw-r--r--plugins/UserFlag/flagprofileform.php94
-rw-r--r--plugins/WikiHashtagsPlugin.php15
-rw-r--r--plugins/recaptcha/README23
122 files changed, 22860 insertions, 329 deletions
diff --git a/plugins/Authentication/AuthenticationPlugin.php b/plugins/Authentication/AuthenticationPlugin.php
new file mode 100644
index 000000000..cd1de1149
--- /dev/null
+++ b/plugins/Authentication/AuthenticationPlugin.php
@@ -0,0 +1,217 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Superclass for plugins that do authentication
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Superclass for plugins that do authentication
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+abstract class AuthenticationPlugin extends Plugin
+{
+ //is this plugin authoritative for authentication?
+ public $authoritative = false;
+
+ //should accounts be automatically created after a successful login attempt?
+ public $autoregistration = false;
+
+ //can the user change their email address
+ public $password_changeable=true;
+
+ //unique name for this authentication provider
+ public $provider_name;
+
+ //------------Auth plugin should implement some (or all) of these methods------------\\
+ /**
+ * Check if a nickname/password combination is valid
+ * @param username
+ * @param password
+ * @return boolean true if the credentials are valid, false if they are invalid.
+ */
+ function checkPassword($username, $password)
+ {
+ return false;
+ }
+
+ /**
+ * Automatically register a user when they attempt to login with valid credentials.
+ * User::register($data) is a very useful method for this implementation
+ * @param username
+ * @return mixed instance of User, or false (if user couldn't be created)
+ */
+ function autoRegister($username)
+ {
+ $registration_data = array();
+ $registration_data['nickname'] = $username ;
+ return User::register($registration_data);
+ }
+
+ /**
+ * Change a user's password
+ * The old password has been verified to be valid by this plugin before this call is made
+ * @param username
+ * @param oldpassword
+ * @param newpassword
+ * @return boolean true if the password was changed, false if password changing failed for some reason
+ */
+ function changePassword($username,$oldpassword,$newpassword)
+ {
+ return false;
+ }
+
+ //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
+ function onInitializePlugin(){
+ if(!isset($this->provider_name)){
+ throw new Exception("must specify a provider_name for this authentication provider");
+ }
+ }
+
+ function onStartCheckPassword($nickname, $password, &$authenticatedUser){
+ //map the nickname to a username
+ $user_username = new User_username();
+ $user_username->username=$nickname;
+ $user_username->provider_name=$this->provider_name;
+ if($user_username->find() && $user_username->fetch()){
+ $username = $user_username->username;
+ $authenticated = $this->checkPassword($username, $password);
+ if($authenticated){
+ $authenticatedUser = User::staticGet('id', $user_username->user_id);
+ return false;
+ }
+ }else{
+ $user = User::staticGet('nickname', $nickname);
+ if($user){
+ //make sure a different provider isn't handling this nickname
+ $user_username = new User_username();
+ $user_username->username=$nickname;
+ if(!$user_username->find()){
+ //no other provider claims this username, so it's safe for us to handle it
+ $authenticated = $this->checkPassword($nickname, $password);
+ if($authenticated){
+ $authenticatedUser = User::staticGet('nickname', $nickname);
+ User_username::register($authenticatedUser,$nickname,$this->provider_name);
+ return false;
+ }
+ }
+ }else{
+ if($this->autoregistration){
+ $authenticated = $this->checkPassword($nickname, $password);
+ if($authenticated){
+ $user = $this->autoregister($nickname);
+ if($user){
+ $authenticatedUser = $user;
+ User_username::register($authenticatedUser,$nickname,$this->provider_name);
+ return false;
+ }
+ }
+ }
+ }
+ }
+ if($this->authoritative){
+ return false;
+ }else{
+ //we're not authoritative, so let other handlers try
+ return;
+ }
+ }
+
+ function onStartChangePassword($user,$oldpassword,$newpassword)
+ {
+ if($this->password_changeable){
+ $user_username = new User_username();
+ $user_username->user_id=$user->id;
+ $user_username->provider_name=$this->provider_name;
+ if($user_username->find() && $user_username->fetch()){
+ $authenticated = $this->checkPassword($user_username->username, $oldpassword);
+ if($authenticated){
+ $result = $this->changePassword($user_username->username,$oldpassword,$newpassword);
+ if($result){
+ //stop handling of other handlers, because what was requested was done
+ return false;
+ }else{
+ throw new Exception(_('Password changing failed'));
+ }
+ }else{
+ if($this->authoritative){
+ //since we're authoritative, no other plugin could do this
+ throw new Exception(_('Password changing failed'));
+ }else{
+ //let another handler try
+ return null;
+ }
+ }
+ }
+ }else{
+ if($this->authoritative){
+ //since we're authoritative, no other plugin could do this
+ throw new Exception(_('Password changing is not allowed'));
+ }
+ }
+ }
+
+ function onStartAccountSettingsPasswordMenuItem($widget)
+ {
+ if($this->authoritative && !$this->password_changeable){
+ //since we're authoritative, no other plugin could change passwords, so do not render the menu item
+ return false;
+ }
+ }
+
+ function onAutoload($cls)
+ {
+ switch ($cls)
+ {
+ case 'User_username':
+ require_once(INSTALLDIR.'/plugins/Authentication/User_username.php');
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ function onCheckSchema() {
+ $schema = Schema::get();
+ $schema->ensureDataObject('User_username');
+ return true;
+ }
+
+ function onUserDeleteRelated($user, &$tables)
+ {
+ $tables[] = 'User_username';
+ return true;
+ }
+}
+
diff --git a/plugins/Authentication/User_username.php b/plugins/Authentication/User_username.php
new file mode 100644
index 000000000..6826f2681
--- /dev/null
+++ b/plugins/Authentication/User_username.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * Table Definition for user_username
+ */
+require_once INSTALLDIR.'/classes/Plugin_DataObject.php';
+
+class User_username extends Plugin_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'user_username'; // table name
+ public $user_id; // int(4) not_null
+ public $provider_name; // varchar(255) primary_key not_null
+ public $username; // varchar(255) primary_key not_null
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=null)
+ { return Memcached_DataObject::staticGet('User_username',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ /**
+ * Register a user with a username on a given provider
+ * @param User User object
+ * @param string username on the given provider
+ * @param provider_name string name of the provider
+ * @return mixed User_username instance if the registration succeeded, false if it did not
+ */
+ static function register($user, $username, $provider_name)
+ {
+ $user_username = new User_username();
+ $user_username->user_id = $user->id;
+ $user_username->provider_name = $provider_name;
+ $user_username->username = $username;
+ $user_username->created = DB_DataObject_Cast::dateTime();
+ if($user_username->insert()){
+ return $user_username;
+ }else{
+ return false;
+ }
+ }
+
+ /**
+ * Get the TableDef object that represents the table backing this class
+ * @return TableDef TableDef instance
+ */
+ function tableDef()
+ {
+ return new TableDef($this->__table,
+ array(new ColumnDef('provider_name', 'varchar',
+ '255', false, 'PRI'),
+ new ColumnDef('username', 'varchar',
+ '255', false, 'PRI'),
+ new ColumnDef('user_id', 'integer',
+ null, false),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'timestamp')));
+ }
+}
diff --git a/plugins/Authorization/AuthorizationPlugin.php b/plugins/Authorization/AuthorizationPlugin.php
new file mode 100644
index 000000000..e4e046d08
--- /dev/null
+++ b/plugins/Authorization/AuthorizationPlugin.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Superclass for plugins that do authorization
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Superclass for plugins that do authorization
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+abstract class AuthorizationPlugin extends Plugin
+{
+ //is this plugin authoritative for authorization?
+ public $authoritative = false;
+
+ //------------Auth plugin should implement some (or all) of these methods------------\\
+
+ /**
+ * Is a user allowed to log in?
+ * @param user
+ * @return boolean true if the user is allowed to login, false if explicitly not allowed to login, null if we don't explicitly allow or deny login
+ */
+ function loginAllowed($user) {
+ return null;
+ }
+
+ /**
+ * Does a profile grant the user a named role?
+ * @param profile
+ * @return boolean true if the profile has the role, false if not
+ */
+ function hasRole($profile, $name) {
+ return false;
+ }
+
+ //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
+ function onInitializePlugin(){
+
+ }
+
+ function onStartSetUser(&$user) {
+ $loginAllowed = $this->loginAllowed($user);
+ if($loginAllowed === true){
+ return;
+ }else if($loginAllowed === false){
+ $user = null;
+ return false;
+ }else{
+ if($this->authoritative) {
+ $user = null;
+ return false;
+ }else{
+ return;
+ }
+ }
+ }
+
+ function onStartSetApiUser(&$user) {
+ return $this->onStartSetUser(&$user);
+ }
+
+ function onStartHasRole($profile, $name, &$has_role) {
+ if($this->hasRole($profile, $name)){
+ $has_role = true;
+ return false;
+ }else{
+ if($this->authoritative) {
+ $has_role = false;
+ return false;
+ }else{
+ return;
+ }
+ }
+ }
+}
+
diff --git a/plugins/Autocomplete/autocomplete.php b/plugins/Autocomplete/autocomplete.php
index aa57b3915..379390ffd 100644
--- a/plugins/Autocomplete/autocomplete.php
+++ b/plugins/Autocomplete/autocomplete.php
@@ -98,11 +98,10 @@ class AutocompleteAction extends Action
$user = new User();
$user->limit($limit);
$user->whereAdd('nickname like \'' . trim($user->escape($q), '\'') . '%\'');
- $user->find();
- while($user->fetch()) {
- $profile = Profile::staticGet($user->id);
- $user->profile=$profile;
- $this->users[]=$user;
+ if($user->find()){
+ while($user->fetch()) {
+ $this->users[]=clone($user);
+ }
}
}
if(substr($q,0,1)=='!'){
@@ -111,9 +110,10 @@ class AutocompleteAction extends Action
$group = new User_group();
$group->limit($limit);
$group->whereAdd('nickname like \'' . trim($group->escape($q), '\'') . '%\'');
- $group->find();
- while($group->fetch()) {
- $this->groups[]=$group;
+ if($group->find()){
+ while($group->fetch()) {
+ $this->groups[]=clone($group);
+ }
}
}
return true;
@@ -124,7 +124,8 @@ class AutocompleteAction extends Action
parent::handle($args);
$results = array();
foreach($this->users as $user){
- $results[]=array('nickname' => $user->nickname, 'fullname'=> $user->profile->fullname, 'type'=>'user');
+ $profile = $user->getProfile();
+ $results[]=array('nickname' => $user->nickname, 'fullname'=> $profile->fullname, 'type'=>'user');
}
foreach($this->groups as $group){
$results[]=array('nickname' => $group->nickname, 'fullname'=> $group->fullname, 'type'=>'group');
diff --git a/plugins/BitlyUrl/BitlyUrlPlugin.php b/plugins/BitlyUrl/BitlyUrlPlugin.php
new file mode 100644
index 000000000..65d0f70e6
--- /dev/null
+++ b/plugins/BitlyUrl/BitlyUrlPlugin.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
+
+class BitlyUrlPlugin extends UrlShortenerPlugin
+{
+ public $serviceUrl;
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->serviceUrl)){
+ throw new Exception("must specify a serviceUrl");
+ }
+ }
+
+ protected function shorten($url) {
+ $response = $this->http_get($url);
+ if(!$response) return;
+ return current(json_decode($response)->results)->hashUrl;
+ }
+}
+
+
diff --git a/plugins/BlogspamNetPlugin.php b/plugins/BlogspamNetPlugin.php
index c14569746..51236001a 100644
--- a/plugins/BlogspamNetPlugin.php
+++ b/plugins/BlogspamNetPlugin.php
@@ -22,6 +22,7 @@
* @category Plugin
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
+ * @author Brion Vibber <brion@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
@@ -69,14 +70,12 @@ class BlogspamNetPlugin extends Plugin
{
$args = $this->testArgs($notice);
common_debug("Blogspamnet args = " . print_r($args, TRUE));
- $request = xmlrpc_encode_request('testComment', array($args));
- $context = stream_context_create(array('http' => array('method' => "POST",
- 'header' =>
- "Content-Type: text/xml\r\n".
- "User-Agent: " . $this->userAgent(),
- 'content' => $request)));
- $file = file_get_contents($this->baseUrl, false, $context);
- $response = xmlrpc_decode($file);
+ $requestBody = xmlrpc_encode_request('testComment', array($args));
+
+ $request = HTTPClient::start();
+ $httpResponse = $request->post($this->baseUrl, array('Content-Type: text/xml'), $requestBody);
+
+ $response = xmlrpc_decode($httpResponse->getBody());
if (xmlrpc_is_fault($response)) {
throw new ServerException("$response[faultString] ($response[faultCode])", 500);
} else {
diff --git a/plugins/EmailAuthentication/EmailAuthenticationPlugin.php b/plugins/EmailAuthentication/EmailAuthenticationPlugin.php
new file mode 100644
index 000000000..25e537735
--- /dev/null
+++ b/plugins/EmailAuthentication/EmailAuthenticationPlugin.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin that uses the email address as a username, and checks the password as normal
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+class EmailAuthenticationPlugin extends Plugin
+{
+ //---interface implementation---//
+
+ function onStartCheckPassword($nickname, $password, &$authenticatedUser)
+ {
+ if(strpos($nickname, '@'))
+ {
+ $user = User::staticGet('email',$nickname);
+ if($user && isset($user->email))
+ {
+ if(common_check_user($user->nickname,$password))
+ {
+ $authenticatedUser = $user;
+ return false;
+ }
+ }
+ }
+ }
+}
+
diff --git a/plugins/EmailAuthentication/README b/plugins/EmailAuthentication/README
new file mode 100644
index 000000000..320815689
--- /dev/null
+++ b/plugins/EmailAuthentication/README
@@ -0,0 +1,7 @@
+The Email Authentication plugin allows users to login using their email address.
+
+The provided email address is used to lookup the user's nickname, then that nickname and the provided password is checked.
+
+Installation
+============
+add "addPlugin('emailAuthentication');" to the bottom of your config.php
diff --git a/plugins/FBConnect/README b/plugins/FBConnect/README
deleted file mode 100644
index 77d57eff9..000000000
--- a/plugins/FBConnect/README
+++ /dev/null
@@ -1,76 +0,0 @@
-This plugin allows you to utilize Facebook Connect with StatusNet.
-Supported Facebook Connect features:
-
-- Authenticate (register/login/logout -- works similar to OpenID)
-- Associate an existing StatusNet account with a Facebook account
-- Disconnect a Facebook account from a StatusNet account
-
-Future planned functionality:
-
-- Invite Facebook friends to use your StatusNet installation
-- Auto-subscribe Facebook friends already using StatusNet
-- Share StatusNet favorite notices to your Facebook stream
-
-To use the plugin you will need to configure a Facebook application
-to point to your StatusNet installation (see the Installation section
-below).
-
-Installation
-============
-
-If you don't already have the built-in Facebook application configured,
-you'll need to log into Facebook and create/configure a new application.
-Please follow the instructions in the section titled, "Setting Up Your
-Application and Getting an API Key," on the following page of the
-Facebook developer wiki:
-
- http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site
-
-If you already are using the build-in StatusNet Facebook application,
-you can modify your existing application's configuration using the
-Facebook Developer Application on Facebook. Use it to edit your
-application settings, and under the 'Connect' tab, change the 'Connect
-URL' to be the main URL for your StatusNet site. E.g.:
-
- http://SITE/PATH_TO_STATUSNET/
-
-After you application is created and configured, you'll need to add its
-API key and secret to your StatusNet config.php file:
-
- $config['facebook']['apikey'] = 'APIKEY';
- $config['facebook']['secret'] = 'SECRET';
-
-Finally, to enable the plugin, add the following stanza to your
-config.php:
-
- addPlugin('FBConnect');
-
-To try out the plugin, fire up your browser and connect to:
-
- http://SITE/PATH_TO_STATUSNET/main/facebooklogin
-
-or, if you do not have fancy URLs turned on:
-
- http://SITE/PATH_TO_STATUSNET/index.php/main/facebooklogin
-
-You should see a page with a blue button that says: "Connect with
-Facebook".
-
-Connect/Disconnect existing account
-===================================
-
-If the Facebook Connect plugin is enabled, there will be a new Facebook
-Connect Settings tab under each user's Connect menu. Users can connect
-and disconnect to their Facebook accounts from it. Note: Before a user
-can disconnect from Facebook, she must set a normal StatusNet password.
-Otherwise, she might not be able to login in to her account in the
-future. This is usually only required for users who have used Facebook
-Connect to register their StatusNet account, and therefore haven't
-already set a local password.
-
-Helpful links
-=============
-
-Facebook Connect Homepage:
-http://developers.facebook.com/connect.php
-
diff --git a/plugins/FBConnect/FBCLoginGroupNav.php b/plugins/Facebook/FBCLoginGroupNav.php
index 81b2520a4..81b2520a4 100644
--- a/plugins/FBConnect/FBCLoginGroupNav.php
+++ b/plugins/Facebook/FBCLoginGroupNav.php
diff --git a/plugins/FBConnect/FBCSettingsNav.php b/plugins/Facebook/FBCSettingsNav.php
index ed02371e2..ed02371e2 100644
--- a/plugins/FBConnect/FBCSettingsNav.php
+++ b/plugins/Facebook/FBCSettingsNav.php
diff --git a/plugins/FBConnect/FBC_XDReceiver.php b/plugins/Facebook/FBC_XDReceiver.php
index 2bc790d5a..2bc790d5a 100644
--- a/plugins/FBConnect/FBC_XDReceiver.php
+++ b/plugins/Facebook/FBC_XDReceiver.php
diff --git a/plugins/FBConnect/FBConnectPlugin.css b/plugins/Facebook/FBConnect.css
index 49217bf13..49217bf13 100644
--- a/plugins/FBConnect/FBConnectPlugin.css
+++ b/plugins/Facebook/FBConnect.css
diff --git a/plugins/FBConnect/FBConnectAuth.php b/plugins/Facebook/FBConnectAuth.php
index 647d5def8..b909a4977 100644
--- a/plugins/FBConnect/FBConnectAuth.php
+++ b/plugins/Facebook/FBConnectAuth.php
@@ -27,7 +27,7 @@
* @link http://status.net/
*/
-require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+require_once INSTALLDIR . '/plugins/Facebook/FacebookPlugin.php';
class FBConnectauthAction extends Action
{
diff --git a/plugins/FBConnect/FBConnectLogin.php b/plugins/Facebook/FBConnectLogin.php
index 5696d8848..f146bef7d 100644
--- a/plugins/FBConnect/FBConnectLogin.php
+++ b/plugins/Facebook/FBConnectLogin.php
@@ -21,7 +21,8 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
-require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+
+require_once INSTALLDIR . '/plugins/Facebook/FacebookPlugin.php';
class FBConnectLoginAction extends Action
{
diff --git a/plugins/FBConnect/FBConnectSettings.php b/plugins/Facebook/FBConnectSettings.php
index 911c56787..911c56787 100644
--- a/plugins/FBConnect/FBConnectSettings.php
+++ b/plugins/Facebook/FBConnectSettings.php
diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/Facebook/FacebookPlugin.php
index ff74aade4..b68534b24 100644
--- a/plugins/FBConnect/FBConnectPlugin.php
+++ b/plugins/Facebook/FacebookPlugin.php
@@ -2,7 +2,7 @@
/**
* StatusNet, the distributed open-source microblogging tool
*
- * Plugin to enable Facebook Connect
+ * Plugin to add a StatusNet Facebook application
*
* PHP version 5
*
@@ -27,22 +27,16 @@
* @link http://status.net/
*/
-if (!defined('STATUSNET') && !defined('LACONICA')) {
+if (!defined('STATUSNET')) {
exit(1);
}
define("FACEBOOK_CONNECT_SERVICE", 3);
-require_once INSTALLDIR . '/lib/facebookutil.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBConnectAuth.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBConnectLogin.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBConnectSettings.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBCLoginGroupNav.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBCSettingsNav.php';
-require_once INSTALLDIR . '/plugins/FBConnect/FBC_XDReceiver.php';
+require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
/**
- * Plugin to enable Facebook Connect
+ * Facebook plugin to add a StatusNet Facebook application
*
* @category Plugin
* @package StatusNet
@@ -51,22 +45,88 @@ require_once INSTALLDIR . '/plugins/FBConnect/FBC_XDReceiver.php';
* @link http://status.net/
*/
-class FBConnectPlugin extends Plugin
+class FacebookPlugin extends Plugin
{
- function __construct()
+
+ /**
+ * Add Facebook app actions to the router table
+ *
+ * Hook for RouterInitialized event.
+ *
+ * @param Net_URL_Mapper &$m path-to-action mapper
+ *
+ * @return boolean hook return
+ */
+
+ function onRouterInitialized($m)
{
- parent::__construct();
- }
- // Hook in new actions
- function onRouterInitialized(&$m) {
+ // Facebook App stuff
+
+ $m->connect('facebook/app', array('action' => 'facebookhome'));
+ $m->connect('facebook/app/index.php', array('action' => 'facebookhome'));
+ $m->connect('facebook/app/settings.php',
+ array('action' => 'facebooksettings'));
+ $m->connect('facebook/app/invite.php', array('action' => 'facebookinvite'));
+ $m->connect('facebook/app/remove', array('action' => 'facebookremove'));
+
+ // Facebook Connect stuff
+
$m->connect('main/facebookconnect', array('action' => 'FBConnectAuth'));
$m->connect('main/facebooklogin', array('action' => 'FBConnectLogin'));
$m->connect('settings/facebook', array('action' => 'FBConnectSettings'));
$m->connect('xd_receiver.html', array('action' => 'FBC_XDReceiver'));
- }
- // Add in xmlns:fb
+ return true;
+ }
+
+ /**
+ * Automatically load the actions and libraries used by the Facebook app
+ *
+ * @param Class $cls the class
+ *
+ * @return boolean hook return
+ *
+ */
+
+ function onAutoload($cls)
+ {
+ switch ($cls) {
+ case 'FacebookAction':
+ case 'FacebookhomeAction':
+ case 'FacebookinviteAction':
+ case 'FacebookremoveAction':
+ case 'FacebooksettingsAction':
+ include_once INSTALLDIR . '/plugins/Facebook/' .
+ strtolower(mb_substr($cls, 0, -6)) . '.php';
+ return false;
+ case 'FBConnectAuthAction':
+ case 'FBConnectLoginAction':
+ case 'FBConnectSettingsAction':
+ case 'FBC_XDReceiverAction':
+ include_once INSTALLDIR . '/plugins/Facebook/' .
+ mb_substr($cls, 0, -6) . '.php';
+ return false;
+ case 'FBCLoginGroupNav':
+ include_once INSTALLDIR . '/plugins/Facebook/FBCLoginGroupNav.php';
+ return false;
+ case 'FBCSettingsNav':
+ include_once INSTALLDIR . '/plugins/Facebook/FBCSettingsNav.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Override normal HTML output to force the content type to
+ * text/html and add in xmlns:fb
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ */
+
function onStartShowHTML($action)
{
@@ -78,6 +138,7 @@ class FBConnectPlugin extends Plugin
// text/html even though Facebook Connect uses XHTML. This is
// A bug in Facebook Connect, and this is a temporary solution
// until they fix their JavaScript libs.
+
header('Content-Type: text/html');
$action->extraHeaders();
@@ -100,59 +161,108 @@ class FBConnectPlugin extends Plugin
}
}
- // Note: this script needs to appear in the <body>
+ /**
+ * Add in the Facebook Connect JavaScript stuff
+ *
+ * Note: this script needs to appear in the <body>
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ *
+ */
- function onStartShowHeader($action)
+ function onEndShowScripts($action)
{
if ($this->reqFbScripts($action)) {
- $apikey = common_config('facebook', 'apikey');
- $plugin_path = common_path('plugins/FBConnect');
+ $apikey = common_config('facebook', 'apikey');
+ $plugin_path = common_path('plugins/Facebook');
- $login_url = common_local_url('FBConnectAuth');
+ $login_url = common_local_url('FBConnectAuth');
$logout_url = common_local_url('logout');
// XXX: Facebook says we don't need this FB_RequireFeatures(),
// but we actually do, for IE and Safari. Gar.
- $html = sprintf('<script type="text/javascript">
- $(document).ready(function () {
- FB_RequireFeatures(
- ["XFBML"],
- function() {
- FB.init("%s", "../xd_receiver.html");
- }
- ); });
-
- function goto_login() {
- window.location = "%s";
- }
-
- function goto_logout() {
- window.location = "%s";
- }
- </script>', $apikey,
- $login_url, $logout_url);
-
- $action->raw($html);
+ $js = '<script type="text/javascript">';
+ $js .= ' $(document).ready(function () {';
+ $js .= ' FB_RequireFeatures(';
+ $js .= ' ["XFBML"], function() {';
+ $js .= ' FB.init("%1$s", "../xd_receiver.html");';
+ $js .= ' }';
+ $js .= ' );';
+ $js .= ' });';
+
+ $js .= ' function goto_login() {';
+ $js .= ' window.location = "%2$s";';
+ $js .= ' }';
+
+ // The below function alters the logout link so that it logs the user out
+ // of Facebook Connect as well as the site. However, for some pages
+ // (FB Connect Settings) we need to output the FB Connect scripts (to
+ // show an existing FB connection even if the user isn't authenticated
+ // with Facebook connect) but NOT alter the logout link. And the only
+ // way to reliably do that is with the FB Connect .js libs. Crazy.
+
+ $js .= ' FB.ensureInit(function() {';
+ $js .= ' FB.Connect.ifUserConnected(';
+ $js .= ' function() { ';
+ $js .= ' $(\'#nav_logout a\').attr(\'href\', \'#\');';
+ $js .= ' $(\'#nav_logout a\').click(function() {';
+ $js .= ' FB.Connect.logoutAndRedirect(\'%3$s\');';
+ $js .= ' return false;';
+ $js .= ' })';
+ $js .= ' },';
+ $js .= ' function() {';
+ $js .= ' return false;';
+ $js .= ' }';
+ $js .= ' );';
+ $js .= ' });';
+ $js .= '</script>';
+
+ $js = sprintf($js, $apikey, $login_url, $logout_url);
+
+ // Compress the bugger down a bit
+
+ $js = str_replace(' ', '', $js);
+
+ $action->raw(" $js"); // leading two spaces to make it line up
}
}
- // Note: this script needs to appear as close as possible to </body>
+ /**
+ * Add in an additional Facebook Connect script that's supposed to
+ * appear as close as possible to </body>
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ *
+ */
function onEndShowFooter($action)
{
if ($this->reqFbScripts($action)) {
- $action->script('http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php');
+ $action->script('http://static.ak.connect.facebook.com' .
+ '/js/api_lib/v0.4/FeatureLoader.js.php');
}
}
+ /**
+ * Output Facebook Connect specific CSS link
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ *
+ */
+
function onEndShowStatusNetStyles($action)
{
-
if ($this->reqFbScripts($action)) {
- $action->cssLink('plugins/FBConnect/FBConnectPlugin.css');
+ $action->cssLink('plugins/Facebook/FBConnect.css');
}
}
@@ -161,12 +271,13 @@ class FBConnectPlugin extends Plugin
* want to output FB namespace, scripts, CSS, etc. on the pages that
* really need them.
*
- * @param Action the action in question
+ * @param Action $action the current action
*
* @return boolean true
*/
- function reqFbScripts($action) {
+ function reqFbScripts($action)
+ {
// If you're logged in w/FB Connect, you always need the FB stuff
@@ -228,10 +339,19 @@ class FBConnectPlugin extends Plugin
return null;
}
+ /**
+ * Add in a Facebook Connect avatar to the primary nav menu
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ *
+ */
+
function onStartPrimaryNav($action)
{
-
$user = common_current_user();
+
$connect = 'FBConnectSettings';
if (common_config('xmpp', 'enabled')) {
$connect = 'imsettings';
@@ -262,78 +382,31 @@ class FBConnectPlugin extends Plugin
'alt' => 'Facebook Connect User',
'width' => '16'), '');
- $iconurl = common_path('plugins/FBConnect/fbfavicon.ico');
+ $iconurl = common_path('plugins/Facebook/fbfavicon.ico');
$action->element('img', array('id' => 'fb_favicon',
'src' => $iconurl));
$action->elementEnd('li');
}
+ }
- $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
- _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
- $action->menuItem(common_local_url('profilesettings'),
- _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
- $action->menuItem(common_local_url($connect),
- _('Connect'), _('Connect to services'), false, 'nav_connect');
- if (common_config('invite', 'enabled')) {
- $action->menuItem(common_local_url('invite'),
- _('Invite'),
- sprintf(_('Invite friends and colleagues to join you on %s'),
- common_config('site', 'name')),
- false, 'nav_invitecontact');
- }
-
- // Need to override the Logout link to make it do FB stuff
- if (!empty($fbuid)) {
-
- $logout_url = common_local_url('logout');
- $title = _('Logout from the site');
- $text = _('Logout');
-
- $html = sprintf('<li id="nav_logout"><a href="#" title="%s" ' .
- 'onclick="FB.Connect.logoutAndRedirect(\'%s\');">%s</a></li>',
- $title, $logout_url, $text);
-
- $action->raw($html);
-
- } else {
- $action->menuItem(common_local_url('logout'),
- _('Logout'), _('Logout from the site'), false, 'nav_logout');
- }
- }
- else {
- if (!common_config('site', 'openidonly')) {
- if (!common_config('site', 'closed')) {
- $action->menuItem(common_local_url('register'),
- _('Register'), _('Create an account'), false, 'nav_register');
- }
- $action->menuItem(common_local_url('login'),
- _('Login'), _('Login to the site'), false, 'nav_login');
- } else {
- $this->menuItem(common_local_url('openidlogin'),
- _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
- }
- }
-
- $action->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'), _('Help me!'), false, 'nav_help');
- if ($user || !common_config('site', 'private')) {
- $action->menuItem(common_local_url('peoplesearch'),
- _('Search'), _('Search for people or text'), false, 'nav_search');
- }
-
- // We are replacing the primary nav entirely; give other
- // plugins a chance to handle it here.
-
- Event::handle('EndPrimaryNav', array($action));
-
- return false;
+ return true;
}
+ /**
+ * Alter the local nav menu to have a Facebook Connect login and
+ * settings pages
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ *
+ */
+
function onStartShowLocalNavBlock($action)
{
- $action_name = get_class($action);
+ $action_name = get_class($action);
$login_actions = array('LoginAction', 'RegisterAction',
'OpenidloginAction', 'FBConnectLoginAction');
@@ -356,8 +429,16 @@ class FBConnectPlugin extends Plugin
return true;
}
+ /**
+ * Have the logout process do some Facebook Connect cookie cleanup
+ *
+ * @param Action $action the current action
+ *
+ * @return void
+ */
+
function onStartLogout($action)
-{
+ {
$action->logout();
$fbuid = $this->loggedIn();
@@ -375,9 +456,16 @@ class FBConnectPlugin extends Plugin
return true;
}
+ /**
+ * Get the URL of the user's Facebook avatar
+ *
+ * @param int $fbuid the Facebook user ID
+ *
+ * @return string $url the url for the user's Facebook avatar
+ */
+
function getProfilePicURL($fbuid)
{
-
$facebook = getFacebook();
$url = null;
@@ -396,8 +484,70 @@ class FBConnectPlugin extends Plugin
"Facebook client failure requesting profile pic!");
}
- return $url;
+ return $url;
+ }
+
+ /**
+ * Add a Facebook queue item for each notice
+ *
+ * @param Notice $notice the notice
+ * @param array &$transports the list of transports (queues)
+ *
+ * @return boolean hook return
+ */
+ function onStartEnqueueNotice($notice, &$transports)
+ {
+ array_push($transports, 'facebook');
+ return true;
+ }
+
+ /**
+ * broadcast the message when not using queuehandler
+ *
+ * @param Notice &$notice the notice
+ * @param array $queue destination queue
+ *
+ * @return boolean hook return
+ */
+
+ function onUnqueueHandleNotice(&$notice, $queue)
+ {
+ if (($queue == 'facebook') && ($this->_isLocal($notice))) {
+ facebookBroadcastNotice($notice);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determine whether the notice was locally created
+ *
+ * @param Notice $notice the notice
+ *
+ * @return boolean locality
+ */
+
+ function _isLocal($notice)
+ {
+ return ($notice->is_local == Notice::LOCAL_PUBLIC ||
+ $notice->is_local == Notice::LOCAL_NONPUBLIC);
+ }
+
+ /**
+ * Add Facebook queuehandler to the list of daemons to start
+ *
+ * @param array $daemons the list fo daemons to run
+ *
+ * @return boolean hook return
+ *
+ */
+
+ function onGetValidDaemons($daemons)
+ {
+ array_push($daemons, INSTALLDIR .
+ '/plugins/Facebook/facebookqueuehandler.php');
+ return true;
}
}
diff --git a/plugins/Facebook/README b/plugins/Facebook/README
new file mode 100644
index 000000000..bf2f4a180
--- /dev/null
+++ b/plugins/Facebook/README
@@ -0,0 +1,129 @@
+This plugin allows you to use Facebook Connect with StatusNet, provides a
+Facebook application for your users, and allows them to update their
+Facebook statuses from StatusNet.
+
+Facebook Connect
+----------------
+
+Facebook connect allows users to register and login using nothing but their
+Facebook credentials. With Facebook Connect, your users can:
+
+- Authenticate (register/login/logout -- works similar to OpenID)
+- Associate an existing StatusNet account with a Facebook account
+- Disconnect a Facebook account from a StatusNet account
+
+Built-in Facebook Application
+-----------------------------
+
+The plugin also installs a StatusNet Facebook application that allows your
+users to automatically update their Facebook statuses with their latest
+notices, invite their friends to use the app (and thus your site), view
+their notice timelines, and post notices -- all from within Facebook. The
+application is built into the StatusNet Facebook plugin and runs on your
+host.
+
+Quick setup instructions*
+-------------------------
+
+Install the Facebook Developer application on Facebook:
+
+ http://www.facebook.com/developers/
+
+Use it to create a new application and generate an API key and secret. Add a
+Facebook app section of your config.php and copy in the key and secret,
+e.g.:
+
+ // Config section for the built-in Facebook application
+ $config['facebook']['apikey'] = 'APIKEY';
+ $config['facebook']['secret'] = 'SECRET';
+
+In Facebook's application editor, specify the following URLs for your app:
+
+- Canvas Callback URL : http://example.net/mublog/facebook/app/
+- Post-Remove Callback URL: http://example.net/mublog/facebook/app/remove
+- Post-Add Redirect URL : http://apps.facebook.com/yourapp/
+- Canvas Page URL : http://apps.facebook.com/yourapp/
+- Connect URL : http://example.net/mublog/
+
+ *** ATTENTION ***
+ These URLs have changed slightly since StatusNet version 0.8.1,
+ so if you have been using the Facebook app previously, you will
+ need to update your configuration!
+
+Replace "example.net" with your host's URL, "mublog" with the path to your
+StatusNet installation, and 'yourapp' with the name of the Facebook
+application you created. (If you don't have "Fancy URLs" on, you'll need to
+change http://example.net/mublog/ to http://example.net/mublog/index.php/).
+
+Additionally, Choose "Web" for Application type in the Advanced tab. In the
+"Canvas setting" section, choose the "FBML" for Render Method, "Smart Size"
+for IFrame size, and "Full width (760px)" for Canvas Width. Everything else
+can be left with default values.
+
+* NOTE: For more under-the-hood detailed instructions about setting up a
+ Facebook application and getting an API key, check out the
+ following pages on the Facebook wiki:
+
+ http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site
+ http://wiki.developers.facebook.com/index.php/Creating_your_first_application
+
+Finally you must activate the plugin by adding the following line to your
+config.php:
+
+ addPlugin('Facebook');
+
+Testing It Out
+--------------
+
+If the Facebook plugin is enabled and working, there will be a new Facebook
+Connect Settings tab under each user's Connect menu. Users can connect and
+disconnect* to their Facebook accounts from it.
+
+To try out the plugin, fire up your browser and connect to:
+
+ http://SITE/PATH_TO_STATUSNET/main/facebooklogin
+
+or, if you do not have fancy URLs turned on:
+
+ http://SITE/PATH_TO_STATUSNET/index.php/main/facebooklogin
+
+You should see a page with a blue button that says: "Connect with Facebook"
+and you should be able to login or register.
+
+From within Facebook, you should also be able to get to the Facebook
+application, and run it by hitting the link you specified above when
+configuring it:
+
+ http://apps.facebook.com/yourapp/
+
+That link should be present you with a login screen. After logging in to
+the app, you are given the option to update their Facebook status via
+StatusNet.
+
+* Note: Before a user can disconnect from Facebook, she must set a normal
+ StatusNet password. Otherwise, she might not be able to login in to her
+ account in the future. This is usually only required for users who have
+ used Facebook Connect to register their StatusNet account, and therefore
+ haven't already set a local password.
+
+Offline Queue Handling
+----------------------
+
+For larger sites needing better performance it's possible to enable queuing
+and have users' notices posted to Facebook via a separate "offline"
+FacebookQueueHandler (facebookqueuhandler.php in the Facebook plugin
+directory), which will be started by the plugin along with their other
+daemons when you run scripts/startdaemons.sh. See the StatusNet README for
+more about queuing and daemons.
+
+TODO
+----
+
+- Invite Facebook friends to use your StatusNet installation via Facebook
+ Connect
+- Auto-subscribe Facebook friends already using StatusNet
+- Share StatusNet favorite notices to your Facebook stream
+- Allow users to update their Facebook statuses once they have authenticated
+ with Facebook Connect (no need for them to use the Facebook app if they
+ don't want to).
+- Re-design the whole thing to support multiple instances of StatusNet
diff --git a/plugins/Facebook/facebook/facebook.php b/plugins/Facebook/facebook/facebook.php
new file mode 100644
index 000000000..016e8e8e0
--- /dev/null
+++ b/plugins/Facebook/facebook/facebook.php
@@ -0,0 +1,598 @@
+<?php
+// Copyright 2004-2009 Facebook. All Rights Reserved.
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+
+include_once 'facebookapi_php5_restlib.php';
+
+define('FACEBOOK_API_VALIDATION_ERROR', 1);
+class Facebook {
+ public $api_client;
+ public $api_key;
+ public $secret;
+ public $generate_session_secret;
+ public $session_expires;
+
+ public $fb_params;
+ public $user;
+ public $profile_user;
+ public $canvas_user;
+ protected $base_domain;
+ /*
+ * Create a Facebook client like this:
+ *
+ * $fb = new Facebook(API_KEY, SECRET);
+ *
+ * This will automatically pull in any parameters, validate them against the
+ * session signature, and chuck them in the public $fb_params member variable.
+ *
+ * @param api_key your Developer API key
+ * @param secret your Developer API secret
+ * @param generate_session_secret whether to automatically generate a session
+ * if the user doesn't have one, but
+ * there is an auth token present in the url,
+ */
+ public function __construct($api_key, $secret, $generate_session_secret=false) {
+ $this->api_key = $api_key;
+ $this->secret = $secret;
+ $this->generate_session_secret = $generate_session_secret;
+ $this->api_client = new FacebookRestClient($api_key, $secret, null);
+ $this->validate_fb_params();
+
+ // Set the default user id for methods that allow the caller to
+ // pass an explicit uid instead of using a session key.
+ $defaultUser = null;
+ if ($this->user) {
+ $defaultUser = $this->user;
+ } else if ($this->profile_user) {
+ $defaultUser = $this->profile_user;
+ } else if ($this->canvas_user) {
+ $defaultUser = $this->canvas_user;
+ }
+
+ $this->api_client->set_user($defaultUser);
+
+
+ if (isset($this->fb_params['friends'])) {
+ $this->api_client->friends_list = explode(',', $this->fb_params['friends']);
+ }
+ if (isset($this->fb_params['added'])) {
+ $this->api_client->added = $this->fb_params['added'];
+ }
+ if (isset($this->fb_params['canvas_user'])) {
+ $this->api_client->canvas_user = $this->fb_params['canvas_user'];
+ }
+ }
+
+ /*
+ * Validates that the parameters passed in were sent from Facebook. It does so
+ * by validating that the signature matches one that could only be generated
+ * by using your application's secret key.
+ *
+ * Facebook-provided parameters will come from $_POST, $_GET, or $_COOKIE,
+ * in that order. $_POST and $_GET are always more up-to-date than cookies,
+ * so we prefer those if they are available.
+ *
+ * For nitty-gritty details of when each of these is used, check out
+ * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
+ *
+ * @param bool resolve_auth_token convert an auth token into a session
+ */
+ public function validate_fb_params($resolve_auth_token=true) {
+ $this->fb_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_sig');
+
+ // note that with preload FQL, it's possible to receive POST params in
+ // addition to GET, so use a different prefix to differentiate them
+ if (!$this->fb_params) {
+ $fb_params = $this->get_valid_fb_params($_GET, 48 * 3600, 'fb_sig');
+ $fb_post_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_post_sig');
+ $this->fb_params = array_merge($fb_params, $fb_post_params);
+ }
+
+ // Okay, something came in via POST or GET
+ if ($this->fb_params) {
+ $user = isset($this->fb_params['user']) ?
+ $this->fb_params['user'] : null;
+ $this->profile_user = isset($this->fb_params['profile_user']) ?
+ $this->fb_params['profile_user'] : null;
+ $this->canvas_user = isset($this->fb_params['canvas_user']) ?
+ $this->fb_params['canvas_user'] : null;
+ $this->base_domain = isset($this->fb_params['base_domain']) ?
+ $this->fb_params['base_domain'] : null;
+
+ if (isset($this->fb_params['session_key'])) {
+ $session_key = $this->fb_params['session_key'];
+ } else if (isset($this->fb_params['profile_session_key'])) {
+ $session_key = $this->fb_params['profile_session_key'];
+ } else {
+ $session_key = null;
+ }
+ $expires = isset($this->fb_params['expires']) ?
+ $this->fb_params['expires'] : null;
+ $this->set_user($user,
+ $session_key,
+ $expires);
+ }
+ // if no Facebook parameters were found in the GET or POST variables,
+ // then fall back to cookies, which may have cached user information
+ // Cookies are also used to receive session data via the Javascript API
+ else if ($cookies =
+ $this->get_valid_fb_params($_COOKIE, null, $this->api_key)) {
+
+ $base_domain_cookie = 'base_domain_' . $this->api_key;
+ if (isset($_COOKIE[$base_domain_cookie])) {
+ $this->base_domain = $_COOKIE[$base_domain_cookie];
+ }
+
+ // use $api_key . '_' as a prefix for the cookies in case there are
+ // multiple facebook clients on the same domain.
+ $expires = isset($cookies['expires']) ? $cookies['expires'] : null;
+ $this->set_user($cookies['user'],
+ $cookies['session_key'],
+ $expires);
+ }
+ // finally, if we received no parameters, but the 'auth_token' GET var
+ // is present, then we are in the middle of auth handshake,
+ // so go ahead and create the session
+ else if ($resolve_auth_token && isset($_GET['auth_token']) &&
+ $session = $this->do_get_session($_GET['auth_token'])) {
+ if ($this->generate_session_secret &&
+ !empty($session['secret'])) {
+ $session_secret = $session['secret'];
+ }
+
+ if (isset($session['base_domain'])) {
+ $this->base_domain = $session['base_domain'];
+ }
+
+ $this->set_user($session['uid'],
+ $session['session_key'],
+ $session['expires'],
+ isset($session_secret) ? $session_secret : null);
+ }
+
+ return !empty($this->fb_params);
+ }
+
+ // Store a temporary session secret for the current session
+ // for use with the JS client library
+ public function promote_session() {
+ try {
+ $session_secret = $this->api_client->auth_promoteSession();
+ if (!$this->in_fb_canvas()) {
+ $this->set_cookies($this->user, $this->api_client->session_key, $this->session_expires, $session_secret);
+ }
+ return $session_secret;
+ } catch (FacebookRestClientException $e) {
+ // API_EC_PARAM means we don't have a logged in user, otherwise who
+ // knows what it means, so just throw it.
+ if ($e->getCode() != FacebookAPIErrorCodes::API_EC_PARAM) {
+ throw $e;
+ }
+ }
+ }
+
+ public function do_get_session($auth_token) {
+ try {
+ return $this->api_client->auth_getSession($auth_token, $this->generate_session_secret);
+ } catch (FacebookRestClientException $e) {
+ // API_EC_PARAM means we don't have a logged in user, otherwise who
+ // knows what it means, so just throw it.
+ if ($e->getCode() != FacebookAPIErrorCodes::API_EC_PARAM) {
+ throw $e;
+ }
+ }
+ }
+
+ // Invalidate the session currently being used, and clear any state associated
+ // with it. Note that the user will still remain logged into Facebook.
+ public function expire_session() {
+ if ($this->api_client->auth_expireSession()) {
+ $this->clear_cookie_state();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /** Logs the user out of all temporary application sessions as well as their
+ * Facebook session. Note this will only work if the user has a valid current
+ * session with the application.
+ *
+ * @param string $next URL to redirect to upon logging out
+ *
+ */
+ public function logout($next) {
+ $logout_url = $this->get_logout_url($next);
+
+ // Clear any stored state
+ $this->clear_cookie_state();
+
+ $this->redirect($logout_url);
+ }
+
+ /**
+ * Clears any persistent state stored about the user, including
+ * cookies and information related to the current session in the
+ * client.
+ *
+ */
+ public function clear_cookie_state() {
+ if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
+ $cookies = array('user', 'session_key', 'expires', 'ss');
+ foreach ($cookies as $name) {
+ setcookie($this->api_key . '_' . $name, false, time() - 3600);
+ unset($_COOKIE[$this->api_key . '_' . $name]);
+ }
+ setcookie($this->api_key, false, time() - 3600);
+ unset($_COOKIE[$this->api_key]);
+ }
+
+ // now, clear the rest of the stored state
+ $this->user = 0;
+ $this->api_client->session_key = 0;
+ }
+
+ public function redirect($url) {
+ if ($this->in_fb_canvas()) {
+ echo '<fb:redirect url="' . $url . '"/>';
+ } else if (preg_match('/^https?:\/\/([^\/]*\.)?facebook\.com(:\d+)?/i', $url)) {
+ // make sure facebook.com url's load in the full frame so that we don't
+ // get a frame within a frame.
+ echo "<script type=\"text/javascript\">\ntop.location.href = \"$url\";\n</script>";
+ } else {
+ header('Location: ' . $url);
+ }
+ exit;
+ }
+
+ public function in_frame() {
+ return isset($this->fb_params['in_canvas'])
+ || isset($this->fb_params['in_iframe']);
+ }
+ public function in_fb_canvas() {
+ return isset($this->fb_params['in_canvas']);
+ }
+
+ public function get_loggedin_user() {
+ return $this->user;
+ }
+
+ public function get_canvas_user() {
+ return $this->canvas_user;
+ }
+
+ public function get_profile_user() {
+ return $this->profile_user;
+ }
+
+ public static function current_url() {
+ return 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ }
+
+ // require_add and require_install have been removed.
+ // see http://developer.facebook.com/news.php?blog=1&story=116 for more details
+ public function require_login() {
+ if ($user = $this->get_loggedin_user()) {
+ return $user;
+ }
+ $this->redirect($this->get_login_url(self::current_url(), $this->in_frame()));
+ }
+
+ public function require_frame() {
+ if (!$this->in_frame()) {
+ $this->redirect($this->get_login_url(self::current_url(), true));
+ }
+ }
+
+ public static function get_facebook_url($subdomain='www') {
+ return 'http://' . $subdomain . '.facebook.com';
+ }
+
+ public function get_install_url($next=null) {
+ // this was renamed, keeping for compatibility's sake
+ return $this->get_add_url($next);
+ }
+
+ public function get_add_url($next=null) {
+ $page = self::get_facebook_url().'/add.php';
+ $params = array('api_key' => $this->api_key);
+
+ if ($next) {
+ $params['next'] = $next;
+ }
+
+ return $page . '?' . http_build_query($params);
+ }
+
+ public function get_login_url($next, $canvas) {
+ $page = self::get_facebook_url().'/login.php';
+ $params = array('api_key' => $this->api_key,
+ 'v' => '1.0');
+
+ if ($next) {
+ $params['next'] = $next;
+ }
+ if ($canvas) {
+ $params['canvas'] = '1';
+ }
+
+ return $page . '?' . http_build_query($params);
+ }
+
+ public function get_logout_url($next) {
+ $page = self::get_facebook_url().'/logout.php';
+ $params = array('app_key' => $this->api_key,
+ 'session_key' => $this->api_client->session_key);
+
+ if ($next) {
+ $params['connect_next'] = 1;
+ $params['next'] = $next;
+ }
+
+ return $page . '?' . http_build_query($params);
+ }
+
+ public function set_user($user, $session_key, $expires=null, $session_secret=null) {
+ if (!$this->in_fb_canvas() && (!isset($_COOKIE[$this->api_key . '_user'])
+ || $_COOKIE[$this->api_key . '_user'] != $user)) {
+ $this->set_cookies($user, $session_key, $expires, $session_secret);
+ }
+ $this->user = $user;
+ $this->api_client->session_key = $session_key;
+ $this->session_expires = $expires;
+ }
+
+ public function set_cookies($user, $session_key, $expires=null, $session_secret=null) {
+ $cookies = array();
+ $cookies['user'] = $user;
+ $cookies['session_key'] = $session_key;
+ if ($expires != null) {
+ $cookies['expires'] = $expires;
+ }
+ if ($session_secret != null) {
+ $cookies['ss'] = $session_secret;
+ }
+
+ foreach ($cookies as $name => $val) {
+ setcookie($this->api_key . '_' . $name, $val, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$this->api_key . '_' . $name] = $val;
+ }
+ $sig = self::generate_sig($cookies, $this->secret);
+ setcookie($this->api_key, $sig, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$this->api_key] = $sig;
+
+ if ($this->base_domain != null) {
+ $base_domain_cookie = 'base_domain_' . $this->api_key;
+ setcookie($base_domain_cookie, $this->base_domain, (int)$expires, '', $this->base_domain);
+ $_COOKIE[$base_domain_cookie] = $this->base_domain;
+ }
+ }
+
+ /**
+ * Tries to undo the badness of magic quotes as best we can
+ * @param string $val Should come directly from $_GET, $_POST, etc.
+ * @return string val without added slashes
+ */
+ public static function no_magic_quotes($val) {
+ if (get_magic_quotes_gpc()) {
+ return stripslashes($val);
+ } else {
+ return $val;
+ }
+ }
+
+ /*
+ * Get the signed parameters that were sent from Facebook. Validates the set
+ * of parameters against the included signature.
+ *
+ * Since Facebook sends data to your callback URL via unsecured means, the
+ * signature is the only way to make sure that the data actually came from
+ * Facebook. So if an app receives a request at the callback URL, it should
+ * always verify the signature that comes with against your own secret key.
+ * Otherwise, it's possible for someone to spoof a request by
+ * pretending to be someone else, i.e.:
+ * www.your-callback-url.com/?fb_user=10101
+ *
+ * This is done automatically by verify_fb_params.
+ *
+ * @param assoc $params a full array of external parameters.
+ * presumed $_GET, $_POST, or $_COOKIE
+ * @param int $timeout number of seconds that the args are good for.
+ * Specifically good for forcing cookies to expire.
+ * @param string $namespace prefix string for the set of parameters we want
+ * to verify. i.e., fb_sig or fb_post_sig
+ *
+ * @return assoc the subset of parameters containing the given prefix,
+ * and also matching the signature associated with them.
+ * OR an empty array if the params do not validate
+ */
+ public function get_valid_fb_params($params, $timeout=null, $namespace='fb_sig') {
+ $prefix = $namespace . '_';
+ $prefix_len = strlen($prefix);
+ $fb_params = array();
+ if (empty($params)) {
+ return array();
+ }
+
+ foreach ($params as $name => $val) {
+ // pull out only those parameters that match the prefix
+ // note that the signature itself ($params[$namespace]) is not in the list
+ if (strpos($name, $prefix) === 0) {
+ $fb_params[substr($name, $prefix_len)] = self::no_magic_quotes($val);
+ }
+ }
+
+ // validate that the request hasn't expired. this is most likely
+ // for params that come from $_COOKIE
+ if ($timeout && (!isset($fb_params['time']) || time() - $fb_params['time'] > $timeout)) {
+ return array();
+ }
+
+ // validate that the params match the signature
+ $signature = isset($params[$namespace]) ? $params[$namespace] : null;
+ if (!$signature || (!$this->verify_signature($fb_params, $signature))) {
+ return array();
+ }
+ return $fb_params;
+ }
+
+ /**
+ * Validates the account that a user was trying to set up an
+ * independent account through Facebook Connect.
+ *
+ * @param user The user attempting to set up an independent account.
+ * @param hash The hash passed to the reclamation URL used.
+ * @return bool True if the user is the one that selected the
+ * reclamation link.
+ */
+ public function verify_account_reclamation($user, $hash) {
+ return $hash == md5($user . $this->secret);
+ }
+
+ /**
+ * Validates that a given set of parameters match their signature.
+ * Parameters all match a given input prefix, such as "fb_sig".
+ *
+ * @param $fb_params an array of all Facebook-sent parameters,
+ * not including the signature itself
+ * @param $expected_sig the expected result to check against
+ */
+ public function verify_signature($fb_params, $expected_sig) {
+ return self::generate_sig($fb_params, $this->secret) == $expected_sig;
+ }
+
+ /**
+ * Validate the given signed public session data structure with
+ * public key of the app that
+ * the session proof belongs to.
+ *
+ * @param $signed_data the session info that is passed by another app
+ * @param string $public_key Optional public key of the app. If this
+ * is not passed, function will make an API call to get it.
+ * return true if the session proof passed verification.
+ */
+ public function verify_signed_public_session_data($signed_data,
+ $public_key = null) {
+
+ // If public key is not already provided, we need to get it through API
+ if (!$public_key) {
+ $public_key = $this->api_client->auth_getAppPublicKey(
+ $signed_data['api_key']);
+ }
+
+ // Create data to verify
+ $data_to_serialize = $signed_data;
+ unset($data_to_serialize['sig']);
+ $serialized_data = implode('_', $data_to_serialize);
+
+ // Decode signature
+ $signature = base64_decode($signed_data['sig']);
+ $result = openssl_verify($serialized_data, $signature, $public_key,
+ OPENSSL_ALGO_SHA1);
+ return $result == 1;
+ }
+
+ /*
+ * Generate a signature using the application secret key.
+ *
+ * The only two entities that know your secret key are you and Facebook,
+ * according to the Terms of Service. Since nobody else can generate
+ * the signature, you can rely on it to verify that the information
+ * came from Facebook.
+ *
+ * @param $params_array an array of all Facebook-sent parameters,
+ * NOT INCLUDING the signature itself
+ * @param $secret your app's secret key
+ *
+ * @return a hash to be checked against the signature provided by Facebook
+ */
+ public static function generate_sig($params_array, $secret) {
+ $str = '';
+
+ ksort($params_array);
+ // Note: make sure that the signature parameter is not already included in
+ // $params_array.
+ foreach ($params_array as $k=>$v) {
+ $str .= "$k=$v";
+ }
+ $str .= $secret;
+
+ return md5($str);
+ }
+
+ public function encode_validationError($summary, $message) {
+ return json_encode(
+ array('errorCode' => FACEBOOK_API_VALIDATION_ERROR,
+ 'errorTitle' => $summary,
+ 'errorMessage' => $message));
+ }
+
+ public function encode_multiFeedStory($feed, $next) {
+ return json_encode(
+ array('method' => 'multiFeedStory',
+ 'content' =>
+ array('next' => $next,
+ 'feed' => $feed)));
+ }
+
+ public function encode_feedStory($feed, $next) {
+ return json_encode(
+ array('method' => 'feedStory',
+ 'content' =>
+ array('next' => $next,
+ 'feed' => $feed)));
+ }
+
+ public function create_templatizedFeedStory($title_template, $title_data=array(),
+ $body_template='', $body_data = array(), $body_general=null,
+ $image_1=null, $image_1_link=null,
+ $image_2=null, $image_2_link=null,
+ $image_3=null, $image_3_link=null,
+ $image_4=null, $image_4_link=null) {
+ return array('title_template'=> $title_template,
+ 'title_data' => $title_data,
+ 'body_template'=> $body_template,
+ 'body_data' => $body_data,
+ 'body_general' => $body_general,
+ 'image_1' => $image_1,
+ 'image_1_link' => $image_1_link,
+ 'image_2' => $image_2,
+ 'image_2_link' => $image_2_link,
+ 'image_3' => $image_3,
+ 'image_3_link' => $image_3_link,
+ 'image_4' => $image_4,
+ 'image_4_link' => $image_4_link);
+ }
+
+
+}
+
diff --git a/plugins/Facebook/facebook/facebook_desktop.php b/plugins/Facebook/facebook/facebook_desktop.php
new file mode 100644
index 000000000..e79a2ca34
--- /dev/null
+++ b/plugins/Facebook/facebook/facebook_desktop.php
@@ -0,0 +1,104 @@
+<?php
+// Copyright 2004-2009 Facebook. All Rights Reserved.
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+//
+
+/**
+ * This class extends and modifies the "Facebook" class to better
+ * suit desktop apps.
+ */
+class FacebookDesktop extends Facebook {
+ // the application secret, which differs from the session secret
+ public $app_secret;
+ public $verify_sig;
+
+ public function __construct($api_key, $secret) {
+ $this->app_secret = $secret;
+ $this->verify_sig = false;
+ parent::__construct($api_key, $secret);
+ }
+
+ public function do_get_session($auth_token) {
+ $this->api_client->secret = $this->app_secret;
+ $this->api_client->session_key = null;
+ $session_info = parent::do_get_session($auth_token);
+ if (!empty($session_info['secret'])) {
+ // store the session secret
+ $this->set_session_secret($session_info['secret']);
+ }
+ return $session_info;
+ }
+
+ public function set_session_secret($session_secret) {
+ $this->secret = $session_secret;
+ $this->api_client->secret = $session_secret;
+ }
+
+ public function require_login() {
+ if ($this->get_loggedin_user()) {
+ try {
+ // try a session-based API call to ensure that we have the correct
+ // session secret
+ $user = $this->api_client->users_getLoggedInUser();
+
+ // now that we have a valid session secret, verify the signature
+ $this->verify_sig = true;
+ if ($this->validate_fb_params(false)) {
+ return $user;
+ } else {
+ // validation failed
+ return null;
+ }
+ } catch (FacebookRestClientException $ex) {
+ if (isset($_GET['auth_token'])) {
+ // if we have an auth_token, use it to establish a session
+ $session_info = $this->do_get_session($_GET['auth_token']);
+ if ($session_info) {
+ return $session_info['uid'];
+ }
+ }
+ }
+ }
+ // if we get here, we need to redirect the user to log in
+ $this->redirect($this->get_login_url(self::current_url(), $this->in_fb_canvas()));
+ }
+
+ public function verify_signature($fb_params, $expected_sig) {
+ // we don't want to verify the signature until we have a valid
+ // session secret
+ if ($this->verify_sig) {
+ return parent::verify_signature($fb_params, $expected_sig);
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/plugins/Facebook/facebook/facebookapi_php5_restlib.php b/plugins/Facebook/facebook/facebookapi_php5_restlib.php
new file mode 100755
index 000000000..55cb7fb86
--- /dev/null
+++ b/plugins/Facebook/facebook/facebookapi_php5_restlib.php
@@ -0,0 +1,3618 @@
+<?php
+// Copyright 2004-2009 Facebook. All Rights Reserved.
+//
+// +---------------------------------------------------------------------------+
+// | Facebook Platform PHP5 client |
+// +---------------------------------------------------------------------------+
+// | Copyright (c) 2007-2009 Facebook, Inc. |
+// | All rights reserved. |
+// | |
+// | Redistribution and use in source and binary forms, with or without |
+// | modification, are permitted provided that the following conditions |
+// | are met: |
+// | |
+// | 1. Redistributions of source code must retain the above copyright |
+// | notice, this list of conditions and the following disclaimer. |
+// | 2. Redistributions in binary form must reproduce the above copyright |
+// | notice, this list of conditions and the following disclaimer in the |
+// | documentation and/or other materials provided with the distribution. |
+// | |
+// | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
+// | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+// | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
+// | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
+// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
+// | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
+// | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+// +---------------------------------------------------------------------------+
+// | For help with this library, contact developers-help@facebook.com |
+// +---------------------------------------------------------------------------+
+//
+
+include_once 'jsonwrapper/jsonwrapper.php';
+
+class FacebookRestClient {
+ public $secret;
+ public $session_key;
+ public $api_key;
+ // to save making the friends.get api call, this will get prepopulated on
+ // canvas pages
+ public $friends_list;
+ public $user;
+ // to save making the pages.isAppAdded api call, this will get prepopulated
+ // on canvas pages
+ public $added;
+ public $is_user;
+ // we don't pass friends list to iframes, but we want to make
+ // friends_get really simple in the canvas_user (non-logged in) case.
+ // So we use the canvas_user as default arg to friends_get
+ public $canvas_user;
+ public $batch_mode;
+ private $batch_queue;
+ private $pending_batch;
+ private $call_as_apikey;
+ private $use_curl_if_available;
+ private $format = null;
+
+ const BATCH_MODE_DEFAULT = 0;
+ const BATCH_MODE_SERVER_PARALLEL = 0;
+ const BATCH_MODE_SERIAL_ONLY = 2;
+
+ /**
+ * Create the client.
+ * @param string $session_key if you haven't gotten a session key yet, leave
+ * this as null and then set it later by just
+ * directly accessing the $session_key member
+ * variable.
+ */
+ public function __construct($api_key, $secret, $session_key=null) {
+ $this->secret = $secret;
+ $this->session_key = $session_key;
+ $this->api_key = $api_key;
+ $this->batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT;
+ $this->last_call_id = 0;
+ $this->call_as_apikey = '';
+ $this->use_curl_if_available = true;
+ $this->server_addr = Facebook::get_facebook_url('api') . '/restserver.php';
+
+ if (!empty($GLOBALS['facebook_config']['debug'])) {
+ $this->cur_id = 0;
+ ?>
+<script type="text/javascript">
+var types = ['params', 'xml', 'php', 'sxml'];
+function getStyle(elem, style) {
+ if (elem.getStyle) {
+ return elem.getStyle(style);
+ } else {
+ return elem.style[style];
+ }
+}
+function setStyle(elem, style, value) {
+ if (elem.setStyle) {
+ elem.setStyle(style, value);
+ } else {
+ elem.style[style] = value;
+ }
+}
+function toggleDisplay(id, type) {
+ for (var i = 0; i < types.length; i++) {
+ var t = types[i];
+ var pre = document.getElementById(t + id);
+ if (pre) {
+ if (t != type || getStyle(pre, 'display') == 'block') {
+ setStyle(pre, 'display', 'none');
+ } else {
+ setStyle(pre, 'display', 'block');
+ }
+ }
+ }
+ return false;
+}
+</script>
+<?php
+ }
+ }
+
+ /**
+ * Set the default user id for methods that allow the caller
+ * to pass an uid parameter to identify the target user
+ * instead of a session key. This currently applies to
+ * the user preferences methods.
+ *
+ * @param $uid int the user id
+ */
+ public function set_user($uid) {
+ $this->user = $uid;
+ }
+
+ /**
+ * Normally, if the cURL library/PHP extension is available, it is used for
+ * HTTP transactions. This allows that behavior to be overridden, falling
+ * back to a vanilla-PHP implementation even if cURL is installed.
+ *
+ * @param $use_curl_if_available bool whether or not to use cURL if available
+ */
+ public function set_use_curl_if_available($use_curl_if_available) {
+ $this->use_curl_if_available = $use_curl_if_available;
+ }
+
+ /**
+ * Start a batch operation.
+ */
+ public function begin_batch() {
+ if ($this->pending_batch()) {
+ $code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ $this->batch_queue = array();
+ $this->pending_batch = true;
+ }
+
+ /*
+ * End current batch operation
+ */
+ public function end_batch() {
+ if (!$this->pending_batch()) {
+ $code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ $this->pending_batch = false;
+
+ $this->execute_server_side_batch();
+ $this->batch_queue = null;
+ }
+
+ /**
+ * are we currently queueing up calls for a batch?
+ */
+ public function pending_batch() {
+ return $this->pending_batch;
+ }
+
+ private function execute_server_side_batch() {
+ $item_count = count($this->batch_queue);
+ $method_feed = array();
+ foreach ($this->batch_queue as $batch_item) {
+ $method = $batch_item['m'];
+ $params = $batch_item['p'];
+ list($get, $post) = $this->finalize_params($method, $params);
+ $method_feed[] = $this->create_url_string(array_merge($post, $get));
+ }
+
+ $serial_only =
+ ($this->batch_mode == FacebookRestClient::BATCH_MODE_SERIAL_ONLY);
+
+ $params = array('method_feed' => json_encode($method_feed),
+ 'serial_only' => $serial_only,
+ 'format' => $this->format);
+ $result = $this->call_method('facebook.batch.run', $params);
+
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+
+ for ($i = 0; $i < $item_count; $i++) {
+ $batch_item = $this->batch_queue[$i];
+ $batch_item['p']['format'] = $this->format;
+ $batch_item_result = $this->convert_result($result[$i],
+ $batch_item['m'],
+ $batch_item['p']);
+
+ if (is_array($batch_item_result) &&
+ isset($batch_item_result['error_code'])) {
+ throw new FacebookRestClientException($batch_item_result['error_msg'],
+ $batch_item_result['error_code']);
+ }
+ $batch_item['r'] = $batch_item_result;
+ }
+ }
+
+ public function begin_permissions_mode($permissions_apikey) {
+ $this->call_as_apikey = $permissions_apikey;
+ }
+
+ public function end_permissions_mode() {
+ $this->call_as_apikey = '';
+ }
+
+
+ /*
+ * If a page is loaded via HTTPS, then all images and static
+ * resources need to be printed with HTTPS urls to avoid
+ * mixed content warnings. If your page loads with an HTTPS
+ * url, then call set_use_ssl_resources to retrieve the correct
+ * urls.
+ */
+ public function set_use_ssl_resources($is_ssl = true) {
+ $this->use_ssl_resources = $is_ssl;
+ }
+
+ /**
+ * Returns public information for an application (as shown in the application
+ * directory) by either application ID, API key, or canvas page name.
+ *
+ * @param int $application_id (Optional) app id
+ * @param string $application_api_key (Optional) api key
+ * @param string $application_canvas_name (Optional) canvas name
+ *
+ * Exactly one argument must be specified, otherwise it is an error.
+ *
+ * @return array An array of public information about the application.
+ */
+ public function application_getPublicInfo($application_id=null,
+ $application_api_key=null,
+ $application_canvas_name=null) {
+ return $this->call_method('facebook.application.getPublicInfo',
+ array('application_id' => $application_id,
+ 'application_api_key' => $application_api_key,
+ 'application_canvas_name' => $application_canvas_name));
+ }
+
+ /**
+ * Creates an authentication token to be used as part of the desktop login
+ * flow. For more information, please see
+ * http://wiki.developers.facebook.com/index.php/Auth.createToken.
+ *
+ * @return string An authentication token.
+ */
+ public function auth_createToken() {
+ return $this->call_method('facebook.auth.createToken');
+ }
+
+ /**
+ * Returns the session information available after current user logs in.
+ *
+ * @param string $auth_token the token returned by
+ * auth_createToken or passed back to
+ * your callback_url.
+ * @param bool $generate_session_secret whether the session returned should
+ * include a session secret
+ *
+ * @return array An assoc array containing session_key, uid
+ */
+ public function auth_getSession($auth_token, $generate_session_secret=false) {
+ if (!$this->pending_batch()) {
+ $result = $this->call_method('facebook.auth.getSession',
+ array('auth_token' => $auth_token,
+ 'generate_session_secret' => $generate_session_secret));
+ $this->session_key = $result['session_key'];
+
+ if (!empty($result['secret']) && !$generate_session_secret) {
+ // desktop apps have a special secret
+ $this->secret = $result['secret'];
+ }
+ return $result;
+ }
+ }
+
+ /**
+ * Generates a session-specific secret. This is for integration with
+ * client-side API calls, such as the JS library.
+ *
+ * @return array A session secret for the current promoted session
+ *
+ * @error API_EC_PARAM_SESSION_KEY
+ * API_EC_PARAM_UNKNOWN
+ */
+ public function auth_promoteSession() {
+ return $this->call_method('facebook.auth.promoteSession');
+ }
+
+ /**
+ * Expires the session that is currently being used. If this call is
+ * successful, no further calls to the API (which require a session) can be
+ * made until a valid session is created.
+ *
+ * @return bool true if session expiration was successful, false otherwise
+ */
+ public function auth_expireSession() {
+ return $this->call_method('facebook.auth.expireSession');
+ }
+
+ /**
+ * Revokes the given extended permission that the user granted at some
+ * prior time (for instance, offline_access or email). If no user is
+ * provided, it will be revoked for the user of the current session.
+ *
+ * @param string $perm The permission to revoke
+ * @param int $uid The user for whom to revoke the permission.
+ */
+ public function auth_revokeExtendedPermission($perm, $uid=null) {
+ return $this->call_method('facebook.auth.revokeExtendedPermission',
+ array('perm' => $perm, 'uid' => $uid));
+ }
+
+ /**
+ * Revokes the user's agreement to the Facebook Terms of Service for your
+ * application. If you call this method for one of your users, you will no
+ * longer be able to make API requests on their behalf until they again
+ * authorize your application. Use with care. Note that if this method is
+ * called without a user parameter, then it will revoke access for the
+ * current session's user.
+ *
+ * @param int $uid (Optional) User to revoke
+ *
+ * @return bool true if revocation succeeds, false otherwise
+ */
+ public function auth_revokeAuthorization($uid=null) {
+ return $this->call_method('facebook.auth.revokeAuthorization',
+ array('uid' => $uid));
+ }
+
+ /**
+ * Get public key that is needed to verify digital signature
+ * an app may pass to other apps. The public key is only used by
+ * other apps for verification purposes.
+ * @param string API key of an app
+ * @return string The public key for the app.
+ */
+ public function auth_getAppPublicKey($target_app_key) {
+ return $this->call_method('facebook.auth.getAppPublicKey',
+ array('target_app_key' => $target_app_key));
+ }
+
+ /**
+ * Get a structure that can be passed to another app
+ * as proof of session. The other app can verify it using public
+ * key of this app.
+ *
+ * @return signed public session data structure.
+ */
+ public function auth_getSignedPublicSessionData() {
+ return $this->call_method('facebook.auth.getSignedPublicSessionData',
+ array());
+ }
+
+ /**
+ * Returns the number of unconnected friends that exist in this application.
+ * This number is determined based on the accounts registered through
+ * connect.registerUsers() (see below).
+ */
+ public function connect_getUnconnectedFriendsCount() {
+ return $this->call_method('facebook.connect.getUnconnectedFriendsCount',
+ array());
+ }
+
+ /**
+ * This method is used to create an association between an external user
+ * account and a Facebook user account, as per Facebook Connect.
+ *
+ * This method takes an array of account data, including a required email_hash
+ * and optional account data. For each connected account, if the user exists,
+ * the information is added to the set of the user's connected accounts.
+ * If the user has already authorized the site, the connected account is added
+ * in the confirmed state. If the user has not yet authorized the site, the
+ * connected account is added in the pending state.
+ *
+ * This is designed to help Facebook Connect recognize when two Facebook
+ * friends are both members of a external site, but perhaps are not aware of
+ * it. The Connect dialog (see fb:connect-form) is used when friends can be
+ * identified through these email hashes. See the following url for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Connect.registerUsers
+ *
+ * @param mixed $accounts A (JSON-encoded) array of arrays, where each array
+ * has three properties:
+ * 'email_hash' (req) - public email hash of account
+ * 'account_id' (opt) - remote account id;
+ * 'account_url' (opt) - url to remote account;
+ *
+ * @return array The list of email hashes for the successfully registered
+ * accounts.
+ */
+ public function connect_registerUsers($accounts) {
+ return $this->call_method('facebook.connect.registerUsers',
+ array('accounts' => $accounts));
+ }
+
+ /**
+ * Unregisters a set of accounts registered using connect.registerUsers.
+ *
+ * @param array $email_hashes The (JSON-encoded) list of email hashes to be
+ * unregistered.
+ *
+ * @return array The list of email hashes which have been successfully
+ * unregistered.
+ */
+ public function connect_unregisterUsers($email_hashes) {
+ return $this->call_method('facebook.connect.unregisterUsers',
+ array('email_hashes' => $email_hashes));
+ }
+
+ /**
+ * Returns events according to the filters specified.
+ *
+ * @param int $uid (Optional) User associated with events. A null
+ * parameter will default to the session user.
+ * @param array/string $eids (Optional) Filter by these event
+ * ids. A null parameter will get all events for
+ * the user. (A csv list will work but is deprecated)
+ * @param int $start_time (Optional) Filter with this unix time as lower
+ * bound. A null or zero parameter indicates no
+ * lower bound.
+ * @param int $end_time (Optional) Filter with this UTC as upper bound.
+ * A null or zero parameter indicates no upper
+ * bound.
+ * @param string $rsvp_status (Optional) Only show events where the given uid
+ * has this rsvp status. This only works if you
+ * have specified a value for $uid. Values are as
+ * in events.getMembers. Null indicates to ignore
+ * rsvp status when filtering.
+ *
+ * @return array The events matching the query.
+ */
+ public function &events_get($uid=null,
+ $eids=null,
+ $start_time=null,
+ $end_time=null,
+ $rsvp_status=null) {
+ return $this->call_method('facebook.events.get',
+ array('uid' => $uid,
+ 'eids' => $eids,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'rsvp_status' => $rsvp_status));
+ }
+
+ /**
+ * Returns membership list data associated with an event.
+ *
+ * @param int $eid event id
+ *
+ * @return array An assoc array of four membership lists, with keys
+ * 'attending', 'unsure', 'declined', and 'not_replied'
+ */
+ public function &events_getMembers($eid) {
+ return $this->call_method('facebook.events.getMembers',
+ array('eid' => $eid));
+ }
+
+ /**
+ * RSVPs the current user to this event.
+ *
+ * @param int $eid event id
+ * @param string $rsvp_status 'attending', 'unsure', or 'declined'
+ *
+ * @return bool true if successful
+ */
+ public function &events_rsvp($eid, $rsvp_status) {
+ return $this->call_method('facebook.events.rsvp',
+ array(
+ 'eid' => $eid,
+ 'rsvp_status' => $rsvp_status));
+ }
+
+ /**
+ * Cancels an event. Only works for events where application is the admin.
+ *
+ * @param int $eid event id
+ * @param string $cancel_message (Optional) message to send to members of
+ * the event about why it is cancelled
+ *
+ * @return bool true if successful
+ */
+ public function &events_cancel($eid, $cancel_message='') {
+ return $this->call_method('facebook.events.cancel',
+ array('eid' => $eid,
+ 'cancel_message' => $cancel_message));
+ }
+
+ /**
+ * Creates an event on behalf of the user is there is a session, otherwise on
+ * behalf of app. Successful creation guarantees app will be admin.
+ *
+ * @param assoc array $event_info json encoded event information
+ * @param string $file (Optional) filename of picture to set
+ *
+ * @return int event id
+ */
+ public function events_create($event_info, $file = null) {
+ if ($file) {
+ return $this->call_upload_method('facebook.events.create',
+ array('event_info' => $event_info),
+ $file,
+ Facebook::get_facebook_url('api-photo') . '/restserver.php');
+ } else {
+ return $this->call_method('facebook.events.create',
+ array('event_info' => $event_info));
+ }
+ }
+
+ /**
+ * Edits an existing event. Only works for events where application is admin.
+ *
+ * @param int $eid event id
+ * @param assoc array $event_info json encoded event information
+ * @param string $file (Optional) filename of new picture to set
+ *
+ * @return bool true if successful
+ */
+ public function events_edit($eid, $event_info, $file = null) {
+ if ($file) {
+ return $this->call_upload_method('facebook.events.edit',
+ array('eid' => $eid, 'event_info' => $event_info),
+ $file,
+ Facebook::get_facebook_url('api-photo') . '/restserver.php');
+ } else {
+ return $this->call_method('facebook.events.edit',
+ array('eid' => $eid,
+ 'event_info' => $event_info));
+ }
+ }
+
+ /**
+ * Fetches and re-caches the image stored at the given URL, for use in images
+ * published to non-canvas pages via the API (for example, to user profiles
+ * via profile.setFBML, or to News Feed via feed.publishUserAction).
+ *
+ * @param string $url The absolute URL from which to refresh the image.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_refreshImgSrc($url) {
+ return $this->call_method('facebook.fbml.refreshImgSrc',
+ array('url' => $url));
+ }
+
+ /**
+ * Fetches and re-caches the content stored at the given URL, for use in an
+ * fb:ref FBML tag.
+ *
+ * @param string $url The absolute URL from which to fetch content. This URL
+ * should be used in a fb:ref FBML tag.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_refreshRefUrl($url) {
+ return $this->call_method('facebook.fbml.refreshRefUrl',
+ array('url' => $url));
+ }
+
+ /**
+ * Lets you insert text strings in their native language into the Facebook
+ * Translations database so they can be translated.
+ *
+ * @param array $native_strings An array of maps, where each map has a 'text'
+ * field and a 'description' field.
+ *
+ * @return int Number of strings uploaded.
+ */
+ public function &fbml_uploadNativeStrings($native_strings) {
+ return $this->call_method('facebook.fbml.uploadNativeStrings',
+ array('native_strings' => json_encode($native_strings)));
+ }
+
+ /**
+ * Associates a given "handle" with FBML markup so that the handle can be
+ * used within the fb:ref FBML tag. A handle is unique within an application
+ * and allows an application to publish identical FBML to many user profiles
+ * and do subsequent updates without having to republish FBML on behalf of
+ * each user.
+ *
+ * @param string $handle The handle to associate with the given FBML.
+ * @param string $fbml The FBML to associate with the given handle.
+ *
+ * @return bool true on success
+ */
+ public function &fbml_setRefHandle($handle, $fbml) {
+ return $this->call_method('facebook.fbml.setRefHandle',
+ array('handle' => $handle, 'fbml' => $fbml));
+ }
+
+ /**
+ * Register custom tags for the application. Custom tags can be used
+ * to extend the set of tags available to applications in FBML
+ * markup.
+ *
+ * Before you call this function,
+ * make sure you read the full documentation at
+ *
+ * http://wiki.developers.facebook.com/index.php/Fbml.RegisterCustomTags
+ *
+ * IMPORTANT: This function overwrites the values of
+ * existing tags if the names match. Use this function with care because
+ * it may break the FBML of any application that is using the
+ * existing version of the tags.
+ *
+ * @param mixed $tags an array of tag objects (the full description is on the
+ * wiki page)
+ *
+ * @return int the number of tags that were registered
+ */
+ public function &fbml_registerCustomTags($tags) {
+ $tags = json_encode($tags);
+ return $this->call_method('facebook.fbml.registerCustomTags',
+ array('tags' => $tags));
+ }
+
+ /**
+ * Get the custom tags for an application. If $app_id
+ * is not specified, the calling app's tags are returned.
+ * If $app_id is different from the id of the calling app,
+ * only the app's public tags are returned.
+ * The return value is an array of the same type as
+ * the $tags parameter of fbml_registerCustomTags().
+ *
+ * @param int $app_id the application's id (optional)
+ *
+ * @return mixed an array containing the custom tag objects
+ */
+ public function &fbml_getCustomTags($app_id = null) {
+ return $this->call_method('facebook.fbml.getCustomTags',
+ array('app_id' => $app_id));
+ }
+
+
+ /**
+ * Delete custom tags the application has registered. If
+ * $tag_names is null, all the application's custom tags will be
+ * deleted.
+ *
+ * IMPORTANT: If your application has registered public tags
+ * that other applications may be using, don't delete those tags!
+ * Doing so can break the FBML ofapplications that are using them.
+ *
+ * @param array $tag_names the names of the tags to delete (optinal)
+ * @return bool true on success
+ */
+ public function &fbml_deleteCustomTags($tag_names = null) {
+ return $this->call_method('facebook.fbml.deleteCustomTags',
+ array('tag_names' => json_encode($tag_names)));
+ }
+
+
+
+ /**
+ * This method is deprecated for calls made on behalf of users. This method
+ * works only for publishing stories on a Facebook Page that has installed
+ * your application. To publish stories to a user's profile, use
+ * feed.publishUserAction instead.
+ *
+ * For more details on this call, please visit the wiki page:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction
+ */
+ public function &feed_publishTemplatizedAction($title_template,
+ $title_data,
+ $body_template,
+ $body_data,
+ $body_general,
+ $image_1=null,
+ $image_1_link=null,
+ $image_2=null,
+ $image_2_link=null,
+ $image_3=null,
+ $image_3_link=null,
+ $image_4=null,
+ $image_4_link=null,
+ $target_ids='',
+ $page_actor_id=null) {
+ return $this->call_method('facebook.feed.publishTemplatizedAction',
+ array('title_template' => $title_template,
+ 'title_data' => $title_data,
+ 'body_template' => $body_template,
+ 'body_data' => $body_data,
+ 'body_general' => $body_general,
+ 'image_1' => $image_1,
+ 'image_1_link' => $image_1_link,
+ 'image_2' => $image_2,
+ 'image_2_link' => $image_2_link,
+ 'image_3' => $image_3,
+ 'image_3_link' => $image_3_link,
+ 'image_4' => $image_4,
+ 'image_4_link' => $image_4_link,
+ 'target_ids' => $target_ids,
+ 'page_actor_id' => $page_actor_id));
+ }
+
+ /**
+ * Registers a template bundle. Template bundles are somewhat involved, so
+ * it's recommended you check out the wiki for more details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.registerTemplateBundle
+ *
+ * @return string A template bundle id
+ */
+ public function &feed_registerTemplateBundle($one_line_story_templates,
+ $short_story_templates = array(),
+ $full_story_template = null,
+ $action_links = array()) {
+
+ $one_line_story_templates = json_encode($one_line_story_templates);
+
+ if (!empty($short_story_templates)) {
+ $short_story_templates = json_encode($short_story_templates);
+ }
+
+ if (isset($full_story_template)) {
+ $full_story_template = json_encode($full_story_template);
+ }
+
+ if (isset($action_links)) {
+ $action_links = json_encode($action_links);
+ }
+
+ return $this->call_method('facebook.feed.registerTemplateBundle',
+ array('one_line_story_templates' => $one_line_story_templates,
+ 'short_story_templates' => $short_story_templates,
+ 'full_story_template' => $full_story_template,
+ 'action_links' => $action_links));
+ }
+
+ /**
+ * Retrieves the full list of active template bundles registered by the
+ * requesting application.
+ *
+ * @return array An array of template bundles
+ */
+ public function &feed_getRegisteredTemplateBundles() {
+ return $this->call_method('facebook.feed.getRegisteredTemplateBundles',
+ array());
+ }
+
+ /**
+ * Retrieves information about a specified template bundle previously
+ * registered by the requesting application.
+ *
+ * @param string $template_bundle_id The template bundle id
+ *
+ * @return array Template bundle
+ */
+ public function &feed_getRegisteredTemplateBundleByID($template_bundle_id) {
+ return $this->call_method('facebook.feed.getRegisteredTemplateBundleByID',
+ array('template_bundle_id' => $template_bundle_id));
+ }
+
+ /**
+ * Deactivates a previously registered template bundle.
+ *
+ * @param string $template_bundle_id The template bundle id
+ *
+ * @return bool true on success
+ */
+ public function &feed_deactivateTemplateBundleByID($template_bundle_id) {
+ return $this->call_method('facebook.feed.deactivateTemplateBundleByID',
+ array('template_bundle_id' => $template_bundle_id));
+ }
+
+ const STORY_SIZE_ONE_LINE = 1;
+ const STORY_SIZE_SHORT = 2;
+ const STORY_SIZE_FULL = 4;
+
+ /**
+ * Publishes a story on behalf of the user owning the session, using the
+ * specified template bundle. This method requires an active session key in
+ * order to be called.
+ *
+ * The parameters to this method ($templata_data in particular) are somewhat
+ * involved. It's recommended you visit the wiki for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
+ *
+ * @param int $template_bundle_id A template bundle id previously registered
+ * @param array $template_data See wiki article for syntax
+ * @param array $target_ids (Optional) An array of friend uids of the
+ * user who shared in this action.
+ * @param string $body_general (Optional) Additional markup that extends
+ * the body of a short story.
+ * @param int $story_size (Optional) A story size (see above)
+ * @param string $user_message (Optional) A user message for a short
+ * story.
+ *
+ * @return bool true on success
+ */
+ public function &feed_publishUserAction(
+ $template_bundle_id, $template_data, $target_ids='', $body_general='',
+ $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE,
+ $user_message='') {
+
+ if (is_array($template_data)) {
+ $template_data = json_encode($template_data);
+ } // allow client to either pass in JSON or an assoc that we JSON for them
+
+ if (is_array($target_ids)) {
+ $target_ids = json_encode($target_ids);
+ $target_ids = trim($target_ids, "[]"); // we don't want square brackets
+ }
+
+ return $this->call_method('facebook.feed.publishUserAction',
+ array('template_bundle_id' => $template_bundle_id,
+ 'template_data' => $template_data,
+ 'target_ids' => $target_ids,
+ 'body_general' => $body_general,
+ 'story_size' => $story_size,
+ 'user_message' => $user_message));
+ }
+
+
+ /**
+ * Publish a post to the user's stream.
+ *
+ * @param $message the user's message
+ * @param $attachment the post's attachment (optional)
+ * @param $action links the post's action links (optional)
+ * @param $target_id the user on whose wall the post will be posted
+ * (optional)
+ * @param $uid the actor (defaults to session user)
+ * @return string the post id
+ */
+ public function stream_publish(
+ $message, $attachment = null, $action_links = null, $target_id = null,
+ $uid = null) {
+
+ return $this->call_method(
+ 'facebook.stream.publish',
+ array('message' => $message,
+ 'attachment' => $attachment,
+ 'action_links' => $action_links,
+ 'target_id' => $target_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Remove a post from the user's stream.
+ * Currently, you may only remove stories you application created.
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_remove($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.remove',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Add a comment to a stream post
+ *
+ * @param $post_id the post id
+ * @param $comment the comment text
+ * @param $uid the actor (defaults to session user)
+ * @return string the id of the created comment
+ */
+ public function stream_addComment($post_id, $comment, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.addComment',
+ array('post_id' => $post_id,
+ 'comment' => $comment,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+
+ /**
+ * Remove a comment from a stream post
+ *
+ * @param $comment_id the comment id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_removeComment($comment_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.removeComment',
+ array('comment_id' => $comment_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Add a like to a stream post
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_addLike($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.addLike',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Remove a like from a stream post
+ *
+ * @param $post_id the post id
+ * @param $uid the actor (defaults to session user)
+ * @return bool
+ */
+ public function stream_removeLike($post_id, $uid = null) {
+ return $this->call_method(
+ 'facebook.stream.removeLike',
+ array('post_id' => $post_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * For the current user, retrieves stories generated by the user's friends
+ * while using this application. This can be used to easily create a
+ * "News Feed" like experience.
+ *
+ * @return array An array of feed story objects.
+ */
+ public function &feed_getAppFriendStories() {
+ return $this->call_method('facebook.feed.getAppFriendStories');
+ }
+
+ /**
+ * Makes an FQL query. This is a generalized way of accessing all the data
+ * in the API, as an alternative to most of the other method calls. More
+ * info at http://wiki.developers.facebook.com/index.php/FQL
+ *
+ * @param string $query the query to evaluate
+ *
+ * @return array generalized array representing the results
+ */
+ public function &fql_query($query) {
+ return $this->call_method('facebook.fql.query',
+ array('query' => $query));
+ }
+
+ /**
+ * Makes a set of FQL queries in parallel. This method takes a dictionary
+ * of FQL queries where the keys are names for the queries. Results from
+ * one query can be used within another query to fetch additional data. More
+ * info about FQL queries at http://wiki.developers.facebook.com/index.php/FQL
+ *
+ * @param string $queries JSON-encoded dictionary of queries to evaluate
+ *
+ * @return array generalized array representing the results
+ */
+ public function &fql_multiquery($queries) {
+ return $this->call_method('facebook.fql.multiquery',
+ array('queries' => $queries));
+ }
+
+ /**
+ * Returns whether or not pairs of users are friends.
+ * Note that the Facebook friend relationship is symmetric.
+ *
+ * @param array/string $uids1 list of ids (id_1, id_2,...)
+ * of some length X (csv is deprecated)
+ * @param array/string $uids2 list of ids (id_A, id_B,...)
+ * of SAME length X (csv is deprecated)
+ *
+ * @return array An array with uid1, uid2, and bool if friends, e.g.:
+ * array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1),
+ * 1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0)
+ * ...)
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
+ */
+ public function &friends_areFriends($uids1, $uids2) {
+ return $this->call_method('facebook.friends.areFriends',
+ array('uids1' => $uids1,
+ 'uids2' => $uids2));
+ }
+
+ /**
+ * Returns the friends of the current session user.
+ *
+ * @param int $flid (Optional) Only return friends on this friend list.
+ * @param int $uid (Optional) Return friends for this user.
+ *
+ * @return array An array of friends
+ */
+ public function &friends_get($flid=null, $uid = null) {
+ if (isset($this->friends_list)) {
+ return $this->friends_list;
+ }
+ $params = array();
+ if (!$uid && isset($this->canvas_user)) {
+ $uid = $this->canvas_user;
+ }
+ if ($uid) {
+ $params['uid'] = $uid;
+ }
+ if ($flid) {
+ $params['flid'] = $flid;
+ }
+ return $this->call_method('facebook.friends.get', $params);
+
+ }
+
+ /**
+ * Returns the mutual friends between the target uid and a source uid or
+ * the current session user.
+ *
+ * @param int $target_uid Target uid for which mutual friends will be found.
+ * @param int $source_uid (optional) Source uid for which mutual friends will
+ * be found. If no source_uid is specified,
+ * source_id will default to the session
+ * user.
+ * @return array An array of friend uids
+ */
+ public function &friends_getMutualFriends($target_uid, $source_uid = null) {
+ return $this->call_method('facebook.friends.getMutualFriends',
+ array("target_uid" => $target_uid,
+ "source_uid" => $source_uid));
+ }
+
+ /**
+ * Returns the set of friend lists for the current session user.
+ *
+ * @return array An array of friend list objects
+ */
+ public function &friends_getLists() {
+ return $this->call_method('facebook.friends.getLists');
+ }
+
+ /**
+ * Returns the friends of the session user, who are also users
+ * of the calling application.
+ *
+ * @return array An array of friends also using the app
+ */
+ public function &friends_getAppUsers() {
+ return $this->call_method('facebook.friends.getAppUsers');
+ }
+
+ /**
+ * Returns groups according to the filters specified.
+ *
+ * @param int $uid (Optional) User associated with groups. A null
+ * parameter will default to the session user.
+ * @param array/string $gids (Optional) Array of group ids to query. A null
+ * parameter will get all groups for the user.
+ * (csv is deprecated)
+ *
+ * @return array An array of group objects
+ */
+ public function &groups_get($uid, $gids) {
+ return $this->call_method('facebook.groups.get',
+ array('uid' => $uid,
+ 'gids' => $gids));
+ }
+
+ /**
+ * Returns the membership list of a group.
+ *
+ * @param int $gid Group id
+ *
+ * @return array An array with four membership lists, with keys 'members',
+ * 'admins', 'officers', and 'not_replied'
+ */
+ public function &groups_getMembers($gid) {
+ return $this->call_method('facebook.groups.getMembers',
+ array('gid' => $gid));
+ }
+
+ /**
+ * Returns cookies according to the filters specified.
+ *
+ * @param int $uid User for which the cookies are needed.
+ * @param string $name (Optional) A null parameter will get all cookies
+ * for the user.
+ *
+ * @return array Cookies! Nom nom nom nom nom.
+ */
+ public function data_getCookies($uid, $name) {
+ return $this->call_method('facebook.data.getCookies',
+ array('uid' => $uid,
+ 'name' => $name));
+ }
+
+ /**
+ * Sets cookies according to the params specified.
+ *
+ * @param int $uid User for which the cookies are needed.
+ * @param string $name Name of the cookie
+ * @param string $value (Optional) if expires specified and is in the past
+ * @param int $expires (Optional) Expiry time
+ * @param string $path (Optional) Url path to associate with (default is /)
+ *
+ * @return bool true on success
+ */
+ public function data_setCookie($uid, $name, $value, $expires, $path) {
+ return $this->call_method('facebook.data.setCookie',
+ array('uid' => $uid,
+ 'name' => $name,
+ 'value' => $value,
+ 'expires' => $expires,
+ 'path' => $path));
+ }
+
+ /**
+ * Retrieves links posted by the given user.
+ *
+ * @param int $uid The user whose links you wish to retrieve
+ * @param int $limit The maximimum number of links to retrieve
+ * @param array $link_ids (Optional) Array of specific link
+ * IDs to retrieve by this user
+ *
+ * @return array An array of links.
+ */
+ public function &links_get($uid, $limit, $link_ids = null) {
+ return $this->call_method('links.get',
+ array('uid' => $uid,
+ 'limit' => $limit,
+ 'link_ids' => $link_ids));
+ }
+
+ /**
+ * Posts a link on Facebook.
+ *
+ * @param string $url URL/link you wish to post
+ * @param string $comment (Optional) A comment about this link
+ * @param int $uid (Optional) User ID that is posting this link;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &links_post($url, $comment='', $uid = null) {
+ return $this->call_method('links.post',
+ array('uid' => $uid,
+ 'url' => $url,
+ 'comment' => $comment));
+ }
+
+ /**
+ * Permissions API
+ */
+
+ /**
+ * Checks API-access granted by self to the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_checkGrantedApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.checkGrantedApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Checks API-access granted to self by the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_checkAvailableApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.checkAvailableApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Grant API-access to the specified methods/namespaces to the specified
+ * application.
+ *
+ * @param string $permissions_apikey Other application key
+ * @param array(string) $method_arr (Optional) API methods/namespaces
+ * allowed
+ *
+ * @return array API methods/namespaces which are allowed access
+ */
+ public function permissions_grantApiAccess($permissions_apikey, $method_arr) {
+ return $this->call_method('facebook.permissions.grantApiAccess',
+ array('permissions_apikey' => $permissions_apikey,
+ 'method_arr' => $method_arr));
+ }
+
+ /**
+ * Revoke API-access granted to the specified application.
+ *
+ * @param string $permissions_apikey Other application key
+ *
+ * @return bool true on success
+ */
+ public function permissions_revokeApiAccess($permissions_apikey) {
+ return $this->call_method('facebook.permissions.revokeApiAccess',
+ array('permissions_apikey' => $permissions_apikey));
+ }
+
+ /**
+ * Payments Order API
+ */
+
+ /**
+ * Set Payments properties for an app.
+ *
+ * @param properties a map from property names to values
+ * @return true on success
+ */
+ public function payments_setProperties($properties) {
+ return $this->call_method ('facebook.payments.setProperties',
+ array('properties' => json_encode($properties)));
+ }
+
+ public function payments_getOrderDetails($order_id) {
+ return json_decode($this->call_method(
+ 'facebook.payments.getOrderDetails',
+ array('order_id' => $order_id)), true);
+ }
+
+ public function payments_updateOrder($order_id, $status,
+ $params) {
+ return $this->call_method('facebook.payments.updateOrder',
+ array('order_id' => $order_id,
+ 'status' => $status,
+ 'params' => json_encode($params)));
+ }
+
+ public function payments_getOrders($status, $start_time,
+ $end_time, $test_mode=false) {
+ return json_decode($this->call_method('facebook.payments.getOrders',
+ array('status' => $status,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'test_mode' => $test_mode)), true);
+ }
+
+ /**
+ * Creates a note with the specified title and content.
+ *
+ * @param string $title Title of the note.
+ * @param string $content Content of the note.
+ * @param int $uid (Optional) The user for whom you are creating a
+ * note; defaults to current session user
+ *
+ * @return int The ID of the note that was just created.
+ */
+ public function &notes_create($title, $content, $uid = null) {
+ return $this->call_method('notes.create',
+ array('uid' => $uid,
+ 'title' => $title,
+ 'content' => $content));
+ }
+
+ /**
+ * Deletes the specified note.
+ *
+ * @param int $note_id ID of the note you wish to delete
+ * @param int $uid (Optional) Owner of the note you wish to delete;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &notes_delete($note_id, $uid = null) {
+ return $this->call_method('notes.delete',
+ array('uid' => $uid,
+ 'note_id' => $note_id));
+ }
+
+ /**
+ * Edits a note, replacing its title and contents with the title
+ * and contents specified.
+ *
+ * @param int $note_id ID of the note you wish to edit
+ * @param string $title Replacement title for the note
+ * @param string $content Replacement content for the note
+ * @param int $uid (Optional) Owner of the note you wish to edit;
+ * defaults to current session user
+ *
+ * @return bool
+ */
+ public function &notes_edit($note_id, $title, $content, $uid = null) {
+ return $this->call_method('notes.edit',
+ array('uid' => $uid,
+ 'note_id' => $note_id,
+ 'title' => $title,
+ 'content' => $content));
+ }
+
+ /**
+ * Retrieves all notes by a user. If note_ids are specified,
+ * retrieves only those specific notes by that user.
+ *
+ * @param int $uid User whose notes you wish to retrieve
+ * @param array $note_ids (Optional) List of specific note
+ * IDs by this user to retrieve
+ *
+ * @return array A list of all of the given user's notes, or an empty list
+ * if the viewer lacks permissions or if there are no visible
+ * notes.
+ */
+ public function &notes_get($uid, $note_ids = null) {
+ return $this->call_method('notes.get',
+ array('uid' => $uid,
+ 'note_ids' => $note_ids));
+ }
+
+
+ /**
+ * Returns the outstanding notifications for the session user.
+ *
+ * @return array An assoc array of notification count objects for
+ * 'messages', 'pokes' and 'shares', a uid list of
+ * 'friend_requests', a gid list of 'group_invites',
+ * and an eid list of 'event_invites'
+ */
+ public function &notifications_get() {
+ return $this->call_method('facebook.notifications.get');
+ }
+
+ /**
+ * Sends a notification to the specified users.
+ *
+ * @return A comma separated list of successful recipients
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
+ */
+ public function &notifications_send($to_ids, $notification, $type) {
+ return $this->call_method('facebook.notifications.send',
+ array('to_ids' => $to_ids,
+ 'notification' => $notification,
+ 'type' => $type));
+ }
+
+ /**
+ * Sends an email to the specified user of the application.
+ *
+ * @param array/string $recipients array of ids of the recipients (csv is deprecated)
+ * @param string $subject subject of the email
+ * @param string $text (plain text) body of the email
+ * @param string $fbml fbml markup for an html version of the email
+ *
+ * @return string A comma separated list of successful recipients
+ * @error
+ * API_EC_PARAM_USER_ID_LIST
+ */
+ public function &notifications_sendEmail($recipients,
+ $subject,
+ $text,
+ $fbml) {
+ return $this->call_method('facebook.notifications.sendEmail',
+ array('recipients' => $recipients,
+ 'subject' => $subject,
+ 'text' => $text,
+ 'fbml' => $fbml));
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of pages.
+ *
+ * @param array/string $page_ids an array of page ids (csv is deprecated)
+ * @param array/string $fields an array of strings describing the
+ * info fields desired (csv is deprecated)
+ * @param int $uid (Optional) limit results to pages of which this
+ * user is a fan.
+ * @param string type limits results to a particular type of page.
+ *
+ * @return array An array of pages
+ */
+ public function &pages_getInfo($page_ids, $fields, $uid, $type) {
+ return $this->call_method('facebook.pages.getInfo',
+ array('page_ids' => $page_ids,
+ 'fields' => $fields,
+ 'uid' => $uid,
+ 'type' => $type));
+ }
+
+ /**
+ * Returns true if the given user is an admin for the passed page.
+ *
+ * @param int $page_id target page id
+ * @param int $uid (Optional) user id (defaults to the logged-in user)
+ *
+ * @return bool true on success
+ */
+ public function &pages_isAdmin($page_id, $uid = null) {
+ return $this->call_method('facebook.pages.isAdmin',
+ array('page_id' => $page_id,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Returns whether or not the given page has added the application.
+ *
+ * @param int $page_id target page id
+ *
+ * @return bool true on success
+ */
+ public function &pages_isAppAdded($page_id) {
+ return $this->call_method('facebook.pages.isAppAdded',
+ array('page_id' => $page_id));
+ }
+
+ /**
+ * Returns true if logged in user is a fan for the passed page.
+ *
+ * @param int $page_id target page id
+ * @param int $uid user to compare. If empty, the logged in user.
+ *
+ * @return bool true on success
+ */
+ public function &pages_isFan($page_id, $uid = null) {
+ return $this->call_method('facebook.pages.isFan',
+ array('page_id' => $page_id,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Adds a tag with the given information to a photo. See the wiki for details:
+ *
+ * http://wiki.developers.facebook.com/index.php/Photos.addTag
+ *
+ * @param int $pid The ID of the photo to be tagged
+ * @param int $tag_uid The ID of the user being tagged. You must specify
+ * either the $tag_uid or the $tag_text parameter
+ * (unless $tags is specified).
+ * @param string $tag_text Some text identifying the person being tagged.
+ * You must specify either the $tag_uid or $tag_text
+ * parameter (unless $tags is specified).
+ * @param float $x The horizontal position of the tag, as a
+ * percentage from 0 to 100, from the left of the
+ * photo.
+ * @param float $y The vertical position of the tag, as a percentage
+ * from 0 to 100, from the top of the photo.
+ * @param array $tags (Optional) An array of maps, where each map
+ * can contain the tag_uid, tag_text, x, and y
+ * parameters defined above. If specified, the
+ * individual arguments are ignored.
+ * @param int $owner_uid (Optional) The user ID of the user whose photo
+ * you are tagging. If this parameter is not
+ * specified, then it defaults to the session user.
+ *
+ * @return bool true on success
+ */
+ public function &photos_addTag($pid,
+ $tag_uid,
+ $tag_text,
+ $x,
+ $y,
+ $tags,
+ $owner_uid=0) {
+ return $this->call_method('facebook.photos.addTag',
+ array('pid' => $pid,
+ 'tag_uid' => $tag_uid,
+ 'tag_text' => $tag_text,
+ 'x' => $x,
+ 'y' => $y,
+ 'tags' => (is_array($tags)) ? json_encode($tags) : null,
+ 'owner_uid' => $this->get_uid($owner_uid)));
+ }
+
+ /**
+ * Creates and returns a new album owned by the specified user or the current
+ * session user.
+ *
+ * @param string $name The name of the album.
+ * @param string $description (Optional) A description of the album.
+ * @param string $location (Optional) A description of the location.
+ * @param string $visible (Optional) A privacy setting for the album.
+ * One of 'friends', 'friends-of-friends',
+ * 'networks', or 'everyone'. Default 'everyone'.
+ * @param int $uid (Optional) User id for creating the album; if
+ * not specified, the session user is used.
+ *
+ * @return array An album object
+ */
+ public function &photos_createAlbum($name,
+ $description='',
+ $location='',
+ $visible='',
+ $uid=0) {
+ return $this->call_method('facebook.photos.createAlbum',
+ array('name' => $name,
+ 'description' => $description,
+ 'location' => $location,
+ 'visible' => $visible,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Returns photos according to the filters specified.
+ *
+ * @param int $subj_id (Optional) Filter by uid of user tagged in the photos.
+ * @param int $aid (Optional) Filter by an album, as returned by
+ * photos_getAlbums.
+ * @param array/string $pids (Optional) Restrict to an array of pids
+ * (csv is deprecated)
+ *
+ * Note that at least one of these parameters needs to be specified, or an
+ * error is returned.
+ *
+ * @return array An array of photo objects.
+ */
+ public function &photos_get($subj_id, $aid, $pids) {
+ return $this->call_method('facebook.photos.get',
+ array('subj_id' => $subj_id, 'aid' => $aid, 'pids' => $pids));
+ }
+
+ /**
+ * Returns the albums created by the given user.
+ *
+ * @param int $uid (Optional) The uid of the user whose albums you want.
+ * A null will return the albums of the session user.
+ * @param string $aids (Optional) An array of aids to restrict
+ * the query. (csv is deprecated)
+ *
+ * Note that at least one of the (uid, aids) parameters must be specified.
+ *
+ * @returns an array of album objects.
+ */
+ public function &photos_getAlbums($uid, $aids) {
+ return $this->call_method('facebook.photos.getAlbums',
+ array('uid' => $uid,
+ 'aids' => $aids));
+ }
+
+ /**
+ * Returns the tags on all photos specified.
+ *
+ * @param string $pids A list of pids to query
+ *
+ * @return array An array of photo tag objects, which include pid,
+ * subject uid, and two floating-point numbers (xcoord, ycoord)
+ * for tag pixel location.
+ */
+ public function &photos_getTags($pids) {
+ return $this->call_method('facebook.photos.getTags',
+ array('pids' => $pids));
+ }
+
+ /**
+ * Uploads a photo.
+ *
+ * @param string $file The location of the photo on the local filesystem.
+ * @param int $aid (Optional) The album into which to upload the
+ * photo.
+ * @param string $caption (Optional) A caption for the photo.
+ * @param int uid (Optional) The user ID of the user whose photo you
+ * are uploading
+ *
+ * @return array An array of user objects
+ */
+ public function photos_upload($file, $aid=null, $caption=null, $uid=null) {
+ return $this->call_upload_method('facebook.photos.upload',
+ array('aid' => $aid,
+ 'caption' => $caption,
+ 'uid' => $uid),
+ $file);
+ }
+
+
+ /**
+ * Uploads a video.
+ *
+ * @param string $file The location of the video on the local filesystem.
+ * @param string $title (Optional) A title for the video. Titles over 65 characters in length will be truncated.
+ * @param string $description (Optional) A description for the video.
+ *
+ * @return array An array with the video's ID, title, description, and a link to view it on Facebook.
+ */
+ public function video_upload($file, $title=null, $description=null) {
+ return $this->call_upload_method('facebook.video.upload',
+ array('title' => $title,
+ 'description' => $description),
+ $file,
+ Facebook::get_facebook_url('api-video') . '/restserver.php');
+ }
+
+ /**
+ * Returns an array with the video limitations imposed on the current session's
+ * associated user. Maximum length is measured in seconds; maximum size is
+ * measured in bytes.
+ *
+ * @return array Array with "length" and "size" keys
+ */
+ public function &video_getUploadLimits() {
+ return $this->call_method('facebook.video.getUploadLimits');
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of users.
+ *
+ * @param array/string $uids An array of user ids (csv is deprecated)
+ * @param array/string $fields An array of info field names desired (csv is deprecated)
+ *
+ * @return array An array of user objects
+ */
+ public function &users_getInfo($uids, $fields) {
+ return $this->call_method('facebook.users.getInfo',
+ array('uids' => $uids,
+ 'fields' => $fields));
+ }
+
+ /**
+ * Returns the requested info fields for the requested set of users. A
+ * session key must not be specified. Only data about users that have
+ * authorized your application will be returned.
+ *
+ * Check the wiki for fields that can be queried through this API call.
+ * Data returned from here should not be used for rendering to application
+ * users, use users.getInfo instead, so that proper privacy rules will be
+ * applied.
+ *
+ * @param array/string $uids An array of user ids (csv is deprecated)
+ * @param array/string $fields An array of info field names desired (csv is deprecated)
+ *
+ * @return array An array of user objects
+ */
+ public function &users_getStandardInfo($uids, $fields) {
+ return $this->call_method('facebook.users.getStandardInfo',
+ array('uids' => $uids,
+ 'fields' => $fields));
+ }
+
+ /**
+ * Returns the user corresponding to the current session object.
+ *
+ * @return integer User id
+ */
+ public function &users_getLoggedInUser() {
+ return $this->call_method('facebook.users.getLoggedInUser');
+ }
+
+ /**
+ * Returns 1 if the user has the specified permission, 0 otherwise.
+ * http://wiki.developers.facebook.com/index.php/Users.hasAppPermission
+ *
+ * @return integer 1 or 0
+ */
+ public function &users_hasAppPermission($ext_perm, $uid=null) {
+ return $this->call_method('facebook.users.hasAppPermission',
+ array('ext_perm' => $ext_perm, 'uid' => $uid));
+ }
+
+ /**
+ * Returns whether or not the user corresponding to the current
+ * session object has the give the app basic authorization.
+ *
+ * @return boolean true if the user has authorized the app
+ */
+ public function &users_isAppUser($uid=null) {
+ if ($uid === null && isset($this->is_user)) {
+ return $this->is_user;
+ }
+
+ return $this->call_method('facebook.users.isAppUser', array('uid' => $uid));
+ }
+
+ /**
+ * Returns whether or not the user corresponding to the current
+ * session object is verified by Facebook. See the documentation
+ * for Users.isVerified for details.
+ *
+ * @return boolean true if the user is verified
+ */
+ public function &users_isVerified() {
+ return $this->call_method('facebook.users.isVerified');
+ }
+
+ /**
+ * Sets the users' current status message. Message does NOT contain the
+ * word "is" , so make sure to include a verb.
+ *
+ * Example: setStatus("is loving the API!")
+ * will produce the status "Luke is loving the API!"
+ *
+ * @param string $status text-only message to set
+ * @param int $uid user to set for (defaults to the
+ * logged-in user)
+ * @param bool $clear whether or not to clear the status,
+ * instead of setting it
+ * @param bool $status_includes_verb if true, the word "is" will *not* be
+ * prepended to the status message
+ *
+ * @return boolean
+ */
+ public function &users_setStatus($status,
+ $uid = null,
+ $clear = false,
+ $status_includes_verb = true) {
+ $args = array(
+ 'status' => $status,
+ 'uid' => $uid,
+ 'clear' => $clear,
+ 'status_includes_verb' => $status_includes_verb,
+ );
+ return $this->call_method('facebook.users.setStatus', $args);
+ }
+
+ /**
+ * Gets the comments for a particular xid. This is essentially a wrapper
+ * around the comment FQL table.
+ *
+ * @param string $xid external id associated with the comments
+ *
+ * @return array of comment objects
+ */
+ public function &comments_get($xid) {
+ $args = array('xid' => $xid);
+ return $this->call_method('facebook.comments.get', $args);
+ }
+
+ /**
+ * Add a comment to a particular xid on behalf of a user. If called
+ * without an app_secret (with session secret), this will only work
+ * for the session user.
+ *
+ * @param string $xid external id associated with the comments
+ * @param string $text text of the comment
+ * @param int $uid user adding the comment (def: session user)
+ * @param string $title optional title for the stream story
+ * @param string $url optional url for the stream story
+ * @param bool $publish_to_stream publish a feed story about this comment?
+ * a link will be generated to title/url in the story
+ *
+ * @return string comment_id associated with the comment
+ */
+ public function &comments_add($xid, $text, $uid=0, $title='', $url='',
+ $publish_to_stream=false) {
+ $args = array(
+ 'xid' => $xid,
+ 'uid' => $this->get_uid($uid),
+ 'text' => $text,
+ 'title' => $title,
+ 'url' => $url,
+ 'publish_to_stream' => $publish_to_stream);
+
+ return $this->call_method('facebook.comments.add', $args);
+ }
+
+ /**
+ * Remove a particular comment.
+ *
+ * @param string $xid the external id associated with the comments
+ * @param string $comment_id id of the comment to remove (returned by
+ * comments.add and comments.get)
+ *
+ * @return boolean
+ */
+ public function &comments_remove($xid, $comment_id) {
+ $args = array(
+ 'xid' => $xid,
+ 'comment_id' => $comment_id);
+ return $this->call_method('facebook.comments.remove', $args);
+ }
+
+ /**
+ * Gets the stream on behalf of a user using a set of users. This
+ * call will return the latest $limit queries between $start_time
+ * and $end_time.
+ *
+ * @param int $viewer_id user making the call (def: session)
+ * @param array $source_ids users/pages to look at (def: all connections)
+ * @param int $start_time start time to look for stories (def: 1 day ago)
+ * @param int $end_time end time to look for stories (def: now)
+ * @param int $limit number of stories to attempt to fetch (def: 30)
+ * @param string $filter_key key returned by stream.getFilters to fetch
+ * @param array $metadata metadata to include with the return, allows
+ * requested metadata to be returned, such as
+ * profiles, albums, photo_tags
+ *
+ * @return array(
+ * 'posts' => array of posts,
+ * // if requested, the following data may be returned
+ * 'profiles' => array of profile metadata of users/pages in posts
+ * 'albums' => array of album metadata in posts
+ * 'photo_tags' => array of photo_tags for photos in posts
+ * )
+ */
+ public function &stream_get($viewer_id = null,
+ $source_ids = null,
+ $start_time = 0,
+ $end_time = 0,
+ $limit = 30,
+ $filter_key = '') {
+ $args = array(
+ 'viewer_id' => $viewer_id,
+ 'source_ids' => $source_ids,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'limit' => $limit,
+ 'filter_key' => $filter_key);
+ return $this->call_method('facebook.stream.get', $args);
+ }
+
+ /**
+ * Gets the filters (with relevant filter keys for stream.get) for a
+ * particular user. These filters are typical things like news feed,
+ * friend lists, networks. They can be used to filter the stream
+ * without complex queries to determine which ids belong in which groups.
+ *
+ * @param int $uid user to get filters for
+ *
+ * @return array of stream filter objects
+ */
+ public function &stream_getFilters($uid = null) {
+ $args = array('uid' => $uid);
+ return $this->call_method('facebook.stream.getFilters', $args);
+ }
+
+ /**
+ * Gets the full comments given a post_id from stream.get or the
+ * stream FQL table. Initially, only a set of preview comments are
+ * returned because some posts can have many comments.
+ *
+ * @param string $post_id id of the post to get comments for
+ *
+ * @return array of comment objects
+ */
+ public function &stream_getComments($post_id) {
+ $args = array('post_id' => $post_id);
+ return $this->call_method('facebook.stream.getComments', $args);
+ }
+
+ /**
+ * Sets the FBML for the profile of the user attached to this session.
+ *
+ * @param string $markup The FBML that describes the profile
+ * presence of this app for the user
+ * @param int $uid The user
+ * @param string $profile Profile FBML
+ * @param string $profile_action Profile action FBML (deprecated)
+ * @param string $mobile_profile Mobile profile FBML
+ * @param string $profile_main Main Tab profile FBML
+ *
+ * @return array A list of strings describing any compile errors for the
+ * submitted FBML
+ */
+ function profile_setFBML($markup,
+ $uid=null,
+ $profile='',
+ $profile_action='',
+ $mobile_profile='',
+ $profile_main='') {
+ return $this->call_method('facebook.profile.setFBML',
+ array('markup' => $markup,
+ 'uid' => $uid,
+ 'profile' => $profile,
+ 'profile_action' => $profile_action,
+ 'mobile_profile' => $mobile_profile,
+ 'profile_main' => $profile_main));
+ }
+
+ /**
+ * Gets the FBML for the profile box that is currently set for a user's
+ * profile (your application set the FBML previously by calling the
+ * profile.setFBML method).
+ *
+ * @param int $uid (Optional) User id to lookup; defaults to session.
+ * @param int $type (Optional) 1 for original style, 2 for profile_main boxes
+ *
+ * @return string The FBML
+ */
+ public function &profile_getFBML($uid=null, $type=null) {
+ return $this->call_method('facebook.profile.getFBML',
+ array('uid' => $uid,
+ 'type' => $type));
+ }
+
+ /**
+ * Returns the specified user's application info section for the calling
+ * application. These info sections have either been set via a previous
+ * profile.setInfo call or by the user editing them directly.
+ *
+ * @param int $uid (Optional) User id to lookup; defaults to session.
+ *
+ * @return array Info fields for the current user. See wiki for structure:
+ *
+ * http://wiki.developers.facebook.com/index.php/Profile.getInfo
+ *
+ */
+ public function &profile_getInfo($uid=null) {
+ return $this->call_method('facebook.profile.getInfo',
+ array('uid' => $uid));
+ }
+
+ /**
+ * Returns the options associated with the specified info field for an
+ * application info section.
+ *
+ * @param string $field The title of the field
+ *
+ * @return array An array of info options.
+ */
+ public function &profile_getInfoOptions($field) {
+ return $this->call_method('facebook.profile.getInfoOptions',
+ array('field' => $field));
+ }
+
+ /**
+ * Configures an application info section that the specified user can install
+ * on the Info tab of her profile. For details on the structure of an info
+ * field, please see:
+ *
+ * http://wiki.developers.facebook.com/index.php/Profile.setInfo
+ *
+ * @param string $title Title / header of the info section
+ * @param int $type 1 for text-only, 5 for thumbnail views
+ * @param array $info_fields An array of info fields. See wiki for details.
+ * @param int $uid (Optional)
+ *
+ * @return bool true on success
+ */
+ public function &profile_setInfo($title, $type, $info_fields, $uid=null) {
+ return $this->call_method('facebook.profile.setInfo',
+ array('uid' => $uid,
+ 'type' => $type,
+ 'title' => $title,
+ 'info_fields' => json_encode($info_fields)));
+ }
+
+ /**
+ * Specifies the objects for a field for an application info section. These
+ * options populate the typeahead for a thumbnail.
+ *
+ * @param string $field The title of the field
+ * @param array $options An array of items for a thumbnail, including
+ * 'label', 'link', and optionally 'image',
+ * 'description' and 'sublabel'
+ *
+ * @return bool true on success
+ */
+ public function profile_setInfoOptions($field, $options) {
+ return $this->call_method('facebook.profile.setInfoOptions',
+ array('field' => $field,
+ 'options' => json_encode($options)));
+ }
+
+ /**
+ * Get all the marketplace categories.
+ *
+ * @return array A list of category names
+ */
+ function marketplace_getCategories() {
+ return $this->call_method('facebook.marketplace.getCategories',
+ array());
+ }
+
+ /**
+ * Get all the marketplace subcategories for a particular category.
+ *
+ * @param category The category for which we are pulling subcategories
+ *
+ * @return array A list of subcategory names
+ */
+ function marketplace_getSubCategories($category) {
+ return $this->call_method('facebook.marketplace.getSubCategories',
+ array('category' => $category));
+ }
+
+ /**
+ * Get listings by either listing_id or user.
+ *
+ * @param listing_ids An array of listing_ids (optional)
+ * @param uids An array of user ids (optional)
+ *
+ * @return array The data for matched listings
+ */
+ function marketplace_getListings($listing_ids, $uids) {
+ return $this->call_method('facebook.marketplace.getListings',
+ array('listing_ids' => $listing_ids, 'uids' => $uids));
+ }
+
+ /**
+ * Search for Marketplace listings. All arguments are optional, though at
+ * least one must be filled out to retrieve results.
+ *
+ * @param category The category in which to search (optional)
+ * @param subcategory The subcategory in which to search (optional)
+ * @param query A query string (optional)
+ *
+ * @return array The data for matched listings
+ */
+ function marketplace_search($category, $subcategory, $query) {
+ return $this->call_method('facebook.marketplace.search',
+ array('category' => $category,
+ 'subcategory' => $subcategory,
+ 'query' => $query));
+ }
+
+ /**
+ * Remove a listing from Marketplace.
+ *
+ * @param listing_id The id of the listing to be removed
+ * @param status 'SUCCESS', 'NOT_SUCCESS', or 'DEFAULT'
+ *
+ * @return bool True on success
+ */
+ function marketplace_removeListing($listing_id,
+ $status='DEFAULT',
+ $uid=null) {
+ return $this->call_method('facebook.marketplace.removeListing',
+ array('listing_id' => $listing_id,
+ 'status' => $status,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Create/modify a Marketplace listing for the loggedinuser.
+ *
+ * @param int listing_id The id of a listing to be modified, 0
+ * for a new listing.
+ * @param show_on_profile bool Should we show this listing on the
+ * user's profile
+ * @param listing_attrs array An array of the listing data
+ *
+ * @return int The listing_id (unchanged if modifying an existing listing).
+ */
+ function marketplace_createListing($listing_id,
+ $show_on_profile,
+ $attrs,
+ $uid=null) {
+ return $this->call_method('facebook.marketplace.createListing',
+ array('listing_id' => $listing_id,
+ 'show_on_profile' => $show_on_profile,
+ 'listing_attrs' => json_encode($attrs),
+ 'uid' => $uid));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Data Store API
+
+ /**
+ * Set a user preference.
+ *
+ * @param pref_id preference identifier (0-200)
+ * @param value preferece's value
+ * @param uid the user id (defaults to current session user)
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_setUserPreference($pref_id, $value, $uid = null) {
+ return $this->call_method('facebook.data.setUserPreference',
+ array('pref_id' => $pref_id,
+ 'value' => $value,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Set a user's all preferences for this application.
+ *
+ * @param values preferece values in an associative arrays
+ * @param replace whether to replace all existing preferences or
+ * merge into them.
+ * @param uid the user id (defaults to current session user)
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_setUserPreferences($values,
+ $replace = false,
+ $uid = null) {
+ return $this->call_method('facebook.data.setUserPreferences',
+ array('values' => json_encode($values),
+ 'replace' => $replace,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Get a user preference.
+ *
+ * @param pref_id preference identifier (0-200)
+ * @param uid the user id (defaults to current session user)
+ * @return preference's value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_getUserPreference($pref_id, $uid = null) {
+ return $this->call_method('facebook.data.getUserPreference',
+ array('pref_id' => $pref_id,
+ 'uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Get a user preference.
+ *
+ * @param uid the user id (defaults to current session user)
+ * @return preference values
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ * API_EC_PERMISSION_OTHER_USER
+ */
+ public function &data_getUserPreferences($uid = null) {
+ return $this->call_method('facebook.data.getUserPreferences',
+ array('uid' => $this->get_uid($uid)));
+ }
+
+ /**
+ * Create a new object type.
+ *
+ * @param name object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_createObjectType($name) {
+ return $this->call_method('facebook.data.createObjectType',
+ array('name' => $name));
+ }
+
+ /**
+ * Delete an object type.
+ *
+ * @param obj_type object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_dropObjectType($obj_type) {
+ return $this->call_method('facebook.data.dropObjectType',
+ array('obj_type' => $obj_type));
+ }
+
+ /**
+ * Rename an object type.
+ *
+ * @param obj_type object type's name
+ * @param new_name new object type's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameObjectType($obj_type, $new_name) {
+ return $this->call_method('facebook.data.renameObjectType',
+ array('obj_type' => $obj_type,
+ 'new_name' => $new_name));
+ }
+
+ /**
+ * Add a new property to an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to add
+ * @param prop_type 1: integer; 2: string; 3: text blob
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_defineObjectProperty($obj_type,
+ $prop_name,
+ $prop_type) {
+ return $this->call_method('facebook.data.defineObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name,
+ 'prop_type' => $prop_type));
+ }
+
+ /**
+ * Remove a previously defined property from an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to remove
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_undefineObjectProperty($obj_type, $prop_name) {
+ return $this->call_method('facebook.data.undefineObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Rename a previously defined property of an object type.
+ *
+ * @param obj_type object type's name
+ * @param prop_name name of the property to rename
+ * @param new_name new name to use
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameObjectProperty($obj_type, $prop_name,
+ $new_name) {
+ return $this->call_method('facebook.data.renameObjectProperty',
+ array('obj_type' => $obj_type,
+ 'prop_name' => $prop_name,
+ 'new_name' => $new_name));
+ }
+
+ /**
+ * Retrieve a list of all object types that have defined for the application.
+ *
+ * @return a list of object type names
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectTypes() {
+ return $this->call_method('facebook.data.getObjectTypes');
+ }
+
+ /**
+ * Get definitions of all properties of an object type.
+ *
+ * @param obj_type object type's name
+ * @return pairs of property name and property types
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectType($obj_type) {
+ return $this->call_method('facebook.data.getObjectType',
+ array('obj_type' => $obj_type));
+ }
+
+ /**
+ * Create a new object.
+ *
+ * @param obj_type object type's name
+ * @param properties (optional) properties to set initially
+ * @return newly created object's id
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_createObject($obj_type, $properties = null) {
+ return $this->call_method('facebook.data.createObject',
+ array('obj_type' => $obj_type,
+ 'properties' => json_encode($properties)));
+ }
+
+ /**
+ * Update an existing object.
+ *
+ * @param obj_id object's id
+ * @param properties new properties
+ * @param replace true for replacing existing properties;
+ * false for merging
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_updateObject($obj_id, $properties, $replace = false) {
+ return $this->call_method('facebook.data.updateObject',
+ array('obj_id' => $obj_id,
+ 'properties' => json_encode($properties),
+ 'replace' => $replace));
+ }
+
+ /**
+ * Delete an existing object.
+ *
+ * @param obj_id object's id
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_deleteObject($obj_id) {
+ return $this->call_method('facebook.data.deleteObject',
+ array('obj_id' => $obj_id));
+ }
+
+ /**
+ * Delete a list of objects.
+ *
+ * @param obj_ids objects to delete
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_deleteObjects($obj_ids) {
+ return $this->call_method('facebook.data.deleteObjects',
+ array('obj_ids' => json_encode($obj_ids)));
+ }
+
+ /**
+ * Get a single property value of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_name individual property's name
+ * @return individual property's value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjectProperty($obj_id, $prop_name) {
+ return $this->call_method('facebook.data.getObjectProperty',
+ array('obj_id' => $obj_id,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Get properties of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_names (optional) properties to return; null for all.
+ * @return specified properties of an object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObject($obj_id, $prop_names = null) {
+ return $this->call_method('facebook.data.getObject',
+ array('obj_id' => $obj_id,
+ 'prop_names' => json_encode($prop_names)));
+ }
+
+ /**
+ * Get properties of a list of objects.
+ *
+ * @param obj_ids object ids
+ * @param prop_names (optional) properties to return; null for all.
+ * @return specified properties of an object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getObjects($obj_ids, $prop_names = null) {
+ return $this->call_method('facebook.data.getObjects',
+ array('obj_ids' => json_encode($obj_ids),
+ 'prop_names' => json_encode($prop_names)));
+ }
+
+ /**
+ * Set a single property value of an object.
+ *
+ * @param obj_id object's id
+ * @param prop_name individual property's name
+ * @param prop_value new value to set
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setObjectProperty($obj_id, $prop_name,
+ $prop_value) {
+ return $this->call_method('facebook.data.setObjectProperty',
+ array('obj_id' => $obj_id,
+ 'prop_name' => $prop_name,
+ 'prop_value' => $prop_value));
+ }
+
+ /**
+ * Read hash value by key.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param prop_name (optional) individual property's name
+ * @return hash value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getHashValue($obj_type, $key, $prop_name = null) {
+ return $this->call_method('facebook.data.getHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Write hash value by key.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param value hash value
+ * @param prop_name (optional) individual property's name
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setHashValue($obj_type,
+ $key,
+ $value,
+ $prop_name = null) {
+ return $this->call_method('facebook.data.setHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'value' => $value,
+ 'prop_name' => $prop_name));
+ }
+
+ /**
+ * Increase a hash value by specified increment atomically.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @param prop_name individual property's name
+ * @param increment (optional) default is 1
+ * @return incremented hash value
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_incHashValue($obj_type,
+ $key,
+ $prop_name,
+ $increment = 1) {
+ return $this->call_method('facebook.data.incHashValue',
+ array('obj_type' => $obj_type,
+ 'key' => $key,
+ 'prop_name' => $prop_name,
+ 'increment' => $increment));
+ }
+
+ /**
+ * Remove a hash key and its values.
+ *
+ * @param obj_type object type's name
+ * @param key hash key
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeHashKey($obj_type, $key) {
+ return $this->call_method('facebook.data.removeHashKey',
+ array('obj_type' => $obj_type,
+ 'key' => $key));
+ }
+
+ /**
+ * Remove hash keys and their values.
+ *
+ * @param obj_type object type's name
+ * @param keys hash keys
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeHashKeys($obj_type, $keys) {
+ return $this->call_method('facebook.data.removeHashKeys',
+ array('obj_type' => $obj_type,
+ 'keys' => json_encode($keys)));
+ }
+
+ /**
+ * Define an object association.
+ *
+ * @param name name of this association
+ * @param assoc_type 1: one-way 2: two-way symmetric 3: two-way asymmetric
+ * @param assoc_info1 needed info about first object type
+ * @param assoc_info2 needed info about second object type
+ * @param inverse (optional) name of reverse association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_defineAssociation($name, $assoc_type, $assoc_info1,
+ $assoc_info2, $inverse = null) {
+ return $this->call_method('facebook.data.defineAssociation',
+ array('name' => $name,
+ 'assoc_type' => $assoc_type,
+ 'assoc_info1' => json_encode($assoc_info1),
+ 'assoc_info2' => json_encode($assoc_info2),
+ 'inverse' => $inverse));
+ }
+
+ /**
+ * Undefine an object association.
+ *
+ * @param name name of this association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_undefineAssociation($name) {
+ return $this->call_method('facebook.data.undefineAssociation',
+ array('name' => $name));
+ }
+
+ /**
+ * Rename an object association or aliases.
+ *
+ * @param name name of this association
+ * @param new_name (optional) new name of this association
+ * @param new_alias1 (optional) new alias for object type 1
+ * @param new_alias2 (optional) new alias for object type 2
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_ALREADY_EXISTS
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_renameAssociation($name, $new_name, $new_alias1 = null,
+ $new_alias2 = null) {
+ return $this->call_method('facebook.data.renameAssociation',
+ array('name' => $name,
+ 'new_name' => $new_name,
+ 'new_alias1' => $new_alias1,
+ 'new_alias2' => $new_alias2));
+ }
+
+ /**
+ * Get definition of an object association.
+ *
+ * @param name name of this association
+ * @return specified association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociationDefinition($name) {
+ return $this->call_method('facebook.data.getAssociationDefinition',
+ array('name' => $name));
+ }
+
+ /**
+ * Get definition of all associations.
+ *
+ * @return all defined associations
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociationDefinitions() {
+ return $this->call_method('facebook.data.getAssociationDefinitions',
+ array());
+ }
+
+ /**
+ * Create or modify an association between two objects.
+ *
+ * @param name name of association
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @param data (optional) extra string data to store
+ * @param assoc_time (optional) extra time data; default to creation time
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setAssociation($name, $obj_id1, $obj_id2, $data = null,
+ $assoc_time = null) {
+ return $this->call_method('facebook.data.setAssociation',
+ array('name' => $name,
+ 'obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2,
+ 'data' => $data,
+ 'assoc_time' => $assoc_time));
+ }
+
+ /**
+ * Create or modify associations between objects.
+ *
+ * @param assocs associations to set
+ * @param name (optional) name of association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_setAssociations($assocs, $name = null) {
+ return $this->call_method('facebook.data.setAssociations',
+ array('assocs' => json_encode($assocs),
+ 'name' => $name));
+ }
+
+ /**
+ * Remove an association between two objects.
+ *
+ * @param name name of association
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociation($name, $obj_id1, $obj_id2) {
+ return $this->call_method('facebook.data.removeAssociation',
+ array('name' => $name,
+ 'obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2));
+ }
+
+ /**
+ * Remove associations between objects by specifying pairs of object ids.
+ *
+ * @param assocs associations to remove
+ * @param name (optional) name of association
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociations($assocs, $name = null) {
+ return $this->call_method('facebook.data.removeAssociations',
+ array('assocs' => json_encode($assocs),
+ 'name' => $name));
+ }
+
+ /**
+ * Remove associations between objects by specifying one object id.
+ *
+ * @param name name of association
+ * @param obj_id who's association to remove
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_removeAssociatedObjects($name, $obj_id) {
+ return $this->call_method('facebook.data.removeAssociatedObjects',
+ array('name' => $name,
+ 'obj_id' => $obj_id));
+ }
+
+ /**
+ * Retrieve a list of associated objects.
+ *
+ * @param name name of association
+ * @param obj_id who's association to retrieve
+ * @param no_data only return object ids
+ * @return associated objects
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjects($name, $obj_id, $no_data = true) {
+ return $this->call_method('facebook.data.getAssociatedObjects',
+ array('name' => $name,
+ 'obj_id' => $obj_id,
+ 'no_data' => $no_data));
+ }
+
+ /**
+ * Count associated objects.
+ *
+ * @param name name of association
+ * @param obj_id who's association to retrieve
+ * @return associated object's count
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjectCount($name, $obj_id) {
+ return $this->call_method('facebook.data.getAssociatedObjectCount',
+ array('name' => $name,
+ 'obj_id' => $obj_id));
+ }
+
+ /**
+ * Get a list of associated object counts.
+ *
+ * @param name name of association
+ * @param obj_ids whose association to retrieve
+ * @return associated object counts
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_DATA_OBJECT_NOT_FOUND
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_INVALID_OPERATION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociatedObjectCounts($name, $obj_ids) {
+ return $this->call_method('facebook.data.getAssociatedObjectCounts',
+ array('name' => $name,
+ 'obj_ids' => json_encode($obj_ids)));
+ }
+
+ /**
+ * Find all associations between two objects.
+ *
+ * @param obj_id1 id of first object
+ * @param obj_id2 id of second object
+ * @param no_data only return association names without data
+ * @return all associations between objects
+ * @error
+ * API_EC_DATA_DATABASE_ERROR
+ * API_EC_PARAM
+ * API_EC_PERMISSION
+ * API_EC_DATA_QUOTA_EXCEEDED
+ * API_EC_DATA_UNKNOWN_ERROR
+ */
+ public function &data_getAssociations($obj_id1, $obj_id2, $no_data = true) {
+ return $this->call_method('facebook.data.getAssociations',
+ array('obj_id1' => $obj_id1,
+ 'obj_id2' => $obj_id2,
+ 'no_data' => $no_data));
+ }
+
+ /**
+ * Get the properties that you have set for an app.
+ *
+ * @param properties List of properties names to fetch
+ *
+ * @return array A map from property name to value
+ */
+ public function admin_getAppProperties($properties) {
+ return json_decode(
+ $this->call_method('facebook.admin.getAppProperties',
+ array('properties' => json_encode($properties))), true);
+ }
+
+ /**
+ * Set properties for an app.
+ *
+ * @param properties A map from property names to values
+ *
+ * @return bool true on success
+ */
+ public function admin_setAppProperties($properties) {
+ return $this->call_method('facebook.admin.setAppProperties',
+ array('properties' => json_encode($properties)));
+ }
+
+ /**
+ * Returns the allocation limit value for a specified integration point name
+ * Integration point names are defined in lib/api/karma/constants.php in the
+ * limit_map.
+ *
+ * @param string $integration_point_name Name of an integration point
+ * (see developer wiki for list).
+ * @param int $uid Specific user to check the limit.
+ *
+ * @return int Integration point allocation value
+ */
+ public function &admin_getAllocation($integration_point_name, $uid=null) {
+ return $this->call_method('facebook.admin.getAllocation',
+ array('integration_point_name' => $integration_point_name,
+ 'uid' => $uid));
+ }
+
+ /**
+ * Returns values for the specified metrics for the current application, in
+ * the given time range. The metrics are collected for fixed-length periods,
+ * and the times represent midnight at the end of each period.
+ *
+ * @param start_time unix time for the start of the range
+ * @param end_time unix time for the end of the range
+ * @param period number of seconds in the desired period
+ * @param metrics list of metrics to look up
+ *
+ * @return array A map of the names and values for those metrics
+ */
+ public function &admin_getMetrics($start_time, $end_time, $period, $metrics) {
+ return $this->call_method('admin.getMetrics',
+ array('start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'period' => $period,
+ 'metrics' => json_encode($metrics)));
+ }
+
+ /**
+ * Sets application restriction info.
+ *
+ * Applications can restrict themselves to only a limited user demographic
+ * based on users' age and/or location or based on static predefined types
+ * specified by facebook for specifying diff age restriction for diff
+ * locations.
+ *
+ * @param array $restriction_info The age restriction settings to set.
+ *
+ * @return bool true on success
+ */
+ public function admin_setRestrictionInfo($restriction_info = null) {
+ $restriction_str = null;
+ if (!empty($restriction_info)) {
+ $restriction_str = json_encode($restriction_info);
+ }
+ return $this->call_method('admin.setRestrictionInfo',
+ array('restriction_str' => $restriction_str));
+ }
+
+ /**
+ * Gets application restriction info.
+ *
+ * Applications can restrict themselves to only a limited user demographic
+ * based on users' age and/or location or based on static predefined types
+ * specified by facebook for specifying diff age restriction for diff
+ * locations.
+ *
+ * @return array The age restriction settings for this application.
+ */
+ public function admin_getRestrictionInfo() {
+ return json_decode(
+ $this->call_method('admin.getRestrictionInfo'),
+ true);
+ }
+
+
+ /**
+ * Bans a list of users from the app. Banned users can't
+ * access the app's canvas page and forums.
+ *
+ * @param array $uids an array of user ids
+ * @return bool true on success
+ */
+ public function admin_banUsers($uids) {
+ return $this->call_method(
+ 'admin.banUsers', array('uids' => json_encode($uids)));
+ }
+
+ /**
+ * Unban users that have been previously banned with
+ * admin_banUsers().
+ *
+ * @param array $uids an array of user ids
+ * @return bool true on success
+ */
+ public function admin_unbanUsers($uids) {
+ return $this->call_method(
+ 'admin.unbanUsers', array('uids' => json_encode($uids)));
+ }
+
+ /**
+ * Gets the list of users that have been banned from the application.
+ * $uids is an optional parameter that filters the result with the list
+ * of provided user ids. If $uids is provided,
+ * only banned user ids that are contained in $uids are returned.
+ *
+ * @param array $uids an array of user ids to filter by
+ * @return bool true on success
+ */
+
+ public function admin_getBannedUsers($uids = null) {
+ return $this->call_method(
+ 'admin.getBannedUsers',
+ array('uids' => $uids ? json_encode($uids) : null));
+ }
+
+
+ /* UTILITY FUNCTIONS */
+
+ /**
+ * Calls the specified normal POST method with the specified parameters.
+ *
+ * @param string $method Name of the Facebook method to invoke
+ * @param array $params A map of param names => param values
+ *
+ * @return mixed Result of method call; this returns a reference to support
+ * 'delayed returns' when in a batch context.
+ * See: http://wiki.developers.facebook.com/index.php/Using_batching_API
+ */
+ public function &call_method($method, $params = array()) {
+ if ($this->format) {
+ $params['format'] = $this->format;
+ }
+ if (!$this->pending_batch()) {
+ if ($this->call_as_apikey) {
+ $params['call_as_apikey'] = $this->call_as_apikey;
+ }
+ $data = $this->post_request($method, $params);
+ $result = $this->convert_result($data, $method, $params);
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+ }
+ else {
+ $result = null;
+ $batch_item = array('m' => $method, 'p' => $params, 'r' => & $result);
+ $this->batch_queue[] = $batch_item;
+ }
+
+ return $result;
+ }
+
+ protected function convert_result($data, $method, $params) {
+ $is_xml = (empty($params['format']) ||
+ strtolower($params['format']) != 'json');
+ return ($is_xml) ? $this->convert_xml_to_result($data, $method, $params)
+ : json_decode($data, true);
+ }
+
+ /**
+ * Change the response format
+ *
+ * @param string $format The response format (json, xml)
+ */
+ public function setFormat($format) {
+ $this->format = $format;
+ return $this;
+ }
+
+ /**
+ * get the current response serialization format
+ *
+ * @return string 'xml', 'json', or null (which means 'xml')
+ */
+ public function getFormat() {
+ return $this->format;
+ }
+
+ /**
+ * Calls the specified file-upload POST method with the specified parameters
+ *
+ * @param string $method Name of the Facebook method to invoke
+ * @param array $params A map of param names => param values
+ * @param string $file A path to the file to upload (required)
+ *
+ * @return array A dictionary representing the response.
+ */
+ public function call_upload_method($method, $params, $file, $server_addr = null) {
+ if (!$this->pending_batch()) {
+ if (!file_exists($file)) {
+ $code =
+ FacebookAPIErrorCodes::API_EC_PARAM;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ if ($this->format) {
+ $params['format'] = $this->format;
+ }
+ $data = $this->post_upload_request($method,
+ $params,
+ $file,
+ $server_addr);
+ $result = $this->convert_result($data, $method, $params);
+
+ if (is_array($result) && isset($result['error_code'])) {
+ throw new FacebookRestClientException($result['error_msg'],
+ $result['error_code']);
+ }
+ }
+ else {
+ $code =
+ FacebookAPIErrorCodes::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE;
+ $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+ throw new FacebookRestClientException($description, $code);
+ }
+
+ return $result;
+ }
+
+ protected function convert_xml_to_result($xml, $method, $params) {
+ $sxml = simplexml_load_string($xml);
+ $result = self::convert_simplexml_to_array($sxml);
+
+ if (!empty($GLOBALS['facebook_config']['debug'])) {
+ // output the raw xml and its corresponding php object, for debugging:
+ print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">';
+ $this->cur_id++;
+ print $this->cur_id . ': Called ' . $method . ', show ' .
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'params\');">Params</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'xml\');">XML</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'sxml\');">SXML</a> | '.
+ '<a href=# onclick="return toggleDisplay(' . $this->cur_id . ', \'php\');">PHP</a>';
+ print '<pre id="params'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($params, true).'</pre>';
+ print '<pre id="xml'.$this->cur_id.'" style="display: none; overflow: auto;">'.htmlspecialchars($xml).'</pre>';
+ print '<pre id="php'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($result, true).'</pre>';
+ print '<pre id="sxml'.$this->cur_id.'" style="display: none; overflow: auto;">'.print_r($sxml, true).'</pre>';
+ print '</div>';
+ }
+ return $result;
+ }
+
+ protected function finalize_params($method, $params) {
+ list($get, $post) = $this->add_standard_params($method, $params);
+ // we need to do this before signing the params
+ $this->convert_array_values_to_json($post);
+ $post['sig'] = Facebook::generate_sig(array_merge($get, $post),
+ $this->secret);
+ return array($get, $post);
+ }
+
+ private function convert_array_values_to_json(&$params) {
+ foreach ($params as $key => &$val) {
+ if (is_array($val)) {
+ $val = json_encode($val);
+ }
+ }
+ }
+
+ /**
+ * Add the generally required params to our request.
+ * Params method, api_key, and v should be sent over as get.
+ */
+ private function add_standard_params($method, $params) {
+ $post = $params;
+ $get = array();
+ if ($this->call_as_apikey) {
+ $get['call_as_apikey'] = $this->call_as_apikey;
+ }
+ $get['method'] = $method;
+ $get['session_key'] = $this->session_key;
+ $get['api_key'] = $this->api_key;
+ $post['call_id'] = microtime(true);
+ if ($post['call_id'] <= $this->last_call_id) {
+ $post['call_id'] = $this->last_call_id + 0.001;
+ }
+ $this->last_call_id = $post['call_id'];
+ if (isset($post['v'])) {
+ $get['v'] = $post['v'];
+ unset($post['v']);
+ } else {
+ $get['v'] = '1.0';
+ }
+ if (isset($this->use_ssl_resources) &&
+ $this->use_ssl_resources) {
+ $post['return_ssl_resources'] = true;
+ }
+ return array($get, $post);
+ }
+
+ private function create_url_string($params) {
+ $post_params = array();
+ foreach ($params as $key => &$val) {
+ $post_params[] = $key.'='.urlencode($val);
+ }
+ return implode('&', $post_params);
+ }
+
+ private function run_multipart_http_transaction($method, $params, $file, $server_addr) {
+
+ // the format of this message is specified in RFC1867/RFC1341.
+ // we add twenty pseudo-random digits to the end of the boundary string.
+ $boundary = '--------------------------FbMuLtIpArT' .
+ sprintf("%010d", mt_rand()) .
+ sprintf("%010d", mt_rand());
+ $content_type = 'multipart/form-data; boundary=' . $boundary;
+ // within the message, we prepend two extra hyphens.
+ $delimiter = '--' . $boundary;
+ $close_delimiter = $delimiter . '--';
+ $content_lines = array();
+ foreach ($params as $key => &$val) {
+ $content_lines[] = $delimiter;
+ $content_lines[] = 'Content-Disposition: form-data; name="' . $key . '"';
+ $content_lines[] = '';
+ $content_lines[] = $val;
+ }
+ // now add the file data
+ $content_lines[] = $delimiter;
+ $content_lines[] =
+ 'Content-Disposition: form-data; filename="' . $file . '"';
+ $content_lines[] = 'Content-Type: application/octet-stream';
+ $content_lines[] = '';
+ $content_lines[] = file_get_contents($file);
+ $content_lines[] = $close_delimiter;
+ $content_lines[] = '';
+ $content = implode("\r\n", $content_lines);
+ return $this->run_http_post_transaction($content_type, $content, $server_addr);
+ }
+
+ public function post_request($method, $params) {
+ list($get, $post) = $this->finalize_params($method, $params);
+ $post_string = $this->create_url_string($post);
+ $get_string = $this->create_url_string($get);
+ $url_with_get = $this->server_addr . '?' . $get_string;
+ if ($this->use_curl_if_available && function_exists('curl_init')) {
+ $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url_with_get);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ $result = $this->curl_exec($ch);
+ curl_close($ch);
+ } else {
+ $content_type = 'application/x-www-form-urlencoded';
+ $content = $post_string;
+ $result = $this->run_http_post_transaction($content_type,
+ $content,
+ $url_with_get);
+ }
+ return $result;
+ }
+
+ /**
+ * execute a curl transaction -- this exists mostly so subclasses can add
+ * extra options and/or process the response, if they wish.
+ *
+ * @param resource $ch a curl handle
+ */
+ protected function curl_exec($ch) {
+ $result = curl_exec($ch);
+ return $result;
+ }
+
+ private function post_upload_request($method, $params, $file, $server_addr = null) {
+ $server_addr = $server_addr ? $server_addr : $this->server_addr;
+ list($get, $post) = $this->finalize_params($method, $params);
+ $get_string = $this->create_url_string($get);
+ $url_with_get = $server_addr . '?' . $get_string;
+ if ($this->use_curl_if_available && function_exists('curl_init')) {
+ // prepending '@' causes cURL to upload the file; the key is ignored.
+ $post['_file'] = '@' . $file;
+ $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url_with_get);
+ // this has to come before the POSTFIELDS set!
+ curl_setopt($ch, CURLOPT_POST, 1);
+ // passing an array gets curl to use the multipart/form-data content type
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+ $result = $this->curl_exec($ch);
+ curl_close($ch);
+ } else {
+ $result = $this->run_multipart_http_transaction($method, $post,
+ $file, $url_with_get);
+ }
+ return $result;
+ }
+
+ private function run_http_post_transaction($content_type, $content, $server_addr) {
+
+ $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) ' . phpversion();
+ $content_length = strlen($content);
+ $context =
+ array('http' =>
+ array('method' => 'POST',
+ 'user_agent' => $user_agent,
+ 'header' => 'Content-Type: ' . $content_type . "\r\n" .
+ 'Content-Length: ' . $content_length,
+ 'content' => $content));
+ $context_id = stream_context_create($context);
+ $sock = fopen($server_addr, 'r', false, $context_id);
+
+ $result = '';
+ if ($sock) {
+ while (!feof($sock)) {
+ $result .= fgets($sock, 4096);
+ }
+ fclose($sock);
+ }
+ return $result;
+ }
+
+ public static function convert_simplexml_to_array($sxml) {
+ $arr = array();
+ if ($sxml) {
+ foreach ($sxml as $k => $v) {
+ if ($sxml['list']) {
+ $arr[] = self::convert_simplexml_to_array($v);
+ } else {
+ $arr[$k] = self::convert_simplexml_to_array($v);
+ }
+ }
+ }
+ if (sizeof($arr) > 0) {
+ return $arr;
+ } else {
+ return (string)$sxml;
+ }
+ }
+
+ protected function get_uid($uid) {
+ return $uid ? $uid : $this->user;
+ }
+}
+
+
+class FacebookRestClientException extends Exception {
+}
+
+// Supporting methods and values------
+
+/**
+ * Error codes and descriptions for the Facebook API.
+ */
+
+class FacebookAPIErrorCodes {
+
+ const API_EC_SUCCESS = 0;
+
+ /*
+ * GENERAL ERRORS
+ */
+ const API_EC_UNKNOWN = 1;
+ const API_EC_SERVICE = 2;
+ const API_EC_METHOD = 3;
+ const API_EC_TOO_MANY_CALLS = 4;
+ const API_EC_BAD_IP = 5;
+ const API_EC_HOST_API = 6;
+ const API_EC_HOST_UP = 7;
+ const API_EC_SECURE = 8;
+ const API_EC_RATE = 9;
+ const API_EC_PERMISSION_DENIED = 10;
+ const API_EC_DEPRECATED = 11;
+ const API_EC_VERSION = 12;
+ const API_EC_INTERNAL_FQL_ERROR = 13;
+ const API_EC_HOST_PUP = 14;
+
+ /*
+ * PARAMETER ERRORS
+ */
+ const API_EC_PARAM = 100;
+ const API_EC_PARAM_API_KEY = 101;
+ const API_EC_PARAM_SESSION_KEY = 102;
+ const API_EC_PARAM_CALL_ID = 103;
+ const API_EC_PARAM_SIGNATURE = 104;
+ const API_EC_PARAM_TOO_MANY = 105;
+ const API_EC_PARAM_USER_ID = 110;
+ const API_EC_PARAM_USER_FIELD = 111;
+ const API_EC_PARAM_SOCIAL_FIELD = 112;
+ const API_EC_PARAM_EMAIL = 113;
+ const API_EC_PARAM_USER_ID_LIST = 114;
+ const API_EC_PARAM_FIELD_LIST = 115;
+ const API_EC_PARAM_ALBUM_ID = 120;
+ const API_EC_PARAM_PHOTO_ID = 121;
+ const API_EC_PARAM_FEED_PRIORITY = 130;
+ const API_EC_PARAM_CATEGORY = 140;
+ const API_EC_PARAM_SUBCATEGORY = 141;
+ const API_EC_PARAM_TITLE = 142;
+ const API_EC_PARAM_DESCRIPTION = 143;
+ const API_EC_PARAM_BAD_JSON = 144;
+ const API_EC_PARAM_BAD_EID = 150;
+ const API_EC_PARAM_UNKNOWN_CITY = 151;
+ const API_EC_PARAM_BAD_PAGE_TYPE = 152;
+
+ /*
+ * USER PERMISSIONS ERRORS
+ */
+ const API_EC_PERMISSION = 200;
+ const API_EC_PERMISSION_USER = 210;
+ const API_EC_PERMISSION_NO_DEVELOPERS = 211;
+ const API_EC_PERMISSION_OFFLINE_ACCESS = 212;
+ const API_EC_PERMISSION_ALBUM = 220;
+ const API_EC_PERMISSION_PHOTO = 221;
+ const API_EC_PERMISSION_MESSAGE = 230;
+ const API_EC_PERMISSION_OTHER_USER = 240;
+ const API_EC_PERMISSION_STATUS_UPDATE = 250;
+ const API_EC_PERMISSION_PHOTO_UPLOAD = 260;
+ const API_EC_PERMISSION_VIDEO_UPLOAD = 261;
+ const API_EC_PERMISSION_SMS = 270;
+ const API_EC_PERMISSION_CREATE_LISTING = 280;
+ const API_EC_PERMISSION_CREATE_NOTE = 281;
+ const API_EC_PERMISSION_SHARE_ITEM = 282;
+ const API_EC_PERMISSION_EVENT = 290;
+ const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291;
+ const API_EC_PERMISSION_LIVEMESSAGE = 292;
+ const API_EC_PERMISSION_RSVP_EVENT = 299;
+
+ /*
+ * DATA EDIT ERRORS
+ */
+ const API_EC_EDIT = 300;
+ const API_EC_EDIT_USER_DATA = 310;
+ const API_EC_EDIT_PHOTO = 320;
+ const API_EC_EDIT_ALBUM_SIZE = 321;
+ const API_EC_EDIT_PHOTO_TAG_SUBJECT = 322;
+ const API_EC_EDIT_PHOTO_TAG_PHOTO = 323;
+ const API_EC_EDIT_PHOTO_FILE = 324;
+ const API_EC_EDIT_PHOTO_PENDING_LIMIT = 325;
+ const API_EC_EDIT_PHOTO_TAG_LIMIT = 326;
+ const API_EC_EDIT_ALBUM_REORDER_PHOTO_NOT_IN_ALBUM = 327;
+ const API_EC_EDIT_ALBUM_REORDER_TOO_FEW_PHOTOS = 328;
+
+ const API_EC_MALFORMED_MARKUP = 329;
+ const API_EC_EDIT_MARKUP = 330;
+
+ const API_EC_EDIT_FEED_TOO_MANY_USER_CALLS = 340;
+ const API_EC_EDIT_FEED_TOO_MANY_USER_ACTION_CALLS = 341;
+ const API_EC_EDIT_FEED_TITLE_LINK = 342;
+ const API_EC_EDIT_FEED_TITLE_LENGTH = 343;
+ const API_EC_EDIT_FEED_TITLE_NAME = 344;
+ const API_EC_EDIT_FEED_TITLE_BLANK = 345;
+ const API_EC_EDIT_FEED_BODY_LENGTH = 346;
+ const API_EC_EDIT_FEED_PHOTO_SRC = 347;
+ const API_EC_EDIT_FEED_PHOTO_LINK = 348;
+
+ const API_EC_EDIT_VIDEO_SIZE = 350;
+ const API_EC_EDIT_VIDEO_INVALID_FILE = 351;
+ const API_EC_EDIT_VIDEO_INVALID_TYPE = 352;
+ const API_EC_EDIT_VIDEO_FILE = 353;
+
+ const API_EC_EDIT_FEED_TITLE_ARRAY = 360;
+ const API_EC_EDIT_FEED_TITLE_PARAMS = 361;
+ const API_EC_EDIT_FEED_BODY_ARRAY = 362;
+ const API_EC_EDIT_FEED_BODY_PARAMS = 363;
+ const API_EC_EDIT_FEED_PHOTO = 364;
+ const API_EC_EDIT_FEED_TEMPLATE = 365;
+ const API_EC_EDIT_FEED_TARGET = 366;
+ const API_EC_EDIT_FEED_MARKUP = 367;
+
+ /**
+ * SESSION ERRORS
+ */
+ const API_EC_SESSION_TIMED_OUT = 450;
+ const API_EC_SESSION_METHOD = 451;
+ const API_EC_SESSION_INVALID = 452;
+ const API_EC_SESSION_REQUIRED = 453;
+ const API_EC_SESSION_REQUIRED_FOR_SECRET = 454;
+ const API_EC_SESSION_CANNOT_USE_SESSION_SECRET = 455;
+
+
+ /**
+ * FQL ERRORS
+ */
+ const FQL_EC_UNKNOWN_ERROR = 600;
+ const FQL_EC_PARSER = 601; // backwards compatibility
+ const FQL_EC_PARSER_ERROR = 601;
+ const FQL_EC_UNKNOWN_FIELD = 602;
+ const FQL_EC_UNKNOWN_TABLE = 603;
+ const FQL_EC_NOT_INDEXABLE = 604; // backwards compatibility
+ const FQL_EC_NO_INDEX = 604;
+ const FQL_EC_UNKNOWN_FUNCTION = 605;
+ const FQL_EC_INVALID_PARAM = 606;
+ const FQL_EC_INVALID_FIELD = 607;
+ const FQL_EC_INVALID_SESSION = 608;
+ const FQL_EC_UNSUPPORTED_APP_TYPE = 609;
+ const FQL_EC_SESSION_SECRET_NOT_ALLOWED = 610;
+ const FQL_EC_DEPRECATED_TABLE = 611;
+ const FQL_EC_EXTENDED_PERMISSION = 612;
+ const FQL_EC_RATE_LIMIT_EXCEEDED = 613;
+ const FQL_EC_UNRESOLVED_DEPENDENCY = 614;
+
+ const API_EC_REF_SET_FAILED = 700;
+
+ /**
+ * DATA STORE API ERRORS
+ */
+ const API_EC_DATA_UNKNOWN_ERROR = 800;
+ const API_EC_DATA_INVALID_OPERATION = 801;
+ const API_EC_DATA_QUOTA_EXCEEDED = 802;
+ const API_EC_DATA_OBJECT_NOT_FOUND = 803;
+ const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804;
+ const API_EC_DATA_DATABASE_ERROR = 805;
+ const API_EC_DATA_CREATE_TEMPLATE_ERROR = 806;
+ const API_EC_DATA_TEMPLATE_EXISTS_ERROR = 807;
+ const API_EC_DATA_TEMPLATE_HANDLE_TOO_LONG = 808;
+ const API_EC_DATA_TEMPLATE_HANDLE_ALREADY_IN_USE = 809;
+ const API_EC_DATA_TOO_MANY_TEMPLATE_BUNDLES = 810;
+ const API_EC_DATA_MALFORMED_ACTION_LINK = 811;
+ const API_EC_DATA_TEMPLATE_USES_RESERVED_TOKEN = 812;
+
+ /*
+ * APPLICATION INFO ERRORS
+ */
+ const API_EC_NO_SUCH_APP = 900;
+
+ /*
+ * BATCH ERRORS
+ */
+ const API_EC_BATCH_TOO_MANY_ITEMS = 950;
+ const API_EC_BATCH_ALREADY_STARTED = 951;
+ const API_EC_BATCH_NOT_STARTED = 952;
+ const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 953;
+
+ /*
+ * EVENT API ERRORS
+ */
+ const API_EC_EVENT_INVALID_TIME = 1000;
+
+ /*
+ * INFO BOX ERRORS
+ */
+ const API_EC_INFO_NO_INFORMATION = 1050;
+ const API_EC_INFO_SET_FAILED = 1051;
+
+ /*
+ * LIVEMESSAGE API ERRORS
+ */
+ const API_EC_LIVEMESSAGE_SEND_FAILED = 1100;
+ const API_EC_LIVEMESSAGE_EVENT_NAME_TOO_LONG = 1101;
+ const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102;
+
+ /*
+ * PAYMENTS API ERRORS
+ */
+ const API_EC_PAYMENTS_UNKNOWN = 1150;
+ const API_EC_PAYMENTS_APP_INVALID = 1151;
+ const API_EC_PAYMENTS_DATABASE = 1152;
+ const API_EC_PAYMENTS_PERMISSION_DENIED = 1153;
+ const API_EC_PAYMENTS_APP_NO_RESPONSE = 1154;
+ const API_EC_PAYMENTS_APP_ERROR_RESPONSE = 1155;
+ const API_EC_PAYMENTS_INVALID_ORDER = 1156;
+ const API_EC_PAYMENTS_INVALID_PARAM = 1157;
+ const API_EC_PAYMENTS_INVALID_OPERATION = 1158;
+ const API_EC_PAYMENTS_PAYMENT_FAILED = 1159;
+ const API_EC_PAYMENTS_DISABLED = 1160;
+
+ /*
+ * CONNECT SESSION ERRORS
+ */
+ const API_EC_CONNECT_FEED_DISABLED = 1300;
+
+ /*
+ * Platform tag bundles errors
+ */
+ const API_EC_TAG_BUNDLE_QUOTA = 1400;
+
+ /*
+ * SHARE
+ */
+ const API_EC_SHARE_BAD_URL = 1500;
+
+ /*
+ * NOTES
+ */
+ const API_EC_NOTE_CANNOT_MODIFY = 1600;
+
+ /*
+ * COMMENTS
+ */
+ const API_EC_COMMENTS_UNKNOWN = 1700;
+ const API_EC_COMMENTS_POST_TOO_LONG = 1701;
+ const API_EC_COMMENTS_DB_DOWN = 1702;
+ const API_EC_COMMENTS_INVALID_XID = 1703;
+ const API_EC_COMMENTS_INVALID_UID = 1704;
+ const API_EC_COMMENTS_INVALID_POST = 1705;
+ const API_EC_COMMENTS_INVALID_REMOVE = 1706;
+
+ /**
+ * This array is no longer maintained; to view the description of an error
+ * code, please look at the message element of the API response or visit
+ * the developer wiki at http://wiki.developers.facebook.com/.
+ */
+ public static $api_error_descriptions = array(
+ self::API_EC_SUCCESS => 'Success',
+ self::API_EC_UNKNOWN => 'An unknown error occurred',
+ self::API_EC_SERVICE => 'Service temporarily unavailable',
+ self::API_EC_METHOD => 'Unknown method',
+ self::API_EC_TOO_MANY_CALLS => 'Application request limit reached',
+ self::API_EC_BAD_IP => 'Unauthorized source IP address',
+ self::API_EC_PARAM => 'Invalid parameter',
+ self::API_EC_PARAM_API_KEY => 'Invalid API key',
+ self::API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
+ self::API_EC_PARAM_CALL_ID => 'Call_id must be greater than previous',
+ self::API_EC_PARAM_SIGNATURE => 'Incorrect signature',
+ self::API_EC_PARAM_USER_ID => 'Invalid user id',
+ self::API_EC_PARAM_USER_FIELD => 'Invalid user info field',
+ self::API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
+ self::API_EC_PARAM_USER_ID_LIST => 'Invalid user id list',
+ self::API_EC_PARAM_FIELD_LIST => 'Invalid field list',
+ self::API_EC_PARAM_ALBUM_ID => 'Invalid album id',
+ self::API_EC_PARAM_BAD_EID => 'Invalid eid',
+ self::API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
+ self::API_EC_PERMISSION => 'Permissions error',
+ self::API_EC_PERMISSION_USER => 'User not visible',
+ self::API_EC_PERMISSION_NO_DEVELOPERS => 'Application has no developers',
+ self::API_EC_PERMISSION_ALBUM => 'Album not visible',
+ self::API_EC_PERMISSION_PHOTO => 'Photo not visible',
+ self::API_EC_PERMISSION_EVENT => 'Creating and modifying events required the extended permission create_event',
+ self::API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
+ self::API_EC_EDIT_ALBUM_SIZE => 'Album is full',
+ self::FQL_EC_PARSER => 'FQL: Parser Error',
+ self::FQL_EC_UNKNOWN_FIELD => 'FQL: Unknown Field',
+ self::FQL_EC_UNKNOWN_TABLE => 'FQL: Unknown Table',
+ self::FQL_EC_NOT_INDEXABLE => 'FQL: Statement not indexable',
+ self::FQL_EC_UNKNOWN_FUNCTION => 'FQL: Attempted to call unknown function',
+ self::FQL_EC_INVALID_PARAM => 'FQL: Invalid parameter passed in',
+ self::API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
+ self::API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
+ self::API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
+ self::API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
+ self::API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
+ self::API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
+ self::API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
+ self::API_EC_BATCH_NOT_STARTED => 'end_batch called before begin_batch',
+ self::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode'
+ );
+}
diff --git a/plugins/Facebook/facebook/jsonwrapper/JSON/JSON.php b/plugins/Facebook/facebook/jsonwrapper/JSON/JSON.php
new file mode 100644
index 000000000..0cddbddb4
--- /dev/null
+++ b/plugins/Facebook/facebook/jsonwrapper/JSON/JSON.php
@@ -0,0 +1,806 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Converts to and from JSON format.
+ *
+ * JSON (JavaScript Object Notation) is a lightweight data-interchange
+ * format. It is easy for humans to read and write. It is easy for machines
+ * to parse and generate. It is based on a subset of the JavaScript
+ * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
+ * This feature can also be found in Python. JSON is a text format that is
+ * completely language independent but uses conventions that are familiar
+ * to programmers of the C-family of languages, including C, C++, C#, Java,
+ * JavaScript, Perl, TCL, and many others. These properties make JSON an
+ * ideal data-interchange language.
+ *
+ * This package provides a simple encoder and decoder for JSON notation. It
+ * is intended for use with client-side Javascript applications that make
+ * use of HTTPRequest to perform server communication functions - data can
+ * be encoded into JSON notation for use in a client-side javascript, or
+ * decoded from incoming Javascript requests. JSON format is native to
+ * Javascript, and can be directly eval()'ed with no further parsing
+ * overhead
+ *
+ * All strings should be in ASCII or UTF-8 format!
+ *
+ * LICENSE: Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met: Redistributions of source code must retain the
+ * above copyright notice, this list of conditions and the following
+ * disclaimer. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * @category
+ * @package Services_JSON
+ * @author Michal Migurski <mike-json@teczno.com>
+ * @author Matt Knapp <mdknapp[at]gmail[dot]com>
+ * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
+ * @copyright 2005 Michal Migurski
+ * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $
+ * @license http://www.opensource.org/licenses/bsd-license.php
+ * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198
+ */
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_SLICE', 1);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_STR', 2);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_ARR', 3);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_OBJ', 4);
+
+/**
+ * Marker constant for Services_JSON::decode(), used to flag stack state
+ */
+define('SERVICES_JSON_IN_CMT', 5);
+
+/**
+ * Behavior switch for Services_JSON::decode()
+ */
+define('SERVICES_JSON_LOOSE_TYPE', 16);
+
+/**
+ * Behavior switch for Services_JSON::decode()
+ */
+define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
+
+/**
+ * Converts to and from JSON format.
+ *
+ * Brief example of use:
+ *
+ * <code>
+ * // create a new instance of Services_JSON
+ * $json = new Services_JSON();
+ *
+ * // convert a complexe value to JSON notation, and send it to the browser
+ * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
+ * $output = $json->encode($value);
+ *
+ * print($output);
+ * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
+ *
+ * // accept incoming POST data, assumed to be in JSON notation
+ * $input = file_get_contents('php://input', 1000000);
+ * $value = $json->decode($input);
+ * </code>
+ */
+class Services_JSON
+{
+ /**
+ * constructs a new JSON instance
+ *
+ * @param int $use object behavior flags; combine with boolean-OR
+ *
+ * possible values:
+ * - SERVICES_JSON_LOOSE_TYPE: loose typing.
+ * "{...}" syntax creates associative arrays
+ * instead of objects in decode().
+ * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
+ * Values which can't be encoded (e.g. resources)
+ * appear as NULL instead of throwing errors.
+ * By default, a deeply-nested resource will
+ * bubble up with an error, so all return values
+ * from encode() should be checked with isError()
+ */
+ function Services_JSON($use = 0)
+ {
+ $this->use = $use;
+ }
+
+ /**
+ * convert a string from one UTF-16 char to one UTF-8 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf16 UTF-16 character
+ * @return string UTF-8 character
+ * @access private
+ */
+ function utf162utf8($utf16)
+ {
+ // oh please oh please oh please oh please oh please
+ if(function_exists('mb_convert_encoding')) {
+ return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
+ }
+
+ $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
+
+ switch(true) {
+ case ((0x7F & $bytes) == $bytes):
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x7F & $bytes);
+
+ case (0x07FF & $bytes) == $bytes:
+ // return a 2-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xC0 | (($bytes >> 6) & 0x1F))
+ . chr(0x80 | ($bytes & 0x3F));
+
+ case (0xFFFF & $bytes) == $bytes:
+ // return a 3-byte UTF-8 character
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0xE0 | (($bytes >> 12) & 0x0F))
+ . chr(0x80 | (($bytes >> 6) & 0x3F))
+ . chr(0x80 | ($bytes & 0x3F));
+ }
+
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * convert a string from one UTF-8 char to one UTF-16 char
+ *
+ * Normally should be handled by mb_convert_encoding, but
+ * provides a slower PHP-only method for installations
+ * that lack the multibye string extension.
+ *
+ * @param string $utf8 UTF-8 character
+ * @return string UTF-16 character
+ * @access private
+ */
+ function utf82utf16($utf8)
+ {
+ // oh please oh please oh please oh please oh please
+ if(function_exists('mb_convert_encoding')) {
+ return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
+ }
+
+ switch(strlen($utf8)) {
+ case 1:
+ // this case should never be reached, because we are in ASCII range
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return $utf8;
+
+ case 2:
+ // return a UTF-16 character from a 2-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr(0x07 & (ord($utf8{0}) >> 2))
+ . chr((0xC0 & (ord($utf8{0}) << 6))
+ | (0x3F & ord($utf8{1})));
+
+ case 3:
+ // return a UTF-16 character from a 3-byte UTF-8 char
+ // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ return chr((0xF0 & (ord($utf8{0}) << 4))
+ | (0x0F & (ord($utf8{1}) >> 2)))
+ . chr((0xC0 & (ord($utf8{1}) << 6))
+ | (0x7F & ord($utf8{2})));
+ }
+
+ // ignoring UTF-32 for now, sorry
+ return '';
+ }
+
+ /**
+ * encodes an arbitrary variable into JSON format
+ *
+ * @param mixed $var any number, boolean, string, array, or object to be encoded.
+ * see argument 1 to Services_JSON() above for array-parsing behavior.
+ * if var is a strng, note that encode() always expects it
+ * to be in ASCII or UTF-8 format!
+ *
+ * @return mixed JSON string representation of input var or an error if a problem occurs
+ * @access public
+ */
+ function encode($var)
+ {
+ switch (gettype($var)) {
+ case 'boolean':
+ return $var ? 'true' : 'false';
+
+ case 'NULL':
+ return 'null';
+
+ case 'integer':
+ return (int) $var;
+
+ case 'double':
+ case 'float':
+ return (float) $var;
+
+ case 'string':
+ // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
+ $ascii = '';
+ $strlen_var = strlen($var);
+
+ /*
+ * Iterate over every character in the string,
+ * escaping with a slash or encoding to UTF-8 where necessary
+ */
+ for ($c = 0; $c < $strlen_var; ++$c) {
+
+ $ord_var_c = ord($var{$c});
+
+ switch (true) {
+ case $ord_var_c == 0x08:
+ $ascii .= '\b';
+ break;
+ case $ord_var_c == 0x09:
+ $ascii .= '\t';
+ break;
+ case $ord_var_c == 0x0A:
+ $ascii .= '\n';
+ break;
+ case $ord_var_c == 0x0C:
+ $ascii .= '\f';
+ break;
+ case $ord_var_c == 0x0D:
+ $ascii .= '\r';
+ break;
+
+ case $ord_var_c == 0x22:
+ case $ord_var_c == 0x2F:
+ case $ord_var_c == 0x5C:
+ // double quote, slash, slosh
+ $ascii .= '\\'.$var{$c};
+ break;
+
+ case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
+ // characters U-00000000 - U-0000007F (same as ASCII)
+ $ascii .= $var{$c};
+ break;
+
+ case (($ord_var_c & 0xE0) == 0xC0):
+ // characters U-00000080 - U-000007FF, mask 110XXXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
+ $c += 1;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xF0) == 0xE0):
+ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}));
+ $c += 2;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xF8) == 0xF0):
+ // characters U-00010000 - U-001FFFFF, mask 11110XXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}));
+ $c += 3;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xFC) == 0xF8):
+ // characters U-00200000 - U-03FFFFFF, mask 111110XX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}),
+ ord($var{$c + 4}));
+ $c += 4;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+
+ case (($ord_var_c & 0xFE) == 0xFC):
+ // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $char = pack('C*', $ord_var_c,
+ ord($var{$c + 1}),
+ ord($var{$c + 2}),
+ ord($var{$c + 3}),
+ ord($var{$c + 4}),
+ ord($var{$c + 5}));
+ $c += 5;
+ $utf16 = $this->utf82utf16($char);
+ $ascii .= sprintf('\u%04s', bin2hex($utf16));
+ break;
+ }
+ }
+
+ return '"'.$ascii.'"';
+
+ case 'array':
+ /*
+ * As per JSON spec if any array key is not an integer
+ * we must treat the the whole array as an object. We
+ * also try to catch a sparsely populated associative
+ * array with numeric keys here because some JS engines
+ * will create an array with empty indexes up to
+ * max_index which can cause memory issues and because
+ * the keys, which may be relevant, will be remapped
+ * otherwise.
+ *
+ * As per the ECMA and JSON specification an object may
+ * have any string as a property. Unfortunately due to
+ * a hole in the ECMA specification if the key is a
+ * ECMA reserved word or starts with a digit the
+ * parameter is only accessible using ECMAScript's
+ * bracket notation.
+ */
+
+ // treat as a JSON object
+ if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($var),
+ array_values($var));
+
+ foreach($properties as $property) {
+ if(Services_JSON::isError($property)) {
+ return $property;
+ }
+ }
+
+ return '{' . join(',', $properties) . '}';
+ }
+
+ // treat it like a regular array
+ $elements = array_map(array($this, 'encode'), $var);
+
+ foreach($elements as $element) {
+ if(Services_JSON::isError($element)) {
+ return $element;
+ }
+ }
+
+ return '[' . join(',', $elements) . ']';
+
+ case 'object':
+ $vars = get_object_vars($var);
+
+ $properties = array_map(array($this, 'name_value'),
+ array_keys($vars),
+ array_values($vars));
+
+ foreach($properties as $property) {
+ if(Services_JSON::isError($property)) {
+ return $property;
+ }
+ }
+
+ return '{' . join(',', $properties) . '}';
+
+ default:
+ return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
+ ? 'null'
+ : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
+ }
+ }
+
+ /**
+ * array-walking function for use in generating JSON-formatted name-value pairs
+ *
+ * @param string $name name of key to use
+ * @param mixed $value reference to an array element to be encoded
+ *
+ * @return string JSON-formatted name-value pair, like '"name":value'
+ * @access private
+ */
+ function name_value($name, $value)
+ {
+ $encoded_value = $this->encode($value);
+
+ if(Services_JSON::isError($encoded_value)) {
+ return $encoded_value;
+ }
+
+ return $this->encode(strval($name)) . ':' . $encoded_value;
+ }
+
+ /**
+ * reduce a string by removing leading and trailing comments and whitespace
+ *
+ * @param $str string string value to strip of comments and whitespace
+ *
+ * @return string string value stripped of comments and whitespace
+ * @access private
+ */
+ function reduce_string($str)
+ {
+ $str = preg_replace(array(
+
+ // eliminate single line comments in '// ...' form
+ '#^\s*//(.+)$#m',
+
+ // eliminate multi-line comments in '/* ... */' form, at start of string
+ '#^\s*/\*(.+)\*/#Us',
+
+ // eliminate multi-line comments in '/* ... */' form, at end of string
+ '#/\*(.+)\*/\s*$#Us'
+
+ ), '', $str);
+
+ // eliminate extraneous space
+ return trim($str);
+ }
+
+ /**
+ * decodes a JSON string into appropriate variable
+ *
+ * @param string $str JSON-formatted string
+ *
+ * @return mixed number, boolean, string, array, or object
+ * corresponding to given JSON input string.
+ * See argument 1 to Services_JSON() above for object-output behavior.
+ * Note that decode() always returns strings
+ * in ASCII or UTF-8 format!
+ * @access public
+ */
+ function decode($str)
+ {
+ $str = $this->reduce_string($str);
+
+ switch (strtolower($str)) {
+ case 'true':
+ return true;
+
+ case 'false':
+ return false;
+
+ case 'null':
+ return null;
+
+ default:
+ $m = array();
+
+ if (is_numeric($str)) {
+ // Lookie-loo, it's a number
+
+ // This would work on its own, but I'm trying to be
+ // good about returning integers where appropriate:
+ // return (float)$str;
+
+ // Return float or int, as appropriate
+ return ((float)$str == (integer)$str)
+ ? (integer)$str
+ : (float)$str;
+
+ } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
+ // STRINGS RETURNED IN UTF-8 FORMAT
+ $delim = substr($str, 0, 1);
+ $chrs = substr($str, 1, -1);
+ $utf8 = '';
+ $strlen_chrs = strlen($chrs);
+
+ for ($c = 0; $c < $strlen_chrs; ++$c) {
+
+ $substr_chrs_c_2 = substr($chrs, $c, 2);
+ $ord_chrs_c = ord($chrs{$c});
+
+ switch (true) {
+ case $substr_chrs_c_2 == '\b':
+ $utf8 .= chr(0x08);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\t':
+ $utf8 .= chr(0x09);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\n':
+ $utf8 .= chr(0x0A);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\f':
+ $utf8 .= chr(0x0C);
+ ++$c;
+ break;
+ case $substr_chrs_c_2 == '\r':
+ $utf8 .= chr(0x0D);
+ ++$c;
+ break;
+
+ case $substr_chrs_c_2 == '\\"':
+ case $substr_chrs_c_2 == '\\\'':
+ case $substr_chrs_c_2 == '\\\\':
+ case $substr_chrs_c_2 == '\\/':
+ if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
+ ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
+ $utf8 .= $chrs{++$c};
+ }
+ break;
+
+ case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
+ // single, escaped unicode character
+ $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
+ . chr(hexdec(substr($chrs, ($c + 4), 2)));
+ $utf8 .= $this->utf162utf8($utf16);
+ $c += 5;
+ break;
+
+ case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
+ $utf8 .= $chrs{$c};
+ break;
+
+ case ($ord_chrs_c & 0xE0) == 0xC0:
+ // characters U-00000080 - U-000007FF, mask 110XXXXX
+ //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 2);
+ ++$c;
+ break;
+
+ case ($ord_chrs_c & 0xF0) == 0xE0:
+ // characters U-00000800 - U-0000FFFF, mask 1110XXXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 3);
+ $c += 2;
+ break;
+
+ case ($ord_chrs_c & 0xF8) == 0xF0:
+ // characters U-00010000 - U-001FFFFF, mask 11110XXX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 4);
+ $c += 3;
+ break;
+
+ case ($ord_chrs_c & 0xFC) == 0xF8:
+ // characters U-00200000 - U-03FFFFFF, mask 111110XX
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 5);
+ $c += 4;
+ break;
+
+ case ($ord_chrs_c & 0xFE) == 0xFC:
+ // characters U-04000000 - U-7FFFFFFF, mask 1111110X
+ // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
+ $utf8 .= substr($chrs, $c, 6);
+ $c += 5;
+ break;
+
+ }
+
+ }
+
+ return $utf8;
+
+ } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
+ // array, or object notation
+
+ if ($str{0} == '[') {
+ $stk = array(SERVICES_JSON_IN_ARR);
+ $arr = array();
+ } else {
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $stk = array(SERVICES_JSON_IN_OBJ);
+ $obj = array();
+ } else {
+ $stk = array(SERVICES_JSON_IN_OBJ);
+ $obj = new stdClass();
+ }
+ }
+
+ array_push($stk, array('what' => SERVICES_JSON_SLICE,
+ 'where' => 0,
+ 'delim' => false));
+
+ $chrs = substr($str, 1, -1);
+ $chrs = $this->reduce_string($chrs);
+
+ if ($chrs == '') {
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ return $arr;
+
+ } else {
+ return $obj;
+
+ }
+ }
+
+ //print("\nparsing {$chrs}\n");
+
+ $strlen_chrs = strlen($chrs);
+
+ for ($c = 0; $c <= $strlen_chrs; ++$c) {
+
+ $top = end($stk);
+ $substr_chrs_c_2 = substr($chrs, $c, 2);
+
+ if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
+ // found a comma that is not inside a string, array, etc.,
+ // OR we've reached the end of the character list
+ $slice = substr($chrs, $top['where'], ($c - $top['where']));
+ array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
+ //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ // we are in an array, so just push an element onto the stack
+ array_push($arr, $this->decode($slice));
+
+ } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+ // we are in an object, so figure
+ // out the property name and set an
+ // element in an associative array,
+ // for now
+ $parts = array();
+
+ if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+ // "name":value pair
+ $key = $this->decode($parts[1]);
+ $val = $this->decode($parts[2]);
+
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $obj[$key] = $val;
+ } else {
+ $obj->$key = $val;
+ }
+ } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
+ // name:value pair, where name is unquoted
+ $key = $parts[1];
+ $val = $this->decode($parts[2]);
+
+ if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
+ $obj[$key] = $val;
+ } else {
+ $obj->$key = $val;
+ }
+ }
+
+ }
+
+ } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
+ // found a quote, and we are not inside a string
+ array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
+ //print("Found start of string at {$c}\n");
+
+ } elseif (($chrs{$c} == $top['delim']) &&
+ ($top['what'] == SERVICES_JSON_IN_STR) &&
+ ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) {
+ // found a quote, we're in a string, and it's not escaped
+ // we know that it's not escaped becase there is _not_ an
+ // odd number of backslashes at the end of the string so far
+ array_pop($stk);
+ //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
+
+ } elseif (($chrs{$c} == '[') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a left-bracket, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
+ //print("Found start of array at {$c}\n");
+
+ } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
+ // found a right-bracket, and we're in an array
+ array_pop($stk);
+ //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ } elseif (($chrs{$c} == '{') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a left-brace, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
+ //print("Found start of object at {$c}\n");
+
+ } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
+ // found a right-brace, and we're in an object
+ array_pop($stk);
+ //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ } elseif (($substr_chrs_c_2 == '/*') &&
+ in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
+ // found a comment start, and we are in an array, object, or slice
+ array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
+ $c++;
+ //print("Found start of comment at {$c}\n");
+
+ } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
+ // found a comment end, and we're in one now
+ array_pop($stk);
+ $c++;
+
+ for ($i = $top['where']; $i <= $c; ++$i)
+ $chrs = substr_replace($chrs, ' ', $i, 1);
+
+ //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
+
+ }
+
+ }
+
+ if (reset($stk) == SERVICES_JSON_IN_ARR) {
+ return $arr;
+
+ } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
+ return $obj;
+
+ }
+
+ }
+ }
+ }
+
+ /**
+ * @todo Ultimately, this should just call PEAR::isError()
+ */
+ function isError($data, $code = null)
+ {
+ if (class_exists('pear')) {
+ return PEAR::isError($data, $code);
+ } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
+ is_subclass_of($data, 'services_json_error'))) {
+ return true;
+ }
+
+ return false;
+ }
+}
+
+if (class_exists('PEAR_Error')) {
+
+ class Services_JSON_Error extends PEAR_Error
+ {
+ function Services_JSON_Error($message = 'unknown error', $code = null,
+ $mode = null, $options = null, $userinfo = null)
+ {
+ parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
+ }
+ }
+
+} else {
+
+ /**
+ * @todo Ultimately, this class shall be descended from PEAR_Error
+ */
+ class Services_JSON_Error
+ {
+ function Services_JSON_Error($message = 'unknown error', $code = null,
+ $mode = null, $options = null, $userinfo = null)
+ {
+
+ }
+ }
+
+}
+
+?>
diff --git a/plugins/Facebook/facebook/jsonwrapper/JSON/LICENSE b/plugins/Facebook/facebook/jsonwrapper/JSON/LICENSE
new file mode 100644
index 000000000..4ae6bef55
--- /dev/null
+++ b/plugins/Facebook/facebook/jsonwrapper/JSON/LICENSE
@@ -0,0 +1,21 @@
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/Facebook/facebook/jsonwrapper/jsonwrapper.php b/plugins/Facebook/facebook/jsonwrapper/jsonwrapper.php
new file mode 100644
index 000000000..29509deba
--- /dev/null
+++ b/plugins/Facebook/facebook/jsonwrapper/jsonwrapper.php
@@ -0,0 +1,6 @@
+<?php
+# In PHP 5.2 or higher we don't need to bring this in
+if (!function_exists('json_encode')) {
+ require_once 'jsonwrapper_inner.php';
+}
+?>
diff --git a/plugins/Facebook/facebook/jsonwrapper/jsonwrapper_inner.php b/plugins/Facebook/facebook/jsonwrapper/jsonwrapper_inner.php
new file mode 100644
index 000000000..36a3f2863
--- /dev/null
+++ b/plugins/Facebook/facebook/jsonwrapper/jsonwrapper_inner.php
@@ -0,0 +1,23 @@
+<?php
+
+require_once 'JSON/JSON.php';
+
+function json_encode($arg)
+{
+ global $services_json;
+ if (!isset($services_json)) {
+ $services_json = new Services_JSON();
+ }
+ return $services_json->encode($arg);
+}
+
+function json_decode($arg)
+{
+ global $services_json;
+ if (!isset($services_json)) {
+ $services_json = new Services_JSON();
+ }
+ return $services_json->decode($arg);
+}
+
+?>
diff --git a/plugins/Facebook/facebookaction.php b/plugins/Facebook/facebookaction.php
new file mode 100644
index 000000000..c852bbf5e
--- /dev/null
+++ b/plugins/Facebook/facebookaction.php
@@ -0,0 +1,648 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Base Facebook Action
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Faceboook
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
+require_once INSTALLDIR . '/lib/noticeform.php';
+
+class FacebookAction extends Action
+{
+
+ var $facebook = null;
+ var $fbuid = null;
+ var $flink = null;
+ var $action = null;
+ var $app_uri = null;
+ var $app_name = null;
+
+ function __construct($output='php://output', $indent=true, $facebook=null, $flink=null)
+ {
+ parent::__construct($output, $indent);
+
+ $this->facebook = $facebook;
+ $this->flink = $flink;
+
+ if ($this->flink) {
+ $this->fbuid = $flink->foreign_id;
+ $this->user = $flink->getUser();
+ }
+
+ $this->args = array();
+ }
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $this->facebook = getFacebook();
+ $this->fbuid = $this->facebook->require_login();
+
+ $this->action = $this->trimmed('action');
+
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(
+ array('canvas_name', 'application_name'));
+
+ $this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name'];
+ $this->app_name = $app_props['application_name'];
+
+ $this->flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
+
+ return true;
+
+ }
+
+ function showStylesheets()
+ {
+ $this->cssLink('css/display.css', 'base');
+ $this->cssLink('css/display.css', null, 'screen, projection, tv');
+ $this->cssLink('plugins/Facebook/facebookapp.css');
+ }
+
+ function showScripts()
+ {
+ $this->script('js/facebookapp.js');
+ }
+
+ /**
+ * Start an Facebook ready HTML document
+ *
+ * For Facebook we don't want to actually output any headers,
+ * DTD info, etc. Just Stylesheet and JavaScript links.
+ *
+ * @param string $type MIME type to use; default is to do negotation.
+ *
+ * @return void
+ */
+
+ function startHTML($type=null)
+ {
+ $this->showStylesheets();
+ $this->showScripts();
+
+ $this->elementStart('div', array('class' => 'facebook-page'));
+ }
+
+ /**
+ * Ends a Facebook ready HTML document
+ *
+ * @return void
+ */
+ function endHTML()
+ {
+ $this->elementEnd('div');
+ $this->endXML();
+ }
+
+ /**
+ * Show notice form.
+ *
+ * @return nothing
+ */
+ function showNoticeForm()
+ {
+ // don't do it for most of the Facebook pages
+ }
+
+ function showBody()
+ {
+ $this->elementStart('div', array('id' => 'wrap'));
+ $this->showHeader();
+ $this->showCore();
+ $this->showFooter();
+ $this->elementEnd('div');
+ }
+
+ function showHead($error, $success)
+ {
+
+ if ($error) {
+ $this->element("h1", null, $error);
+ }
+
+ if ($success) {
+ $this->element("h1", null, $success);
+ }
+
+ $this->elementStart('fb:if-section-not-added', array('section' => 'profile'));
+ $this->elementStart('span', array('id' => 'add_to_profile'));
+ $this->element('fb:add-section-button', array('section' => 'profile'));
+ $this->elementEnd('span');
+ $this->elementEnd('fb:if-section-not-added');
+
+ }
+
+ // Make this into a widget later
+ function showLocalNav()
+ {
+ $this->elementStart('ul', array('class' => 'nav'));
+
+ $this->elementStart('li', array('class' =>
+ ($this->action == 'facebookhome') ? 'current' : 'facebook_home'));
+ $this->element('a',
+ array('href' => 'index.php', 'title' => _('Home')), _('Home'));
+ $this->elementEnd('li');
+
+ if (common_config('invite', 'enabled')) {
+ $this->elementStart('li',
+ array('class' =>
+ ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite'));
+ $this->element('a',
+ array('href' => 'invite.php', 'title' => _('Invite')), _('Invite'));
+ $this->elementEnd('li');
+ }
+
+ $this->elementStart('li',
+ array('class' =>
+ ($this->action == 'facebooksettings') ? 'current' : 'facebook_settings'));
+ $this->element('a',
+ array('href' => 'settings.php',
+ 'title' => _('Settings')), _('Settings'));
+ $this->elementEnd('li');
+
+ $this->elementEnd('ul');
+ }
+
+ /**
+ * Show header of the page.
+ *
+ * @return nothing
+ */
+ function showHeader()
+ {
+ $this->elementStart('div', array('id' => 'header'));
+ $this->showLogo();
+ $this->showNoticeForm();
+ $this->elementEnd('div');
+ }
+
+ /**
+ * Show page, a template method.
+ *
+ * @return nothing
+ */
+ function showPage($error = null, $success = null)
+ {
+ $this->startHTML();
+ $this->showHead($error, $success);
+ $this->showBody();
+ $this->endHTML();
+ }
+
+ function showInstructions()
+ {
+
+ $this->elementStart('div', array('class' => 'facebook_guide'));
+
+ $this->elementStart('dl', array('class' => 'system_notice'));
+ $this->element('dt', null, 'Page Notice');
+
+ $loginmsg_part1 = _('To use the %s Facebook Application you need to login ' .
+ 'with your username and password. Don\'t have a username yet? ');
+ $loginmsg_part2 = _(' a new account.');
+
+ $this->elementStart('dd');
+ $this->elementStart('p');
+ $this->text(sprintf($loginmsg_part1, common_config('site', 'name')));
+ $this->element('a',
+ array('href' => common_local_url('register')), _('Register'));
+ $this->text($loginmsg_part2);
+ $this->elementEnd('p');
+ $this->elementEnd('dd');
+
+ $this->elementEnd('dl');
+ $this->elementEnd('div');
+ }
+
+ function showLoginForm($msg = null)
+ {
+
+ $this->elementStart('div', array('id' => 'content'));
+ $this->element('h1', null, _('Login'));
+
+ if ($msg) {
+ $this->element('fb:error', array('message' => $msg));
+ }
+
+ $this->showInstructions();
+
+ $this->elementStart('div', array('id' => 'content_inner'));
+
+ $this->elementStart('form', array('method' => 'post',
+ 'class' => 'form_settings',
+ 'id' => 'login',
+ 'action' => 'index.php'));
+
+ $this->elementStart('fieldset');
+
+ $this->elementStart('ul', array('class' => 'form_datas'));
+ $this->elementStart('li');
+ $this->input('nickname', _('Nickname'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->password('password', _('Password'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->submit('submit', _('Login'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+
+ $this->elementStart('p');
+ $this->element('a', array('href' => common_local_url('recoverpassword')),
+ _('Lost or forgotten password?'));
+ $this->elementEnd('p');
+
+ $this->elementEnd('div');
+ $this->elementEnd('div');
+
+ }
+
+ function updateProfileBox($notice)
+ {
+
+ // Need to include inline CSS for styling the Profile box
+
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(array('icon_url'));
+ $icon_url = $app_props['icon_url'];
+
+ $style = '<style>
+ .entry-title *,
+ .entry-content * {
+ font-size:14px;
+ font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
+ }
+ .entry-title a,
+ .entry-content a {
+ color:#002E6E;
+ }
+
+ .entry-title .vcard .photo {
+ float:left;
+ display:inline;
+ margin-right:11px;
+ margin-bottom:11px
+ }
+ .entry-title {
+ margin-bottom:11px;
+ }
+ .entry-title p.entry-content {
+ display:inline;
+ margin-left:5px;
+ }
+
+ div.entry-content {
+ clear:both;
+ }
+ div.entry-content dl,
+ div.entry-content dt,
+ div.entry-content dd {
+ display:inline;
+ text-transform:lowercase;
+ }
+
+ div.entry-content dd,
+ div.entry-content .device dt {
+ margin-left:0;
+ margin-right:5px;
+ }
+ div.entry-content dl.timestamp dt,
+ div.entry-content dl.response dt {
+ display:none;
+ }
+ div.entry-content dd a {
+ display:inline-block;
+ }
+
+ #facebook_statusnet_app {
+ text-indent:-9999px;
+ height:16px;
+ width:16px;
+ display:block;
+ background:url('.$icon_url.') no-repeat 0 0;
+ float:right;
+ }
+ </style>';
+
+ $this->xw->openMemory();
+
+ $item = new FacebookProfileBoxNotice($notice, $this);
+ $item->show();
+
+ $fbml = "<fb:wide>$style " . $this->xw->outputMemory(false) . "</fb:wide>";
+ $fbml .= "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>";
+
+ $fbml_main = "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>";
+
+ $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main);
+
+ $this->xw->openURI('php://output');
+ }
+
+ /**
+ * Generate pagination links
+ *
+ * @param boolean $have_before is there something before?
+ * @param boolean $have_after is there something after?
+ * @param integer $page current page
+ * @param string $action current action
+ * @param array $args rest of query arguments
+ *
+ * @return nothing
+ */
+ function pagination($have_before, $have_after, $page, $action, $args=null)
+ {
+ // Does a little before-after block for next/prev page
+ if ($have_before || $have_after) {
+ $this->elementStart('dl', 'pagination');
+ $this->element('dt', null, _('Pagination'));
+ $this->elementStart('dd', null);
+ $this->elementStart('ul', array('class' => 'nav'));
+ }
+ if ($have_before) {
+ $pargs = array('page' => $page-1);
+ $newargs = $args ? array_merge($args, $pargs) : $pargs;
+ $this->elementStart('li', array('class' => 'nav_prev'));
+ $this->element('a', array('href' => "$this->app_uri/$action?page=$newargs[page]", 'rel' => 'prev'),
+ _('After'));
+ $this->elementEnd('li');
+ }
+ if ($have_after) {
+ $pargs = array('page' => $page+1);
+ $newargs = $args ? array_merge($args, $pargs) : $pargs;
+ $this->elementStart('li', array('class' => 'nav_next'));
+ $this->element('a', array('href' => "$this->app_uri/$action?page=$newargs[page]", 'rel' => 'next'),
+ _('Before'));
+ $this->elementEnd('li');
+ }
+ if ($have_before || $have_after) {
+ $this->elementEnd('ul');
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+ }
+ }
+
+ function saveNewNotice()
+ {
+
+ $user = $this->flink->getUser();
+
+ $content = $this->trimmed('status_textarea');
+
+ if (!$content) {
+ $this->showPage(_('No notice content!'));
+ return;
+ } else {
+ $content_shortened = common_shorten_links($content);
+
+ if (Notice::contentTooLong($content_shortened)) {
+ $this->showPage(sprintf(_('That\'s too long. Max notice size is %d chars.'),
+ Notice::maxContent()));
+ return;
+ }
+ }
+
+ $inter = new CommandInterpreter();
+
+ $cmd = $inter->handle_command($user, $content_shortened);
+
+ if ($cmd) {
+
+ // XXX fix this
+
+ $cmd->execute(new WebChannel());
+ return;
+ }
+
+ $replyto = $this->trimmed('inreplyto');
+
+ try {
+ $notice = Notice::saveNew($user->id, $content,
+ 'web', 1, ($replyto == 'false') ? null : $replyto);
+ } catch (Exception $e) {
+ $this->showPage($e->getMessage());
+ return;
+ }
+
+ common_broadcast_notice($notice);
+
+ // Also update the user's Facebook status
+ facebookBroadcastNotice($notice);
+
+ }
+
+}
+
+class FacebookNoticeForm extends NoticeForm
+{
+
+ var $post_action = null;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output channel
+ * @param string $action action to return to, if any
+ * @param string $content content to pre-fill
+ */
+
+ function __construct($out=null, $action=null, $content=null,
+ $post_action=null, $user=null)
+ {
+ parent::__construct($out, $action, $content, $user);
+ $this->post_action = $post_action;
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return $this->post_action;
+ }
+
+}
+
+class FacebookNoticeList extends NoticeList
+{
+
+ /**
+ * constructor
+ *
+ * @param Notice $notice stream of notices from DB_DataObject
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
+ /**
+ * show the list of notices
+ *
+ * "Uses up" the stream by looping through it. So, probably can't
+ * be called twice on the same list.
+ *
+ * @return int count of notices listed.
+ */
+
+ function show()
+ {
+ $this->out->elementStart('div', array('id' =>'notices_primary'));
+ $this->out->element('h2', null, _('Notices'));
+ $this->out->elementStart('ul', array('class' => 'notices'));
+
+ $cnt = 0;
+
+ while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) {
+ $cnt++;
+
+ if ($cnt > NOTICES_PER_PAGE) {
+ break;
+ }
+
+ $item = $this->newListItem($this->notice);
+ $item->show();
+ }
+
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('div');
+
+ return $cnt;
+ }
+
+ /**
+ * returns a new list item for the current notice
+ *
+ * Overridden to return a Facebook specific list item.
+ *
+ * @param Notice $notice the current notice
+ *
+ * @return FacebookNoticeListItem a list item for displaying the notice
+ * formatted for display in the Facebook App.
+ */
+
+ function newListItem($notice)
+ {
+ return new FacebookNoticeListItem($notice, $this);
+ }
+
+}
+
+class FacebookNoticeListItem extends NoticeListItem
+{
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
+ /**
+ * recipe function for displaying a single notice in the Facebook App.
+ *
+ * Overridden to strip out some of the controls that we don't
+ * want to be available.
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->showStart();
+ $this->showNotice();
+ $this->showNoticeInfo();
+
+ // XXX: Need to update to show attachements and controls
+
+ $this->showEnd();
+ }
+
+}
+
+class FacebookProfileBoxNotice extends FacebookNoticeListItem
+{
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
+ /**
+ * Recipe function for displaying a single notice in the
+ * Facebook App profile notice box
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->showNotice();
+ $this->showNoticeInfo();
+ $this->showAppLink();
+ }
+
+ function showAppLink()
+ {
+
+ $this->facebook = getFacebook();
+
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(
+ array('canvas_name', 'application_name'));
+
+ $this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name'];
+ $this->app_name = $app_props['application_name'];
+
+ $this->out->elementStart('a', array('id' => 'facebook_statusnet_app',
+ 'href' => $this->app_uri));
+ $this->out->text($this->app_name);
+ $this->out->elementEnd('a');
+ }
+
+}
diff --git a/plugins/Facebook/facebookapp.css b/plugins/Facebook/facebookapp.css
new file mode 100644
index 000000000..8cd06f78a
--- /dev/null
+++ b/plugins/Facebook/facebookapp.css
@@ -0,0 +1,115 @@
+* {
+font-size:14px;
+font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+
+#wrap {
+background-color:#F0F2F5;
+padding-left:1.795%;
+padding-right:1.795%;
+width:auto;
+}
+
+p,label,
+h1,h2,h3,h4,h5,h6 {
+color:#000;
+}
+
+#header {
+width:131%;
+}
+
+#content {
+width:92.7%;
+}
+
+#aside_primary {
+display:none;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
+#site_nav_local_views a:hover {
+background-color:#FAFBFC;
+}
+
+#form_notice .form_note + label,
+#form_notice #notice_data-attach {
+display:none;
+}
+
+#form_notice #notice_action-submit {
+height:47px !important;
+}
+
+
+span.facebook-button {
+border: 2px solid #aaa;
+padding: 3px;
+display: block;
+float: left;
+margin-right: 20px;
+-moz-border-radius: 4px;
+border-radius:4px;
+-webkit-border-radius:4px;
+font-weight: bold;
+background-color:#A9BF4F;
+color:#fff;
+font-size:1.2em
+}
+
+span.facebook-button a { color:#fff }
+
+.facebook_guide {
+margin-bottom:18px;
+}
+.facebook_guide p {
+font-weight:bold;
+}
+
+
+input {
+height:auto !important;
+}
+
+#facebook-friends {
+float:left;
+width:100%;
+}
+
+#facebook-friends li {
+float:left;
+margin-right:2%;
+margin-bottom:11px;
+width:18%;
+height:115px;
+}
+#facebook-friends li a {
+float:left;
+}
+
+#add_to_profile {
+position:absolute;
+right:18px;
+top:10px;
+z-index:2;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+margin-right:5px;
+}
+
+#content_inner p {
+margin-bottom:18px;
+}
+
+#content_inner ul {
+list-style-type:none;
+}
+
+.form_settings label {
+margin-right:18px;
+}
diff --git a/plugins/Facebook/facebookhome.php b/plugins/Facebook/facebookhome.php
new file mode 100644
index 000000000..ea141c2c2
--- /dev/null
+++ b/plugins/Facebook/facebookhome.php
@@ -0,0 +1,275 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+
+class FacebookhomeAction extends FacebookAction
+{
+
+ var $page = null;
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $this->page = $this->trimmed('page');
+
+ if (!$this->page) {
+ $this->page = 1;
+ }
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ // If the user has opted not to initially allow the app to have
+ // Facebook status update permission, store that preference. Only
+ // promt the user the first time she uses the app
+ if ($this->arg('skip') || $args['fb_sig_request_method'] == 'GET') {
+ $this->facebook->api_client->data_setUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF, 'true');
+ }
+
+ if ($this->flink) {
+
+ $this->user = $this->flink->getUser();
+
+ // If this is the first time the user has started the app
+ // prompt for Facebook status update permission
+ if (!$this->facebook->api_client->users_hasAppPermission('publish_stream')) {
+
+ if ($this->facebook->api_client->data_getUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF) != 'true') {
+ $this->getUpdatePermission();
+ return;
+ }
+ }
+
+ // Make sure the user's profile box has the lastest notice
+ $notice = $this->user->getCurrentNotice();
+ if ($notice) {
+ $this->updateProfileBox($notice);
+ }
+
+ if ($this->arg('status_submit') == 'Send') {
+ $this->saveNewNotice();
+ }
+
+ // User is authenticated and has already been prompted once for
+ // Facebook status update permission? Then show the main page
+ // of the app
+ $this->showPage();
+
+ } else {
+
+ // User hasn't authenticated yet, prompt for creds
+ $this->login();
+ }
+
+ }
+
+ function login()
+ {
+
+ $this->showStylesheets();
+
+ $nickname = common_canonical_nickname($this->trimmed('nickname'));
+ $password = $this->arg('password');
+
+ $msg = null;
+
+ if ($nickname) {
+
+ if (common_check_user($nickname, $password)) {
+
+ $user = User::staticGet('nickname', $nickname);
+
+ if (!$user) {
+ $this->showLoginForm(_("Server error - couldn't get user!"));
+ }
+
+ $flink = DB_DataObject::factory('foreign_link');
+ $flink->user_id = $user->id;
+ $flink->foreign_id = $this->fbuid;
+ $flink->service = FACEBOOK_SERVICE;
+ $flink->created = common_sql_now();
+ $flink->set_flags(true, false, false, false);
+
+ $flink_id = $flink->insert();
+
+ // XXX: Do some error handling here
+
+ $this->setDefaults();
+
+ $this->getUpdatePermission();
+ return;
+
+ } else {
+ $msg = _('Incorrect username or password.');
+ }
+ }
+
+ $this->showLoginForm($msg);
+ $this->showFooter();
+
+ }
+
+ function setDefaults()
+ {
+ $this->facebook->api_client->data_setUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF, 'false');
+ }
+
+ function showNoticeForm()
+ {
+ $post_action = "$this->app_uri/index.php";
+
+ $notice_form = new FacebookNoticeForm($this, $post_action, null,
+ $post_action, $this->user);
+ $notice_form->show();
+ }
+
+ function title()
+ {
+ if ($this->page > 1) {
+ return sprintf(_("%s and friends, page %d"), $this->user->nickname, $this->page);
+ } else {
+ return sprintf(_("%s and friends"), $this->user->nickname);
+ }
+ }
+
+ function showContent()
+ {
+ $notice = $this->user->noticeInbox(($this->page-1) * NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+
+ $nl = new NoticeList($notice, $this);
+
+ $cnt = $nl->show();
+
+ $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
+ $this->page, 'index.php', array('nickname' => $this->user->nickname));
+ }
+
+ function showNoticeList($notice)
+ {
+
+ $nl = new NoticeList($notice, $this);
+ return $nl->show();
+ }
+
+ function getUpdatePermission() {
+
+ $this->showStylesheets();
+
+ $this->elementStart('div', array('class' => 'facebook_guide'));
+
+ $instructions = sprintf(_('If you would like the %s app to automatically update ' .
+ 'your Facebook status with your latest notice, you need ' .
+ 'to give it permission.'), $this->app_name);
+
+ $this->elementStart('p');
+ $this->element('span', array('id' => 'permissions_notice'), $instructions);
+ $this->elementEnd('p');
+
+ $this->elementStart('form', array('method' => 'post',
+ 'action' => "index.php",
+ 'id' => 'facebook-skip-permissions'));
+
+ $this->elementStart('ul', array('id' => 'fb-permissions-list'));
+ $this->elementStart('li', array('id' => 'fb-permissions-item'));
+
+ $next = urlencode("$this->app_uri/index.php");
+ $api_key = common_config('facebook', 'apikey');
+
+ $auth_url = 'http://www.facebook.com/authorize.php?api_key=' .
+ $api_key . '&v=1.0&ext_perm=publish_stream&next=' . $next .
+ '&next_cancel=' . $next . '&submit=skip';
+
+ $this->elementStart('span', array('class' => 'facebook-button'));
+ $this->element('a', array('href' => $auth_url),
+ sprintf(_('Okay, do it!'), $this->app_name));
+ $this->elementEnd('span');
+
+ $this->elementEnd('li');
+
+ $this->elementStart('li', array('id' => 'fb-permissions-item'));
+ $this->submit('skip', _('Skip'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->elementEnd('form');
+ $this->elementEnd('div');
+
+ }
+
+ /**
+ * Generate pagination links
+ *
+ * @param boolean $have_before is there something before?
+ * @param boolean $have_after is there something after?
+ * @param integer $page current page
+ * @param string $action current action
+ * @param array $args rest of query arguments
+ *
+ * @return nothing
+ */
+ function pagination($have_before, $have_after, $page, $action, $args=null)
+ {
+
+ // Does a little before-after block for next/prev page
+
+ // XXX: Fix so this uses common_local_url() if possible.
+
+ if ($have_before || $have_after) {
+ $this->elementStart('dl', 'pagination');
+ $this->element('dt', null, _('Pagination'));
+ $this->elementStart('dd', null);
+ $this->elementStart('ul', array('class' => 'nav'));
+ }
+ if ($have_before) {
+ $pargs = array('page' => $page-1);
+ $newargs = $args ? array_merge($args, $pargs) : $pargs;
+ $this->elementStart('li', array('class' => 'nav_prev'));
+ $this->element('a', array('href' => "$action?page=$newargs[page]", 'rel' => 'prev'),
+ _('After'));
+ $this->elementEnd('li');
+ }
+ if ($have_after) {
+ $pargs = array('page' => $page+1);
+ $newargs = $args ? array_merge($args, $pargs) : $pargs;
+ $this->elementStart('li', array('class' => 'nav_next'));
+ $this->element('a', array('href' => "$action?page=$newargs[page]", 'rel' => 'next'),
+ _('Before'));
+ $this->elementEnd('li');
+ }
+ if ($have_before || $have_after) {
+ $this->elementEnd('ul');
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+ }
+ }
+
+}
diff --git a/plugins/Facebook/facebookinvite.php b/plugins/Facebook/facebookinvite.php
new file mode 100644
index 000000000..3380b4c85
--- /dev/null
+++ b/plugins/Facebook/facebookinvite.php
@@ -0,0 +1,146 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+
+class FacebookinviteAction extends FacebookAction
+{
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showForm();
+ }
+
+ /**
+ * Wrapper for showing a page
+ *
+ * Stores an error and shows the page
+ *
+ * @param string $error Error, if any
+ *
+ * @return void
+ */
+
+ function showForm($error=null)
+ {
+ $this->error = $error;
+ $this->showPage();
+ }
+
+ /**
+ * Show the page content
+ *
+ * Either shows the registration form or, if registration was successful,
+ * instructions for using the site.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ if ($this->arg('ids')) {
+ $this->showSuccessContent();
+ } else {
+ $this->showFormContent();
+ }
+ }
+
+ function showSuccessContent()
+ {
+
+ $this->element('h2', null, sprintf(_('Thanks for inviting your friends to use %s'),
+ common_config('site', 'name')));
+ $this->element('p', null, _('Invitations have been sent to the following users:'));
+
+ $friend_ids = $_POST['ids']; // XXX: Hmm... is this the best way to access the list?
+
+ $this->elementStart('ul', array('id' => 'facebook-friends'));
+
+ foreach ($friend_ids as $friend) {
+ $this->elementStart('li');
+ $this->element('fb:profile-pic', array('uid' => $friend, 'size' => 'square'));
+ $this->element('fb:name', array('uid' => $friend,
+ 'capitalize' => 'true'));
+ $this->elementEnd('li');
+ }
+
+ $this->elementEnd("ul");
+
+ }
+
+ function showFormContent()
+ {
+ $content = sprintf(_('You have been invited to %s'), common_config('site', 'name')) .
+ htmlentities('<fb:req-choice url="' . $this->app_uri . '" label="Add"/>');
+
+ $this->elementStart('fb:request-form', array('action' => 'invite.php',
+ 'method' => 'post',
+ 'invite' => 'true',
+ 'type' => common_config('site', 'name'),
+ 'content' => $content));
+ $this->hidden('invite', 'true');
+ $actiontext = sprintf(_('Invite your friends to use %s'), common_config('site', 'name'));
+
+ $multi_params = array('showborder' => 'false');
+ $multi_params['actiontext'] = $actiontext;
+ $multi_params['bypass'] = 'cancel';
+ $multi_params['cols'] = 4;
+
+ // Get a list of users who are already using the app for exclusion
+ $exclude_ids = $this->facebook->api_client->friends_getAppUsers();
+ $exclude_ids_csv = null;
+
+ // fbml needs these as a csv string, not an array
+ if ($exclude_ids) {
+ $exclude_ids_csv = implode(',', $exclude_ids);
+ $multi_params['exclude_ids'] = $exclude_ids_csv;
+ }
+
+ $this->element('fb:multi-friend-selector', $multi_params);
+ $this->elementEnd('fb:request-form');
+
+ if ($exclude_ids) {
+
+ $this->element('h2', null, sprintf(_('Friends already using %s:'),
+ common_config('site', 'name')));
+ $this->elementStart('ul', array('id' => 'facebook-friends'));
+
+ foreach ($exclude_ids as $friend) {
+ $this->elementStart('li');
+ $this->element('fb:profile-pic', array('uid' => $friend, 'size' => 'square'));
+ $this->element('fb:name', array('uid' => $friend,
+ 'capitalize' => 'true'));
+ $this->elementEnd('li');
+ }
+
+ $this->elementEnd("ul");
+ }
+ }
+
+ function title()
+ {
+ return sprintf(_('Send invitations'));
+ }
+
+}
diff --git a/plugins/Facebook/facebooklogin.php b/plugins/Facebook/facebooklogin.php
new file mode 100644
index 000000000..f77aecca3
--- /dev/null
+++ b/plugins/Facebook/facebooklogin.php
@@ -0,0 +1,99 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+
+class FacebookinviteAction extends FacebookAction
+{
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ $this->error = $error;
+
+ if ($this->flink) {
+ if (!$this->facebook->api_client->users_hasAppPermission('publish_stream') &&
+ $this->facebook->api_client->data_getUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF) == 'true') {
+
+ echo '<h1>REDIRECT TO HOME</h1>';
+ }
+ } else {
+ $this->showPage();
+ }
+ }
+
+ function showContent()
+ {
+
+ // If the user has opted not to initially allow the app to have
+ // Facebook status update permission, store that preference. Only
+ // promt the user the first time she uses the app
+ if ($this->arg('skip')) {
+ $this->facebook->api_client->data_setUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF, 'true');
+ }
+
+ if ($this->flink) {
+
+ $this->user = $this->flink->getUser();
+
+ // If this is the first time the user has started the app
+ // prompt for Facebook status update permission
+ if (!$this->facebook->api_client->users_hasAppPermission('publish_stream')) {
+
+ if ($this->facebook->api_client->data_getUserPreference(
+ FACEBOOK_PROMPTED_UPDATE_PREF) != 'true') {
+ $this->getUpdatePermission();
+ return;
+ }
+ }
+
+ } else {
+ $this->showLoginForm();
+ }
+
+ }
+
+ function showSuccessContent()
+ {
+
+ }
+
+ function showFormContent()
+ {
+
+ }
+
+ function title()
+ {
+ return sprintf(_('Login'));
+ }
+
+ function redirectHome()
+ {
+
+ }
+
+}
diff --git a/plugins/Facebook/facebookqueuehandler.php b/plugins/Facebook/facebookqueuehandler.php
new file mode 100755
index 000000000..e4ae7d4ee
--- /dev/null
+++ b/plugins/Facebook/facebookqueuehandler.php
@@ -0,0 +1,73 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_FACEBOOK_HELP
+Daemon script for pushing new notices to Facebook.
+
+ -i --id Identity (default none)
+
+END_OF_FACEBOOK_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php';
+require_once INSTALLDIR . '/lib/queuehandler.php';
+
+class FacebookQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'facebook';
+ }
+
+ function start()
+ {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ return facebookBroadcastNotice($notice);
+ }
+
+ function finish()
+ {
+ }
+
+}
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+$handler = new FacebookQueueHandler($id);
+
+$handler->runOnce();
diff --git a/plugins/Facebook/facebookremove.php b/plugins/Facebook/facebookremove.php
new file mode 100644
index 000000000..8531a8e6e
--- /dev/null
+++ b/plugins/Facebook/facebookremove.php
@@ -0,0 +1,69 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+
+class FacebookremoveAction extends FacebookAction
+{
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ $secret = common_config('facebook', 'secret');
+
+ $sig = '';
+
+ ksort($_POST);
+
+ foreach ($_POST as $key => $val) {
+ if (substr($key, 0, 7) == 'fb_sig_') {
+ $sig .= substr($key, 7) . '=' . $val;
+ }
+ }
+
+ $sig .= $secret;
+ $verify = md5($sig);
+
+ if ($verify == $this->arg('fb_sig')) {
+
+ $flink = Foreign_link::getByForeignID($this->arg('fb_sig_user'), 2);
+
+ common_debug("Removing foreign link to Facebook - local user ID: $flink->user_id, Facebook ID: $flink->foreign_id");
+
+ $result = $flink->delete();
+
+ if (!$result) {
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ $this->serverError(_('Couldn\'t remove Facebook user.'));
+ return;
+ }
+
+ } else {
+ # Someone bad tried to remove facebook link?
+ common_log(LOG_ERR, "Someone from $_SERVER[REMOTE_ADDR] " .
+ 'unsuccessfully tried to remove a foreign link to Facebook!');
+ }
+ }
+
+}
diff --git a/plugins/Facebook/facebooksettings.php b/plugins/Facebook/facebooksettings.php
new file mode 100644
index 000000000..2f182e368
--- /dev/null
+++ b/plugins/Facebook/facebooksettings.php
@@ -0,0 +1,159 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+
+class FacebooksettingsAction extends FacebookAction
+{
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ /**
+ * Show the page content
+ *
+ * Either shows the registration form or, if registration was successful,
+ * instructions for using the site.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ if ($this->arg('save')) {
+ $this->saveSettings();
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function saveSettings() {
+
+ $noticesync = $this->arg('noticesync');
+ $replysync = $this->arg('replysync');
+ $prefix = $this->trimmed('prefix');
+
+ $original = clone($this->flink);
+ $this->flink->set_flags($noticesync, $replysync, false, false);
+ $result = $this->flink->update($original);
+
+ if ($prefix == '' || $prefix == '0') {
+ // Facebook bug: saving empty strings to prefs now fails
+ // http://bugs.developers.facebook.com/show_bug.cgi?id=7110
+ $trimmed = $prefix . ' ';
+ } else {
+ $trimmed = substr($prefix, 0, 128);
+ }
+ $this->facebook->api_client->data_setUserPreference(FACEBOOK_NOTICE_PREFIX,
+ $trimmed);
+
+ if ($result === false) {
+ $this->showForm(_('There was a problem saving your sync preferences!'));
+ } else {
+ $this->showForm(_('Sync preferences saved.'), true);
+ }
+ }
+
+ function showForm($msg = null, $success = false) {
+
+ if ($msg) {
+ if ($success) {
+ $this->element('fb:success', array('message' => $msg));
+ } else {
+ $this->element('fb:error', array('message' => $msg));
+ }
+ }
+
+ if ($this->facebook->api_client->users_hasAppPermission('publish_stream')) {
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'facebook_settings'));
+
+ $this->elementStart('ul', 'form_data');
+
+ $this->elementStart('li');
+
+ $this->checkbox('noticesync', _('Automatically update my Facebook status with my notices.'),
+ ($this->flink) ? ($this->flink->noticesync & FOREIGN_NOTICE_SEND) : true);
+
+ $this->elementEnd('li');
+
+ $this->elementStart('li');
+
+ $this->checkbox('replysync', _('Send "@" replies to Facebook.'),
+ ($this->flink) ? ($this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : true);
+
+ $this->elementEnd('li');
+
+ $this->elementStart('li');
+
+ $prefix = trim($this->facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX));
+
+ $this->input('prefix', _('Prefix'),
+ ($prefix) ? $prefix : null,
+ _('A string to prefix notices with.'));
+
+ $this->elementEnd('li');
+
+ $this->elementStart('li');
+
+ $this->submit('save', _('Save'));
+
+ $this->elementEnd('li');
+
+ $this->elementEnd('ul');
+
+ $this->elementEnd('form');
+
+ } else {
+
+ $instructions = sprintf(_('If you would like %s to automatically update ' .
+ 'your Facebook status with your latest notice, you need ' .
+ 'to give it permission.'), $this->app_name);
+
+ $this->elementStart('p');
+ $this->element('span', array('id' => 'permissions_notice'), $instructions);
+ $this->elementEnd('p');
+
+ $this->elementStart('ul', array('id' => 'fb-permissions-list'));
+ $this->elementStart('li', array('id' => 'fb-permissions-item'));
+ $this->elementStart('fb:prompt-permission', array('perms' => 'publish_stream',
+ 'next_fbjs' => 'document.setLocation(\'' . "$this->app_uri/settings.php" . '\')'));
+ $this->element('span', array('class' => 'facebook-button'),
+ sprintf(_('Allow %s to update my Facebook status'), common_config('site', 'name')));
+ $this->elementEnd('fb:prompt-permission');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ }
+
+ }
+
+ function title()
+ {
+ return _('Sync preferences');
+ }
+
+}
diff --git a/plugins/Facebook/facebookutil.php b/plugins/Facebook/facebookutil.php
new file mode 100644
index 000000000..6f50c173a
--- /dev/null
+++ b/plugins/Facebook/facebookutil.php
@@ -0,0 +1,295 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once INSTALLDIR . '/plugins/Facebook/facebook/facebook.php';
+require_once INSTALLDIR . '/plugins/Facebook/facebookaction.php';
+require_once INSTALLDIR . '/lib/noticelist.php';
+
+define("FACEBOOK_SERVICE", 2); // Facebook is foreign_service ID 2
+define("FACEBOOK_NOTICE_PREFIX", 1);
+define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
+
+function getFacebook()
+{
+ static $facebook = null;
+
+ $apikey = common_config('facebook', 'apikey');
+ $secret = common_config('facebook', 'secret');
+
+ if ($facebook === null) {
+ $facebook = new Facebook($apikey, $secret);
+ }
+
+ if (empty($facebook)) {
+ common_log(LOG_ERR, 'Could not make new Facebook client obj!',
+ __FILE__);
+ }
+
+ return $facebook;
+}
+
+function isFacebookBound($notice, $flink) {
+
+ if (empty($flink)) {
+ return false;
+ }
+
+ // Avoid a loop
+
+ if ($notice->source == 'Facebook') {
+ common_log(LOG_INFO, "Skipping notice $notice->id because its " .
+ 'source is Facebook.');
+ return false;
+ }
+
+ // If the user does not want to broadcast to Facebook, move along
+
+ if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
+ common_log(LOG_INFO, "Skipping notice $notice->id " .
+ 'because user has FOREIGN_NOTICE_SEND bit off.');
+ return false;
+ }
+
+ // If it's not a reply, or if the user WANTS to send @-replies,
+ // then, yeah, it can go to Facebook.
+
+ if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+ return true;
+ }
+
+ return false;
+
+}
+
+function facebookBroadcastNotice($notice)
+{
+ $facebook = getFacebook();
+ $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
+
+ if (isFacebookBound($notice, $flink)) {
+
+ // Okay, we're good to go, update the FB status
+
+ $status = null;
+ $fbuid = $flink->foreign_id;
+ $user = $flink->getUser();
+ $attachments = $notice->attachments();
+
+ try {
+
+ // Get the status 'verb' (prefix) the user has set
+
+ // XXX: Does this call count against our per user FB request limit?
+ // If so we should consider storing verb elsewhere or not storing
+
+ $prefix = trim($facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX,
+ $fbuid));
+
+ $status = "$prefix $notice->content";
+
+ $can_publish = $facebook->api_client->users_hasAppPermission('publish_stream',
+ $fbuid);
+
+ $can_update = $facebook->api_client->users_hasAppPermission('status_update',
+ $fbuid);
+ if (!empty($attachments) && $can_publish == 1) {
+ $fbattachment = format_attachments($attachments);
+ $facebook->api_client->stream_publish($status, $fbattachment,
+ null, null, $fbuid);
+ common_log(LOG_INFO,
+ "Posted notice $notice->id w/attachment " .
+ "to Facebook user's stream (fbuid = $fbuid).");
+ } elseif ($can_update == 1 || $can_publish == 1) {
+ $facebook->api_client->users_setStatus($status, $fbuid, false, true);
+ common_log(LOG_INFO,
+ "Posted notice $notice->id to Facebook " .
+ "as a status update (fbuid = $fbuid).");
+ } else {
+ $msg = "Not sending notice $notice->id to Facebook " .
+ "because user $user->nickname hasn't given the " .
+ 'Facebook app \'status_update\' or \'publish_stream\' permission.';
+ common_log(LOG_WARNING, $msg);
+ }
+
+ // Finally, attempt to update the user's profile box
+
+ if ($can_publish == 1 || $can_update == 1) {
+ updateProfileBox($facebook, $flink, $notice);
+ }
+
+ } catch (FacebookRestClientException $e) {
+
+ $code = $e->getCode();
+
+ common_log(LOG_WARNING, 'Facebook returned error code ' .
+ $code . ': ' . $e->getMessage());
+ common_log(LOG_WARNING,
+ 'Unable to update Facebook status for ' .
+ "$user->nickname (user id: $user->id)!");
+
+ if ($code == 200 || $code == 250) {
+
+ // 200 The application does not have permission to operate on the passed in uid parameter.
+ // 250 Updating status requires the extended permission status_update or publish_stream.
+ // see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML
+
+ remove_facebook_app($flink);
+
+ } else {
+
+ // Try sending again later.
+
+ return false;
+ }
+
+ }
+ }
+
+ return true;
+
+}
+
+function updateProfileBox($facebook, $flink, $notice) {
+ $fbaction = new FacebookAction($output = 'php://output',
+ $indent = true, $facebook, $flink);
+ $fbaction->updateProfileBox($notice);
+}
+
+function format_attachments($attachments)
+{
+ $fbattachment = array();
+ $fbattachment['media'] = array();
+
+ foreach($attachments as $attachment)
+ {
+ if($enclosure = $attachment->getEnclosure()){
+ $fbmedia = get_fbmedia_for_attachment($enclosure);
+ }else{
+ $fbmedia = get_fbmedia_for_attachment($attachment);
+ }
+ if($fbmedia){
+ $fbattachment['media'][]=$fbmedia;
+ }else{
+ $fbattachment['name'] = ($attachment->title ?
+ $attachment->title : $attachment->url);
+ $fbattachment['href'] = $attachment->url;
+ }
+ }
+ if(count($fbattachment['media'])>0){
+ unset($fbattachment['name']);
+ unset($fbattachment['href']);
+ }
+ return $fbattachment;
+}
+
+/**
+* given an File objects, returns an associative array suitable for Facebook media
+*/
+function get_fbmedia_for_attachment($attachment)
+{
+ $fbmedia = array();
+
+ if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
+ $fbmedia['type'] = 'image';
+ $fbmedia['src'] = $attachment->url;
+ $fbmedia['href'] = $attachment->url;
+ } else if ($attachment->mimetype == 'audio/mpeg') {
+ $fbmedia['type'] = 'mp3';
+ $fbmedia['src'] = $attachment->url;
+ }else if ($attachment->mimetype == 'application/x-shockwave-flash') {
+ $fbmedia['type'] = 'flash';
+
+ // http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
+ // says that imgsrc is required... but we have no value to put in it
+ // $fbmedia['imgsrc']='';
+
+ $fbmedia['swfsrc'] = $attachment->url;
+ }else{
+ return false;
+ }
+ return $fbmedia;
+}
+
+function remove_facebook_app($flink)
+{
+
+ $user = $flink->getUser();
+
+ common_log(LOG_INFO, 'Removing Facebook App Foreign link for ' .
+ "user $user->nickname (user id: $user->id).");
+
+ $result = $flink->delete();
+
+ if (empty($result)) {
+ common_log(LOG_ERR, 'Could not remove Facebook App ' .
+ "Foreign_link for $user->nickname (user id: $user->id)!");
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ }
+
+ // Notify the user that we are removing their FB app access
+
+ $result = mail_facebook_app_removed($user);
+
+ if (!$result) {
+
+ $msg = 'Unable to send email to notify ' .
+ "$user->nickname (user id: $user->id) " .
+ 'that their Facebook app link was ' .
+ 'removed!';
+
+ common_log(LOG_WARNING, $msg);
+ }
+
+}
+
+/**
+ * Send a mail message to notify a user that her Facebook Application
+ * access has been removed.
+ *
+ * @param User $user user whose Facebook app link has been removed
+ *
+ * @return boolean success flag
+ */
+
+function mail_facebook_app_removed($user)
+{
+ common_init_locale($user->language);
+
+ $profile = $user->getProfile();
+
+ $site_name = common_config('site', 'name');
+
+ $subject = sprintf(
+ _('Your %1$s Facebook application access has been disabled.',
+ $site_name));
+
+ $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " .
+ 'unable to update your Facebook status from %2$s, and have disabled ' .
+ 'the Facebook application for your account. This may be because ' .
+ 'you have removed the Facebook application\'s authorization, or ' .
+ 'have deleted your Facebook account. You can re-enable the ' .
+ 'Facebook application and automatic status updating by ' .
+ "re-installing the %2\$s Facebook application.\n\nRegards,\n\n%2\$s"),
+ $user->nickname, $site_name);
+
+ common_init_locale();
+ return mail_to_user($user, $subject, $body);
+
+}
diff --git a/plugins/FBConnect/fbfavicon.ico b/plugins/Facebook/fbfavicon.ico
index c57c0342f..c57c0342f 100644
--- a/plugins/FBConnect/fbfavicon.ico
+++ b/plugins/Facebook/fbfavicon.ico
Binary files differ
diff --git a/plugins/GeoURLPlugin.php b/plugins/GeoURLPlugin.php
new file mode 100644
index 000000000..30ff2c278
--- /dev/null
+++ b/plugins/GeoURLPlugin.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to add ICBM metadata to HTML pages and report data to GeoURL.org
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin to add ICBM metadata to HTML pages and report data to GeoURL.org
+ *
+ * Adds metadata to notice and profile pages that geourl.org and others
+ * understand. Also, pings geourl.org when a new notice is saved or
+ * a profile is changed.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso Location
+ */
+
+class GeoURLPlugin extends Plugin
+{
+ public $ping = 'http://geourl.org/ping/';
+
+ /**
+ * Add extra <meta> headers for certain pages that geourl.org understands
+ *
+ * @param Action $action page being shown
+ *
+ * @return boolean event handler flag
+ */
+
+ function onEndShowHeadElements($action)
+ {
+ $name = $action->trimmed('action');
+
+ $location = null;
+
+ if ($name == 'showstream') {
+ $profile = $action->profile;
+ if (!empty($profile) && !empty($profile->lat) && !empty($profile->lon)) {
+ $location = $profile->lat . ', ' . $profile->lon;
+ }
+ } else if ($name == 'shownotice') {
+ $notice = $action->profile;
+ if (!empty($notice) && !empty($notice->lat) && !empty($notice->lon)) {
+ $location = $notice->lat . ', ' . $notice->lon;
+ }
+ }
+
+ if (!empty($location)) {
+ $action->element('meta', array('name' => 'ICBM',
+ 'content' => $location));
+ $action->element('meta', array('name' => 'DC.title',
+ 'content' => $action->title()));
+ }
+
+ return true;
+ }
+
+ /**
+ * Report local notices to GeoURL.org when they're created
+ *
+ * @param Notice &$notice queued notice
+ *
+ * @return boolean event handler flag
+ */
+
+ function onHandleQueuedNotice(&$notice)
+ {
+ if ($notice->is_local == 1) {
+
+ $request = HTTPClient::start();
+
+ $url = common_local_url('shownotice',
+ array('notice' => $notice->id));
+
+ try {
+ $request->post($this->ping,
+ null,
+ array('p' => $url));
+ } catch (HTTP_Request2_Exception $e) {
+ common_log(LOG_WARNING,
+ "GeoURL.org ping failed for '$url' ($this->ping)");
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/GeonamesPlugin.php b/plugins/GeonamesPlugin.php
new file mode 100644
index 000000000..59232c1c5
--- /dev/null
+++ b/plugins/GeonamesPlugin.php
@@ -0,0 +1,329 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to convert string locations to Geonames IDs and vice versa
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin to convert string locations to Geonames IDs and vice versa
+ *
+ * This handles most of the events that Location class emits. It uses
+ * the geonames.org Web service to convert names like 'Montreal, Quebec, Canada'
+ * into IDs and lat/lon pairs.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso Location
+ */
+
+class GeonamesPlugin extends Plugin
+{
+ const LOCATION_NS = 1;
+
+ /**
+ * convert a name into a Location object
+ *
+ * @param string $name Name to convert
+ * @param string $language ISO code for anguage the name is in
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromName($name, $language, &$location)
+ {
+ $client = HTTPClient::start();
+
+ // XXX: break down a name by commas, narrow by each
+
+ $str = http_build_query(array('maxRows' => 1,
+ 'q' => $name,
+ 'lang' => $language,
+ 'type' => 'json'));
+
+ $result = $client->get('http://ws.geonames.org/search?'.$str);
+
+ if ($result->isOk()) {
+ $rj = json_decode($result->getBody());
+ if (count($rj->geonames) > 0) {
+ $n = $rj->geonames[0];
+
+ $location = new Location();
+
+ $location->lat = $n->lat;
+ $location->lon = $n->lng;
+ $location->names[$language] = $n->name;
+ $location->location_id = $n->geonameId;
+ $location->location_ns = self::LOCATION_NS;
+
+ // handled, don't continue processing!
+ return false;
+ }
+ }
+
+ // Continue processing; we don't have the answer
+ return true;
+ }
+
+ /**
+ * convert an id into a Location object
+ *
+ * @param string $id Name to convert
+ * @param string $ns Name to convert
+ * @param string $language ISO code for language for results
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromId($id, $ns, $language, &$location)
+ {
+ if ($ns != self::LOCATION_NS) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('geonameId' => $id,
+ 'lang' => $language));
+
+ $result = $client->get('http://ws.geonames.org/hierarchyJSON?'.$str);
+
+ if ($result->isOk()) {
+
+ $rj = json_decode($result->getBody());
+
+ if (count($rj->geonames) > 0) {
+
+ $parts = array();
+
+ foreach ($rj->geonames as $level) {
+ if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $level->name;
+ }
+ }
+
+ $last = $rj->geonames[count($rj->geonames)-1];
+
+ if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $last->name;
+ }
+
+ $location = new Location();
+
+ $location->location_id = $last->geonameId;
+ $location->location_ns = self::LOCATION_NS;
+ $location->lat = $last->lat;
+ $location->lon = $last->lng;
+ $location->names[$language] = implode(', ', array_reverse($parts));
+ }
+ }
+
+ // We're responsible for this NAMESPACE; nobody else
+ // can resolve it
+
+ return false;
+ }
+
+ /**
+ * convert a lat/lon pair into a Location object
+ *
+ * Given a lat/lon, we try to find a Location that's around
+ * it or nearby. We prefer populated places (cities, towns, villages).
+ *
+ * @param string $lat Latitude
+ * @param string $lon Longitude
+ * @param string $language ISO code for language for results
+ * @param Location &$location Location object (may be null)
+ *
+ * @return boolean whether to continue (results in $location)
+ */
+
+ function onLocationFromLatLon($lat, $lon, $language, &$location)
+ {
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('lat' => $lat,
+ 'lng' => $lon,
+ 'lang' => $language));
+
+ $result =
+ $client->get('http://ws.geonames.org/findNearbyPlaceNameJSON?'.$str);
+
+ if ($result->isOk()) {
+
+ $rj = json_decode($result->getBody());
+
+ if (count($rj->geonames) > 0) {
+
+ $n = $rj->geonames[0];
+
+ $parts = array();
+
+ $location = new Location();
+
+ $parts[] = $n->name;
+
+ if (!empty($n->adminName1)) {
+ $parts[] = $n->adminName1;
+ }
+
+ if (!empty($n->countryName)) {
+ $parts[] = $n->countryName;
+ }
+
+ $location->location_id = $n->geonameId;
+ $location->location_ns = self::LOCATION_NS;
+ $location->lat = $lat;
+ $location->lon = $lon;
+
+ $location->names[$language] = implode(', ', $parts);
+
+ // Success! We handled it, so no further processing
+
+ return false;
+ }
+ }
+
+ // For some reason we don't know, so pass.
+
+ return true;
+ }
+
+ /**
+ * Human-readable name for a location
+ *
+ * Given a location, we try to retrieve a human-readable name
+ * in the target language.
+ *
+ * @param Location $location Location to get the name for
+ * @param string $language ISO code for language to find name in
+ * @param string &$name Place to put the name
+ *
+ * @return boolean whether to continue
+ */
+
+ function onLocationNameLanguage($location, $language, &$name)
+ {
+ if ($location->location_ns != self::LOCATION_NS) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $client = HTTPClient::start();
+
+ $str = http_build_query(array('geonameId' => $id,
+ 'lang' => $language));
+
+ $result = $client->get('http://ws.geonames.org/hierarchyJSON?'.$str);
+
+ if ($result->isOk()) {
+
+ $rj = json_decode($result->getBody());
+
+ if (count($rj->geonames) > 0) {
+
+ $parts = array();
+
+ foreach ($rj->geonames as $level) {
+ if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $level->name;
+ }
+ }
+
+ $last = $rj->geonames[count($rj->geonames)-1];
+
+ if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
+ $parts[] = $last->name;
+ }
+
+ if (count($parts)) {
+ $name = implode(', ', array_reverse($parts));
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Human-readable name for a location
+ *
+ * Given a location, we try to retrieve a geonames.org URL.
+ *
+ * @param Location $location Location to get the url for
+ * @param string &$url Place to put the url
+ *
+ * @return boolean whether to continue
+ */
+
+ function onLocationUrl($location, &$url)
+ {
+ if ($location->location_ns != self::LOCATION_NS) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $url = 'http://www.geonames.org/' . $location->location_id;
+
+ // it's been filled, so don't process further.
+ return false;
+ }
+
+ /**
+ * Machine-readable name for a location
+ *
+ * Given a location, we try to retrieve a geonames.org URL.
+ *
+ * @param Location $location Location to get the url for
+ * @param string &$url Place to put the url
+ *
+ * @return boolean whether to continue
+ */
+
+ function onLocationRdfUrl($location, &$url)
+ {
+ if ($location->location_ns != self::LOCATION_NS) {
+ // It's not one of our IDs... keep processing
+ return true;
+ }
+
+ $url = 'http://sw.geonames.org/' . $location->location_id . '/';
+
+ // it's been filled, so don't process further.
+ return false;
+ }
+}
diff --git a/plugins/InfiniteScroll/infinitescroll.js b/plugins/InfiniteScroll/infinitescroll.js
index 9e5593d25..0c8edce2b 100644
--- a/plugins/InfiniteScroll/infinitescroll.js
+++ b/plugins/InfiniteScroll/infinitescroll.js
@@ -12,12 +12,10 @@ jQuery(document).ready(function($){
loadingImg : $('address .url')[0].href+'plugins/InfiniteScroll/ajax-loader.gif',
text : "<em>Loading the next set of posts...</em>",
donetext : "<em>Congratulations, you\'ve reached the end of the Internet.</em>",
- navSelector : "div.pagination",
+ navSelector : ".pagination",
contentSelector : "#notices_primary ol.notices",
itemSelector : "#notices_primary ol.notices li"
},function(){
- NoticeAttachments();
- NoticeReply();
- NoticeFavors();
+ SN.Init.Notices();
});
});
diff --git a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php
new file mode 100644
index 000000000..8caacff46
--- /dev/null
+++ b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php
@@ -0,0 +1,331 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to enable LDAP Authentication
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/Authentication/AuthenticationPlugin.php';
+require_once 'Net/LDAP2.php';
+
+class LdapAuthenticationPlugin extends AuthenticationPlugin
+{
+ public $host=null;
+ public $port=null;
+ public $version=null;
+ public $starttls=null;
+ public $binddn=null;
+ public $bindpw=null;
+ public $basedn=null;
+ public $options=null;
+ public $filter=null;
+ public $scope=null;
+ public $password_encoding=null;
+ public $attributes=array();
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->host)){
+ throw new Exception("must specify a host");
+ }
+ if(!isset($this->basedn)){
+ throw new Exception("must specify a basedn");
+ }
+ if(!isset($this->attributes['nickname'])){
+ throw new Exception("must specify a nickname attribute");
+ }
+ if(!isset($this->attributes['username'])){
+ throw new Exception("must specify a username attribute");
+ }
+ if($this->password_changeable && (! isset($this->attributes['password']) || !isset($this->password_encoding))){
+ throw new Exception("if password_changeable is set, the password attribute and password_encoding must also be specified");
+ }
+ }
+
+ //---interface implementation---//
+
+ function checkPassword($username, $password)
+ {
+ $entry = $this->ldap_get_user($username);
+ if(!$entry){
+ return false;
+ }else{
+ $config = $this->ldap_get_config();
+ $config['binddn']=$entry->dn();
+ $config['bindpw']=$password;
+ if($this->ldap_get_connection($config)){
+ return true;
+ }else{
+ return false;
+ }
+ }
+ }
+
+ function autoRegister($username)
+ {
+ $entry = $this->ldap_get_user($username,$this->attributes);
+ if($entry){
+ $registration_data = array();
+ foreach($this->attributes as $sn_attribute=>$ldap_attribute){
+ $registration_data[$sn_attribute]=$entry->getValue($ldap_attribute,'single');
+ }
+ if(isset($registration_data['email']) && !empty($registration_data['email'])){
+ $registration_data['email_confirmed']=true;
+ }
+ //set the database saved password to a random string.
+ $registration_data['password']=common_good_rand(16);
+ return User::register($registration_data);
+ }else{
+ //user isn't in ldap, so we cannot register him
+ return false;
+ }
+ }
+
+ function changePassword($username,$oldpassword,$newpassword)
+ {
+ if(! isset($this->attributes['password']) || !isset($this->password_encoding)){
+ //throw new Exception(_('Sorry, changing LDAP passwords is not supported at this time'));
+ return false;
+ }
+ $entry = $this->ldap_get_user($username);
+ if(!$entry){
+ return false;
+ }else{
+ $config = $this->ldap_get_config();
+ $config['binddn']=$entry->dn();
+ $config['bindpw']=$oldpassword;
+ if($ldap = $this->ldap_get_connection($config)){
+ $entry = $this->ldap_get_user($username,array(),$ldap);
+
+ $newCryptedPassword = $this->hashPassword($newpassword, $this->password_encoding);
+ if ($newCryptedPassword===false) {
+ return false;
+ }
+ if($this->password_encoding=='ad') {
+ //TODO I believe this code will work once this bug is fixed: http://pear.php.net/bugs/bug.php?id=16796
+ $oldCryptedPassword = $this->hashPassword($oldpassword, $this->password_encoding);
+ $entry->delete( array($this->attributes['password'] => $oldCryptedPassword ));
+ }
+ $entry->replace( array($this->attributes['password'] => $newCryptedPassword ), true);
+ if( Net_LDAP2::isError($entry->upate()) ) {
+ return false;
+ }
+ return true;
+ }else{
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ //---utility functions---//
+ function ldap_get_config(){
+ $config = array();
+ $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope');
+ foreach($keys as $key){
+ $value = $this->$key;
+ if($value!==null){
+ $config[$key]=$value;
+ }
+ }
+ return $config;
+ }
+
+ function ldap_get_connection($config = null){
+ if($config == null && isset($this->default_ldap)){
+ return $this->default_ldap;
+ }
+
+ //cannot use Net_LDAP2::connect() as StatusNet uses
+ //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
+ //PEAR handling can be overridden on instance objects, so we do that.
+ $ldap = new Net_LDAP2(isset($config)?$config:$this->ldap_get_config());
+ $ldap->setErrorHandling(PEAR_ERROR_RETURN);
+ $err=$ldap->bind();
+ if (Net_LDAP2::isError($err)) {
+ common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage());
+ return false;
+ }
+ if($config == null) $this->default_ldap=$ldap;
+ return $ldap;
+ }
+
+ /**
+ * get an LDAP entry for a user with a given username
+ *
+ * @param string $username
+ * $param array $attributes LDAP attributes to retrieve
+ * @return string DN
+ */
+ function ldap_get_user($username,$attributes=array(),$ldap=null){
+ if($ldap==null) {
+ $ldap = $this->ldap_get_connection();
+ }
+ $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username);
+ $options = array(
+ 'attributes' => $attributes
+ );
+ $search = $ldap->search(null,$filter,$options);
+
+ if (PEAR::isError($search)) {
+ common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage());
+ return false;
+ }
+
+ if($search->count()==0){
+ return false;
+ }else if($search->count()==1){
+ $entry = $search->shiftEntry();
+ return $entry;
+ }else{
+ common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username);
+ return false;
+ }
+ }
+
+ /**
+ * Code originaly from the phpLDAPadmin development team
+ * http://phpldapadmin.sourceforge.net/
+ *
+ * Hashes a password and returns the hash based on the specified enc_type.
+ *
+ * @param string $passwordClear The password to hash in clear text.
+ * @param string $encodageType Standard LDAP encryption type which must be one of
+ * crypt, ext_des, md5crypt, blowfish, md5, sha, smd5, ssha, or clear.
+ * @return string The hashed password.
+ *
+ */
+
+ function hashPassword( $passwordClear, $encodageType )
+ {
+ $encodageType = strtolower( $encodageType );
+ switch( $encodageType ) {
+ case 'crypt':
+ $cryptedPassword = '{CRYPT}' . crypt($passwordClear,$this->randomSalt(2));
+ break;
+
+ case 'ext_des':
+ // extended des crypt. see OpenBSD crypt man page.
+ if ( ! defined( 'CRYPT_EXT_DES' ) || CRYPT_EXT_DES == 0 ) {return FALSE;} //Your system crypt library does not support extended DES encryption.
+ $cryptedPassword = '{CRYPT}' . crypt( $passwordClear, '_' . $this->randomSalt(8) );
+ break;
+
+ case 'md5crypt':
+ if( ! defined( 'CRYPT_MD5' ) || CRYPT_MD5 == 0 ) {return FALSE;} //Your system crypt library does not support md5crypt encryption.
+ $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$1$' . $this->randomSalt(9) );
+ break;
+
+ case 'blowfish':
+ if( ! defined( 'CRYPT_BLOWFISH' ) || CRYPT_BLOWFISH == 0 ) {return FALSE;} //Your system crypt library does not support blowfish encryption.
+ $cryptedPassword = '{CRYPT}' . crypt( $passwordClear , '$2a$12$' . $this->randomSalt(13) ); // hardcoded to second blowfish version and set number of rounds
+ break;
+
+ case 'md5':
+ $cryptedPassword = '{MD5}' . base64_encode( pack( 'H*' , md5( $passwordClear) ) );
+ break;
+
+ case 'sha':
+ if( function_exists('sha1') ) {
+ // use php 4.3.0+ sha1 function, if it is available.
+ $cryptedPassword = '{SHA}' . base64_encode( pack( 'H*' , sha1( $passwordClear) ) );
+ } elseif( function_exists( 'mhash' ) ) {
+ $cryptedPassword = '{SHA}' . base64_encode( mhash( MHASH_SHA1, $passwordClear) );
+ } else {
+ return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes.
+ }
+ break;
+
+ case 'ssha':
+ if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) {
+ mt_srand( (double) microtime() * 1000000 );
+ $salt = mhash_keygen_s2k( MHASH_SHA1, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 );
+ $cryptedPassword = "{SSHA}".base64_encode( mhash( MHASH_SHA1, $passwordClear.$salt ).$salt );
+ } else {
+ return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes.
+ }
+ break;
+
+ case 'smd5':
+ if( function_exists( 'mhash' ) && function_exists( 'mhash_keygen_s2k' ) ) {
+ mt_srand( (double) microtime() * 1000000 );
+ $salt = mhash_keygen_s2k( MHASH_MD5, $passwordClear, substr( pack( "h*", md5( mt_rand() ) ), 0, 8 ), 4 );
+ $cryptedPassword = "{SMD5}".base64_encode( mhash( MHASH_MD5, $passwordClear.$salt ).$salt );
+ } else {
+ return FALSE; //Your PHP install does not have the mhash() function. Cannot do SHA hashes.
+ }
+ break;
+
+ case 'ad':
+ $cryptedPassword = '';
+ $passwordClear = "\"" . $passwordClear . "\"";
+ $len = strlen($passwordClear);
+ for ($i = 0; $i < $len; $i++) {
+ $cryptedPassword .= "{$passwordClear{$i}}\000";
+ }
+
+ case 'clear':
+ default:
+ $cryptedPassword = $passwordClear;
+ }
+
+ return $cryptedPassword;
+ }
+
+ /**
+ * Code originaly from the phpLDAPadmin development team
+ * http://phpldapadmin.sourceforge.net/
+ *
+ * Used to generate a random salt for crypt-style passwords. Salt strings are used
+ * to make pre-built hash cracking dictionaries difficult to use as the hash algorithm uses
+ * not only the user's password but also a randomly generated string. The string is
+ * stored as the first N characters of the hash for reference of hashing algorithms later.
+ *
+ * --- added 20021125 by bayu irawan <bayuir@divnet.telkom.co.id> ---
+ * --- ammended 20030625 by S C Rigler <srigler@houston.rr.com> ---
+ *
+ * @param int $length The length of the salt string to generate.
+ * @return string The generated salt string.
+ */
+
+ function randomSalt( $length )
+ {
+ $possible = '0123456789'.
+ 'abcdefghijklmnopqrstuvwxyz'.
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
+ './';
+ $str = "";
+ mt_srand((double)microtime() * 1000000);
+
+ while( strlen( $str ) < $length )
+ $str .= substr( $possible, ( rand() % strlen( $possible ) ), 1 );
+
+ return $str;
+ }
+}
diff --git a/plugins/LdapAuthentication/README b/plugins/LdapAuthentication/README
new file mode 100644
index 000000000..2226159c2
--- /dev/null
+++ b/plugins/LdapAuthentication/README
@@ -0,0 +1,82 @@
+The LDAP Authentication plugin allows for StatusNet to handle authentication
+through LDAP.
+
+Installation
+============
+add "addPlugin('ldapAuthentication',
+ array('setting'=>'value', 'setting2'=>'value2', ...);"
+to the bottom of your config.php
+
+Settings
+========
+provider_name*: a unique name for this authentication provider.
+authoritative (false): Set to true if LDAP's responses are authoritative
+ (if authorative and LDAP fails, no other password checking will be done).
+autoregistration (false): Set to true if users should be automatically created
+ when they attempt to login.
+email_changeable (true): Are users allowed to change their email address?
+ (true or false)
+password_changeable (true): Are users allowed to change their passwords?
+ (true or false)
+password_encoding: required if users are to be able to change their passwords
+ Possible values are: crypt, ext_des, md5crypt, blowfish, md5, sha, ssha,
+ smd5, ad, clear
+
+host*: LDAP server name to connect to. You can provide several hosts in an
+ array in which case the hosts are tried from left to right.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+port: Port on the server.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+version: LDAP version.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+starttls: TLS is started after connecting.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+binddn: The distinguished name to bind as (username).
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+bindpw: Password for the binddn.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+basedn*: LDAP base name (root directory).
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+options: See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+filter: Default search filter.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+scope: Default search scope.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+
+attributes: an array that relates StatusNet user attributes to LDAP ones
+ username*: LDAP attribute value entered when authenticating to StatusNet
+ nickname*: LDAP attribute value shown as the user's nickname
+ email
+ fullname
+ homepage
+ location
+ password: required if users are to be able to change their passwords
+
+* required
+default values are in (parenthesis)
+
+For most LDAP installations, the "nickname" and "username" attributes should
+ be the same.
+
+Example
+=======
+Here's an example of an LDAP plugin configuration that connects to
+ Microsoft Active Directory.
+
+addPlugin('ldapAuthentication', array(
+ 'provider_name'=>'Example',
+ 'authoritative'=>true,
+ 'autoregistration'=>true,
+ 'binddn'=>'username',
+ 'bindpw'=>'password',
+ 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
+ 'host'=>array('server1', 'server2'),
+ 'password_encoding'=>'ad',
+ 'attributes'=>array(
+ 'username'=>'sAMAccountName',
+ 'nickname'=>'sAMAccountName',
+ 'email'=>'mail',
+ 'fullname'=>'displayName',
+ 'password'=>'unicodePwd')
+));
+
diff --git a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php
new file mode 100644
index 000000000..5e759c379
--- /dev/null
+++ b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to enable LDAP Authorization
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/Authorization/AuthorizationPlugin.php';
+require_once 'Net/LDAP2.php';
+
+class LdapAuthorizationPlugin extends AuthorizationPlugin
+{
+ public $host=null;
+ public $port=null;
+ public $version=null;
+ public $starttls=null;
+ public $binddn=null;
+ public $bindpw=null;
+ public $basedn=null;
+ public $options=null;
+ public $filter=null;
+ public $scope=null;
+ public $provider_name = null;
+ public $uniqueMember_attribute = null;
+ public $roles_to_groups = array();
+ public $login_group = null;
+ public $attributes = array();
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->host)){
+ throw new Exception("must specify a host");
+ }
+ if(!isset($this->basedn)){
+ throw new Exception("must specify a basedn");
+ }
+ if(!isset($this->provider_name)){
+ throw new Exception("provider_name must be set. Use the provider_name from the LDAP Authentication plugin.");
+ }
+ if(!isset($this->uniqueMember_attribute)){
+ throw new Exception("uniqueMember_attribute must be set.");
+ }
+ if(!isset($this->attributes['username'])){
+ throw new Exception("username attribute must be set.");
+ }
+ }
+
+ //---interface implementation---//
+ function loginAllowed($user) {
+ $user_username = new User_username();
+ $user_username->user_id=$user->id;
+ $user_username->provider_name=$this->provider_name;
+ if($user_username->find() && $user_username->fetch()){
+ $entry = $this->ldap_get_user($user_username->username);
+ if($entry){
+ if(isset($this->login_group)){
+ if(is_array($this->login_group)){
+ foreach($this->login_group as $group){
+ if($this->ldap_is_dn_member_of_group($entry->dn(),$group)){
+ return true;
+ }
+ }
+ }else{
+ if($this->ldap_is_dn_member_of_group($entry->dn(),$this->login_group)){
+ return true;
+ }
+ }
+ return null;
+ }else{
+ //if a user exists, we can assume he's allowed to login
+ return true;
+ }
+ }else{
+ return null;
+ }
+ }else{
+ return null;
+ }
+ }
+
+ function hasRole($profile, $name) {
+ $user_username = new User_username();
+ $user_username->user_id=$profile->id;
+ $user_username->provider_name=$this->provider_name;
+ if($user_username->find() && $user_username->fetch()){
+ $entry = $this->ldap_get_user($user_username->username);
+ if($entry){
+ if(isset($this->roles_to_groups[$name])){
+ if(is_array($this->roles_to_groups[$name])){
+ foreach($this->roles_to_groups[$name] as $group){
+ if($this->ldap_is_dn_member_of_group($entry->dn(),$group)){
+ return true;
+ }
+ }
+ }else{
+ if($this->ldap_is_dn_member_of_group($entry->dn(),$this->roles_to_groups[$name])){
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ function ldap_is_dn_member_of_group($userDn, $groupDn)
+ {
+ $ldap = $this->ldap_get_connection();
+ $link = $ldap->getLink();
+ $r = ldap_compare($link, $groupDn, $this->uniqueMember_attribute, $userDn);
+ if ($r === true){
+ return true;
+ }else if($r === false){
+ return false;
+ }else{
+ common_log(LOG_ERR, ldap_error($r));
+ return false;
+ }
+ }
+
+ function ldap_get_config(){
+ $config = array();
+ $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope');
+ foreach($keys as $key){
+ $value = $this->$key;
+ if($value!==null){
+ $config[$key]=$value;
+ }
+ }
+ return $config;
+ }
+
+ //-----the below function were copied from LDAPAuthenticationPlugin. They will be moved to a utility class soon.----\\
+ function ldap_get_connection($config = null){
+ if($config == null && isset($this->default_ldap)){
+ return $this->default_ldap;
+ }
+
+ //cannot use Net_LDAP2::connect() as StatusNet uses
+ //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
+ //PEAR handling can be overridden on instance objects, so we do that.
+ $ldap = new Net_LDAP2(isset($config)?$config:$this->ldap_get_config());
+ $ldap->setErrorHandling(PEAR_ERROR_RETURN);
+ $err=$ldap->bind();
+ if (Net_LDAP2::isError($err)) {
+ common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage());
+ return false;
+ }
+ if($config == null) $this->default_ldap=$ldap;
+ return $ldap;
+ }
+
+ /**
+ * get an LDAP entry for a user with a given username
+ *
+ * @param string $username
+ * $param array $attributes LDAP attributes to retrieve
+ * @return string DN
+ */
+ function ldap_get_user($username,$attributes=array(),$ldap=null){
+ if($ldap==null) {
+ $ldap = $this->ldap_get_connection();
+ }
+ $filter = Net_LDAP2_Filter::create($this->attributes['username'], 'equals', $username);
+ $options = array(
+ 'attributes' => $attributes
+ );
+ $search = $ldap->search(null,$filter,$options);
+
+ if (PEAR::isError($search)) {
+ common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage());
+ return false;
+ }
+
+ if($search->count()==0){
+ return false;
+ }else if($search->count()==1){
+ $entry = $search->shiftEntry();
+ return $entry;
+ }else{
+ common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username);
+ return false;
+ }
+ }
+}
diff --git a/plugins/LdapAuthorization/README b/plugins/LdapAuthorization/README
new file mode 100644
index 000000000..44239d8e0
--- /dev/null
+++ b/plugins/LdapAuthorization/README
@@ -0,0 +1,91 @@
+The LDAP Authorization plugin allows for StatusNet to handle authorization
+through LDAP.
+
+Installation
+============
+add "addPlugin('ldapAuthorization',
+ array('setting'=>'value', 'setting2'=>'value2', ...);"
+to the bottom of your config.php
+
+You *cannot* use this plugin without the LDAP Authentication plugin
+
+Settings
+========
+provider_name*: name of the LDAP authentication provider that this plugin works with.
+authoritative (false): should this plugin be authoritative for
+ authorization?
+uniqueMember_attribute ('uniqueMember')*: the attribute of a group
+ that lists the DNs of its members
+roles_to_groups: array that maps StatusNet roles to LDAP groups
+ some StatusNet roles are: moderator, administrator, sandboxed, silenced
+login_group: if this is set to a group DN, only members of that group will be
+ allowed to login
+
+The below settings must be exact copies of the settings used for the
+ corresponding LDAP Authentication plugin.
+
+host*: LDAP server name to connect to. You can provide several hosts in an
+ array in which case the hosts are tried from left to right.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+port: Port on the server.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+version: LDAP version.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+starttls: TLS is started after connecting.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+binddn: The distinguished name to bind as (username).
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+bindpw: Password for the binddn.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+basedn*: LDAP base name (root directory).
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+options: See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+filter: Default search filter.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+scope: Default search scope.
+ See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
+
+attributes: an array that relates StatusNet user attributes to LDAP ones
+ username*: LDAP attribute value entered when authenticating to StatusNet
+
+* required
+default values are in (parenthesis)
+
+Example
+=======
+Here's an example of an LDAP plugin configuration that connects to
+ Microsoft Active Directory.
+
+addPlugin('ldapAuthentication', array(
+ 'provider_name'=>'Example',
+ 'authoritative'=>true,
+ 'autoregistration'=>true,
+ 'binddn'=>'username',
+ 'bindpw'=>'password',
+ 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
+ 'host'=>array('server1', 'server2'),
+ 'password_encoding'=>'ad',
+ 'attributes'=>array(
+ 'username'=>'sAMAccountName',
+ 'nickname'=>'sAMAccountName',
+ 'email'=>'mail',
+ 'fullname'=>'displayName',
+ 'password'=>'unicodePwd')
+));
+addPlugin('ldapAuthorization', array(
+ 'provider_name'=>'Example',
+ 'authoritative'=>false,
+ 'uniqueMember_attribute'=>'member',
+ 'roles_to_groups'=> array(
+ 'moderator'=>'CN=SN-Moderators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
+ 'administrator'=> array('CN=System-Adminstrators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
+ 'CN=SN-Administrators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc')
+ ),
+ 'binddn'=>'username',
+ 'bindpw'=>'password',
+ 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
+ 'host'=>array('server1', 'server2'),
+ 'attributes'=>array(
+ 'username'=>'sAMAccountName')
+));
+
diff --git a/plugins/LilUrl/LilUrlPlugin.php b/plugins/LilUrl/LilUrlPlugin.php
new file mode 100644
index 000000000..e906751e8
--- /dev/null
+++ b/plugins/LilUrl/LilUrlPlugin.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
+
+class LilUrlPlugin extends UrlShortenerPlugin
+{
+ public $serviceUrl;
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->serviceUrl)){
+ throw new Exception("must specify a serviceUrl");
+ }
+ }
+
+ protected function shorten($url) {
+ $data = array('longurl' => $url);
+
+ $responseBody = $this->http_post($this->serviceUrl,$data);
+
+ if (!$responseBody) return;
+ $y = @simplexml_load_string($responseBody);
+ if (!isset($y->body)) return;
+ $x = $y->body->p[0]->a->attributes();
+ if (isset($x['href'])) {
+ return $x['href'];
+ }
+ }
+}
+
diff --git a/plugins/LinkbackPlugin.php b/plugins/LinkbackPlugin.php
index 60f7a60c7..f220fff8f 100644
--- a/plugins/LinkbackPlugin.php
+++ b/plugins/LinkbackPlugin.php
@@ -59,7 +59,7 @@ class LinkbackPlugin extends Plugin
parent::__construct();
}
- function onEndNoticeSave($notice)
+ function onHandleQueuedNotice($notice)
{
if ($notice->is_local == 1) {
// Try to avoid actually mucking with the
@@ -129,18 +129,12 @@ class LinkbackPlugin extends Plugin
}
}
- $request = xmlrpc_encode_request('pingback.ping', $args);
- $context = stream_context_create(array('http' => array('method' => "POST",
- 'header' =>
- "Content-Type: text/xml\r\n".
- "User-Agent: " . $this->userAgent(),
- 'content' => $request)));
- $file = file_get_contents($endpoint, false, $context);
- if (!$file) {
- common_log(LOG_WARNING,
- "Pingback request failed for '$url' ($endpoint)");
- } else {
- $response = xmlrpc_decode($file);
+ $request = HTTPClient::start();
+ try {
+ $response = $request->post($endpoint,
+ array('Content-Type: text/xml'),
+ xmlrpc_encode_request('pingback.ping', $args));
+ $response = xmlrpc_decode($response->getBody());
if (xmlrpc_is_fault($response)) {
common_log(LOG_WARNING,
"Pingback error for '$url' ($endpoint): ".
@@ -150,6 +144,9 @@ class LinkbackPlugin extends Plugin
"Pingback success for '$url' ($endpoint): ".
"'$response'");
}
+ } catch (HTTP_Request2_Exception $e) {
+ common_log(LOG_WARNING,
+ "Pingback request failed for '$url' ($endpoint)");
}
}
diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php
new file mode 100644
index 000000000..eabd0d0f0
--- /dev/null
+++ b/plugins/Mapstraction/MapstractionPlugin.php
@@ -0,0 +1,245 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to provide map visualization of location data
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin to provide map visualization of location data
+ *
+ * This plugin uses the Mapstraction JavaScript library to
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso Location
+ */
+
+class MapstractionPlugin extends Plugin
+{
+ /** provider name, one of:
+ 'cloudmade', 'google', 'microsoft', 'openlayers', 'yahoo' */
+ public $provider = 'openlayers';
+ /** provider API key (or 'appid'), if required ('google' and 'yahoo' only) */
+ public $apikey = null;
+
+ /**
+ * Hook for new URLs
+ *
+ * The way to register new actions from a plugin.
+ *
+ * @param Router &$m reference to router
+ *
+ * @return boolean event handler return
+ */
+
+ function onRouterInitialized(&$m)
+ {
+ $m->connect(':nickname/all/map',
+ array('action' => 'allmap'),
+ array('nickname' => '['.NICKNAME_FMT.']{1,64}'));
+ $m->connect(':nickname/map',
+ array('action' => 'usermap'),
+ array('nickname' => '['.NICKNAME_FMT.']{1,64}'));
+ return true;
+ }
+
+ /**
+ * Hook for autoloading classes
+ *
+ * This makes sure our classes get autoloaded from our directory
+ *
+ * @param string $cls name of class being used
+ *
+ * @return boolean event handler return
+ */
+
+ function onAutoload($cls)
+ {
+ switch ($cls)
+ {
+ case 'AllmapAction':
+ case 'UsermapAction':
+ include_once INSTALLDIR.'/plugins/Mapstraction/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Hook for adding extra JavaScript
+ *
+ * This makes sure our scripts get loaded for map-related pages
+ *
+ * @param Action $action Action object for the page
+ *
+ * @return boolean event handler return
+ */
+
+ function onEndShowScripts($action)
+ {
+ $actionName = $action->trimmed('action');
+ // These are the ones that have maps on 'em
+ if (!in_array($actionName,
+ array('showstream', 'all', 'allmap', 'usermap'))) {
+ return true;
+ }
+
+ switch ($this->provider)
+ {
+ case 'cloudmade':
+ $action->script('http://tile.cloudmade.com/wml/0.2/web-maps-lite.js');
+ break;
+ case 'google':
+ $action->script(sprintf('http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=false&amp;key=%s',
+ $this->apikey));
+ break;
+ case 'microsoft':
+ $action->script('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6');
+ break;
+ case 'openlayers':
+ // XXX: is this not nice...?
+ $action->script('http://openlayers.org/api/OpenLayers.js');
+ break;
+ case 'yahoo':
+ $action->script(sprintf('http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=%s',
+ $this->apikey));
+ break;
+ case 'geocommons': // don't support this yet
+ default:
+ return true;
+ }
+
+ $action->script(sprintf('%s?(%s)',
+ common_path('plugins/Mapstraction/js/mxn.js'),
+ $this->provider));
+
+ $action->script(common_path('plugins/Mapstraction/usermap.js'));
+
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw(sprintf('var _provider = "%s";', $this->provider));
+ $action->elementEnd('script');
+
+ switch ($actionName) {
+ case 'usermap':
+ case 'showstream':
+ $notice = empty($action->tag)
+ ? $action->user->getNotices(($action->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1)
+ : $action->user->getTaggedNotices($action->tag, ($action->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1, 0, 0, null);
+ break;
+ case 'all':
+ case 'allmap':
+ $cur = common_current_user();
+ if (!empty($cur) && $cur->id == $action->user->id) {
+ $notice = $action->user->noticeInbox(($action->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ } else {
+ $notice = $action->user->noticesWithFriends(($action->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+ }
+ break;
+ }
+
+ $jsonArray = array();
+
+ while ($notice->fetch()) {
+ if (!empty($notice->lat) && !empty($notice->lon)) {
+ $jsonNotice = $this->noticeAsJson($notice);
+ $jsonArray[] = $jsonNotice;
+ }
+ }
+
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw('var _notices = ' . json_encode($jsonArray));
+ $action->elementEnd('script');
+
+ return true;
+ }
+
+ function onEndShowSections($action)
+ {
+ $actionName = $action->trimmed('action');
+ // These are the ones that have maps on 'em
+ if (!in_array($actionName,
+ array('showstream', 'all'))) {
+ return true;
+ }
+
+ $action->elementStart('div', array('id' => 'entity_map',
+ 'class' => 'section'));
+
+ $action->element('h2', null, _('Map'));
+
+ $action->element('div', array('id' => 'map_canvas',
+ 'class' => 'gray smallmap',
+ 'style' => "width: 100%; height: 240px"));
+
+ $mapAct = ($actionName == 'showstream') ? 'usermap' : 'allmap';
+ $mapUrl = common_local_url($mapAct,
+ array('nickname' => $action->trimmed('nickname')));
+
+ $action->element('a', array('href' => $mapUrl),
+ _("Full size"));
+
+ $action->elementEnd('div');
+ }
+
+ function noticeAsJson($notice)
+ {
+ // FIXME: this code should be abstracted to a neutral third
+ // party, like Notice::asJson(). I'm not sure of the ethics
+ // of refactoring from within a plugin, so I'm just abusing
+ // the ApiAction method. Don't do this unless you're me!
+
+ require_once(INSTALLDIR.'/lib/api.php');
+
+ $act = new ApiAction('/dev/null');
+
+ $arr = $act->twitterStatusArray($notice, true);
+ $arr['url'] = $notice->bestUrl();
+ $arr['html'] = htmlspecialchars($notice->rendered);
+ $arr['source'] = htmlspecialchars($arr['source']);
+
+ if (!empty($notice->reply_to)) {
+ $reply_to = Notice::staticGet('id', $notice->reply_to);
+ if (!empty($reply_to)) {
+ $arr['in_reply_to_status_url'] = $reply_to->bestUrl();
+ }
+ $reply_to = null;
+ }
+
+ $profile = $notice->getProfile();
+ $arr['user']['profile_url'] = $profile->profileurl;
+
+ return $arr;
+ }
+}
diff --git a/plugins/Mapstraction/allmap.php b/plugins/Mapstraction/allmap.php
new file mode 100644
index 000000000..6a48b141f
--- /dev/null
+++ b/plugins/Mapstraction/allmap.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show a map of user's friends' notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Mapstraction
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Show a map of user's notices
+ *
+ * @category Mapstraction
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class AllmapAction extends OwnerDesignAction
+{
+ var $profile = null;
+ var $page = null;
+ var $notices = null;
+
+ public $plugin = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $nickname_arg = $this->arg('nickname');
+ $nickname = common_canonical_nickname($nickname_arg);
+
+ // Permanent redirect on non-canonical nickname
+
+ if ($nickname_arg != $nickname) {
+ $args = array('nickname' => $nickname);
+ if ($this->arg('page') && $this->arg('page') != 1) {
+ $args['page'] = $this->arg['page'];
+ }
+ common_redirect(common_local_url($this->trimmed('action'), $args), 301);
+ return false;
+ }
+
+ $this->user = User::staticGet('nickname', $nickname);
+
+ if (!$this->user) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+
+ $this->profile = $this->user->getProfile();
+
+ if (!$this->profile) {
+ $this->serverError(_('User has no profile.'));
+ return false;
+ }
+
+ $page = $this->trimmed('page');
+
+ if (!empty($page) && Validate::number($page)) {
+ $this->page = $page+0;
+ } else {
+ $this->page = 1;
+ }
+
+ return true;
+ }
+
+ function title()
+ {
+ if (!empty($this->profile->fullname)) {
+ $base = $this->profile->fullname . ' (' . $this->user->nickname . ') ';
+ } else {
+ $base = $this->user->nickname;
+ }
+
+ if ($this->page == 1) {
+ return sprintf(_("%s friends map"),
+ $base);
+ } else {
+ return sprintf(_("%s friends map, page %d"),
+ $base,
+ $this->page);
+ }
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ function showContent()
+ {
+ $this->element('div', array('id' => 'map_canvas',
+ 'class' => 'gray smallmap',
+ 'style' => "width: 100%; height: 400px"));
+ }
+} \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.(provider).core.js b/plugins/Mapstraction/js/mxn.(provider).core.js
new file mode 100644
index 000000000..cc9752ffd
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.(provider).core.js
@@ -0,0 +1,289 @@
+mxn.register('{{api_id}}', {
+
+Mapstraction: {
+
+ init: function(element, api) {
+ var me = this;
+
+ // TODO: Add provider code
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ resizeTo: function(width, height){
+ // TODO: Add provider code
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+
+ // TODO: Add provider code
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ var pin = marker.toProprietary(this.api);
+
+ // TODO: Add provider code
+
+ return pin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ declutterMarkers: function(opts) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+
+ // TODO: Add provider code
+
+ return pl;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getCenter: function() {
+ var point;
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ return point;
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ if(options && options.pan) {
+ // TODO: Add provider code
+ }
+ else {
+ // TODO: Add provider code
+ }
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ var zoom;
+
+ // TODO: Add provider code
+
+ return zoom;
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // NE and SW points from the bounding box.
+ var ne = bbox.getNorthEast();
+ var sw = bbox.getSouthWest();
+ var zoom;
+
+ // TODO: Add provider code
+
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ // TODO: Add provider code
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ // TODO: Add provider code
+ break;
+ case mxn.Mapstraction.HYBRID:
+ // TODO: Add provider code
+ break;
+ default:
+ // TODO: Add provider code
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ //return mxn.Mapstraction.ROAD;
+ //return mxn.Mapstraction.SATELLITE;
+ //return mxn.Mapstraction.HYBRID;
+
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ //return new mxn.BoundingBox( , , , );
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+
+ // TODO: Add provider code
+
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ // TODO: Add provider code
+
+ //oContext.pixels.top = ...;
+ //oContext.pixels.left = ...;
+ //oContext.pixels.bottom = ...;
+ //oContext.pixels.right = ...;
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ toggleTileLayer: function(tile_url) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ mousePosition: function(element) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ }
+},
+
+LatLonPoint: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ fromProprietary: function(googlePoint) {
+ // TODO: Add provider code
+ }
+
+},
+
+Marker: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ openBubble: function() {
+ // TODO: Add provider code
+ },
+
+ hide: function() {
+ // TODO: Add provider code
+ },
+
+ show: function() {
+ // TODO: Add provider code
+ },
+
+ update: function() {
+ // TODO: Add provider code
+ }
+
+},
+
+Polyline: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ show: function() {
+ // TODO: Add provider code
+ },
+
+ hide: function() {
+ // TODO: Add provider code
+ }
+
+}
+
+}); \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.cloudmade.core.js b/plugins/Mapstraction/js/mxn.cloudmade.core.js
new file mode 100644
index 000000000..b6ee70b8f
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.cloudmade.core.js
@@ -0,0 +1,357 @@
+mxn.register('cloudmade', {
+
+ Mapstraction: {
+
+ init: function(element, api) {
+ var me = this;
+ var cloudmade = new CM.Tiles.CloudMade.Web({key: cloudmade_key});
+ this.maps[api] = new CM.Map(element, cloudmade);
+ this.loaded[api] = true;
+
+ CM.Event.addListener(this.maps[api], 'click', function(location,marker) {
+ if ( marker && marker.mapstraction_marker ) {
+ marker.mapstraction_marker.click.fire();
+ }
+ else if ( location ) {
+ me.click.fire({'location': new mxn.LatLonPoint(location.lat(), location.lng())});
+ }
+
+ // If the user puts their own Google markers directly on the map
+ // then there is no location and this event should not fire.
+ if ( location ) {
+ me.clickHandler(location.lat(),location.lng(),location,me);
+ }
+ });
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+ if(this.options.enableScrollWheelZoom){
+ map.enableScrollWheelZoom();
+ }
+ },
+
+ resizeTo: function(width, height){
+ this.maps[this.api].checkResize();
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+
+ var c = this.addControlsArgs;
+ switch (c.zoom) {
+ case 'large':
+ this.addLargeControls();
+ break;
+ case 'small':
+ this.addSmallControls();
+ break;
+ }
+
+ if (c.map_type) {
+ this.addMapTypeControls();
+ }
+ if (c.scale) {
+ map.addControl(new CM.ScaleControl());
+ this.addControlsArgs.scale = true;
+ }
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ map.addControl(new CM.SmallMapControl());
+ this.addControlsArgs.zoom = 'small';
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ map.addControl(new CM.LargeMapControl());
+ this.addControlsArgs.zoom = 'large';
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+
+ map.addControl(new CM.TileLayerControl());
+ this.addControlsArgs.map_type = true;
+ },
+
+ dragging: function(on) {
+ var map = this.maps[this.api];
+
+ if (on) {
+ map.enableDragging();
+ } else {
+ map.disableDragging();
+ }
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.setCenter(pt, zoom);
+
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ var pin = marker.toProprietary(this.api);
+ map.addOverlay(pin);
+ return pin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+ marker.proprietary_marker.closeInfoWindow();
+ map.removeOverlay(marker.proprietary_marker);
+ },
+
+ removeAllMarkers: function() {
+ // Done in mxn.core.js
+ },
+
+ declutterMarkers: function(opts) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+ map.addOverlay(pl);
+ return pl;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ map.removeOverlay(polyline.proprietary_polyline);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var pt = map.getCenter();
+
+ return new mxn.LatLonPoint(pt.lat(), pt.lng());
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ if(options !== null && options.pan) { map.panTo(pt); }
+ else { map.setCenter(pt); }
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.setZoom(zoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return map.getZoom();
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // NE and SW points from the bounding box.
+ var ne = bbox.getNorthEast();
+ var sw = bbox.getSouthWest();
+
+ var zoom = map.getBoundsZoomLevel(new CM.LatLngBounds(sw.toProprietary(this.api), ne.toProprietary(this.api)));
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+
+ // TODO: Are there any MapTypes for Cloudmade?
+
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ // TODO: Add provider code
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ // TODO: Add provider code
+ break;
+ case mxn.Mapstraction.HYBRID:
+ // TODO: Add provider code
+ break;
+ default:
+ // TODO: Add provider code
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Are there any MapTypes for Cloudmade?
+
+ return mxn.Mapstraction.ROAD;
+ //return mxn.Mapstraction.SATELLITE;
+ //return mxn.Mapstraction.HYBRID;
+
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+
+ var box = map.getBounds();
+ var sw = box.getSouthWest();
+ var ne = box.getNorthEast();
+
+ return new mxn.BoundingBox(sw.lat(), sw.lng(), ne.lat(), ne.lng());
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+
+ map.zoomToBounds(new CM.LatLngBounds(sw.toProprietary(this.api), ne.toProprietary(this.api)));
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ // TODO: Add provider code
+
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ toggleTileLayer: function(tile_url) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ mousePosition: function(element) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ }
+ },
+
+ LatLonPoint: {
+
+ toProprietary: function() {
+ var cll = new CM.LatLng(this.lat,this.lon);
+ return cll;
+ },
+
+ fromProprietary: function(point) {
+ return new mxn.LatLonPoint(point.lat(),point.lng());
+ }
+
+ },
+
+ Marker: {
+
+ toProprietary: function() {
+ var pt = this.location.toProprietary(this.api);
+ var options = {};
+
+ if (this.iconUrl) {
+ var cicon = new CM.Icon();
+ cicon.image = this.iconUrl;
+ if (this.iconSize) {
+ cicon.iconSize = new CM.Size(this.iconSize[0], this.iconSize[1]);
+ if (this.iconAnchor) {
+ cicon.iconAnchor = new CM.Point(this.iconAnchor[0], this.iconAnchor[1]);
+ }
+ }
+ if (this.iconShadowUrl) {
+ cicon.shadow = this.iconShadowUrl;
+ if (this.iconShadowSize) {
+ cicon.shadowSize = new CM.Size(this.iconShadowSize[0], this.iconShadowSize[1]);
+ }
+ }
+ options.icon = cicon;
+ }
+ if (this.labelText) {
+ options.title = this.labelText;
+ }
+ var cmarker = new CM.Marker(pt, options);
+
+ if (this.infoBubble) {
+ cmarker.bindInfoWindow(this.infoBubble);
+ }
+
+
+ return cmarker;
+ },
+
+ openBubble: function() {
+ var pin = this.proprietary_marker;
+ pin.openInfoWindow(this.infoBubble);
+ },
+
+ hide: function() {
+ var pin = this.proprietary_marker;
+ pin.hide();
+ },
+
+ show: function() {
+ var pin = this.proprietary_marker;
+ pin.show();
+ },
+
+ update: function() {
+ // TODO: Add provider code
+ }
+
+ },
+
+ Polyline: {
+
+ toProprietary: function() {
+ var pts = [];
+ var poly;
+
+ for (var i = 0, length = this.points.length ; i< length; i++){
+ pts.push(this.points[i].toProprietary(this.api));
+ }
+ if (this.closed || pts[0].equals(pts[pts.length-1])) {
+ poly = new CM.Polygon(pts, this.color, this.width, this.opacity, this.fillColor || "#5462E3", this.opacity || "0.3");
+ } else {
+ poly = new CM.Polyline(pts, this.color, this.width, this.opacity);
+ }
+ return poly;
+ },
+
+ show: function() {
+ this.proprietary_polyline.show();
+ },
+
+ hide: function() {
+ this.proprietary_polyline.hide();
+ }
+
+ }
+
+});
diff --git a/plugins/Mapstraction/js/mxn.core.js b/plugins/Mapstraction/js/mxn.core.js
new file mode 100644
index 000000000..c75d0969e
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.core.js
@@ -0,0 +1,1758 @@
+(function(){
+
+/**
+ * @exports mxn.util.$m as $m
+ */
+var $m = mxn.util.$m;
+
+/**
+ * Initialise our provider. This function should only be called
+ * from within mapstraction code, not exposed as part of the API.
+ * @private
+ */
+var init = function() {
+ this.invoker.go('init', [ this.currentElement, this.api ]);
+ this.applyOptions();
+};
+
+/**
+ * Mapstraction instantiates a map with some API choice into the HTML element given
+ * @name mxn.Mapstraction
+ * @constructor
+ * @param {String} element The HTML element to replace with a map
+ * @param {String} api The API to use, one of 'google', 'googlev3', 'yahoo', 'microsoft', 'openstreetmap', 'multimap', 'map24', 'openlayers', 'mapquest'. If omitted, first loaded provider implementation is used.
+ * @param {Bool} debug optional parameter to turn on debug support - this uses alert panels for unsupported actions
+ * @exports Mapstraction as mxn.Mapstraction
+ */
+var Mapstraction = mxn.Mapstraction = function(element, api, debug) {
+ if (!api){
+ api = mxn.util.getAvailableProviders()[0];
+ }
+ this.api = api;
+ this.maps = {};
+ this.currentElement = $m(element);
+ this.eventListeners = [];
+ this.markers = [];
+ this.layers = [];
+ this.polylines = [];
+ this.images = [];
+ this.controls = [];
+ this.loaded = {};
+ this.onload = {};
+ this.element = element;
+
+ // option defaults
+ this.options = {
+ enableScrollWheelZoom: false,
+ enableDragging: true
+ };
+
+ this.addControlsArgs = {};
+
+ // set up our invoker for calling API methods
+ this.invoker = new mxn.Invoker(this, 'Mapstraction', function(){ return this.api; });
+
+ // Adding our events
+ mxn.addEvents(this, [
+
+ /**
+ * Map has loaded
+ * @name mxn.Mapstraction#load
+ * @event
+ */
+ 'load',
+
+ /**
+ * Map is clicked {location: LatLonPoint}
+ * @name mxn.Mapstraction#click
+ * @event
+ */
+ 'click',
+
+ /**
+ * Map is panned
+ * @name mxn.Mapstraction#endPan
+ * @event
+ */
+ 'endPan',
+
+ /**
+ * Zoom is changed
+ * @name mxn.Mapstraction#changeZoom
+ * @event
+ */
+ 'changeZoom',
+
+ /**
+ * Marker is removed {marker: Marker}
+ * @name mxn.Mapstraction#markerAdded
+ * @event
+ */
+ 'markerAdded',
+
+ /**
+ * Marker is removed {marker: Marker}
+ * @name mxn.Mapstraction#markerRemoved
+ * @event
+ */
+ 'markerRemoved',
+
+ /**
+ * Polyline is added {polyline: Polyline}
+ * @name mxn.Mapstraction#polylineAdded
+ * @event
+ */
+ 'polylineAdded',
+
+ /**
+ * Polyline is removed {polyline: Polyline}
+ * @name mxn.Mapstraction#polylineRemoved
+ * @event
+ */
+ 'polylineRemoved'
+ ]);
+
+ // finally initialize our proper API map
+ init.apply(this);
+};
+
+// Map type constants
+Mapstraction.ROAD = 1;
+Mapstraction.SATELLITE = 2;
+Mapstraction.HYBRID = 3;
+
+// methods that have no implementation in mapstraction core
+mxn.addProxyMethods(Mapstraction, [
+ /**
+ * Adds a large map panning control and zoom buttons to the map
+ * @name mxn.Mapstraction#addLargeControls
+ * @function
+ */
+ 'addLargeControls',
+
+ /**
+ * Adds a map type control to the map (streets, aerial imagery etc)
+ * @name mxn.Mapstraction#addMapTypeControls
+ * @function
+ */
+ 'addMapTypeControls',
+
+ /**
+ * Adds a GeoRSS or KML overlay to the map
+ * some flavors of GeoRSS and KML are not supported by some of the Map providers
+ * @name mxn.Mapstraction#addOverlay
+ * @function
+ * @param {String} url GeoRSS or KML feed URL
+ * @param {Boolean} autoCenterAndZoom Set true to auto center and zoom after the feed is loaded
+ */
+ 'addOverlay',
+
+ /**
+ * Adds a small map panning control and zoom buttons to the map
+ * @name mxn.Mapstraction#addSmallControls
+ * @function
+ */
+ 'addSmallControls',
+
+ /**
+ * Applies the current option settings
+ * @name mxn.Mapstraction#applyOptions
+ * @function
+ */
+ 'applyOptions',
+
+ /**
+ * Gets the BoundingBox of the map
+ * @name mxn.Mapstraction#getBounds
+ * @function
+ * @returns {BoundingBox} The bounding box for the current map state
+ */
+ 'getBounds',
+
+ /**
+ * Gets the central point of the map
+ * @name mxn.Mapstraction#getCenter
+ * @function
+ * @returns {LatLonPoint} The center point of the map
+ */
+ 'getCenter',
+
+ /**
+ * Gets the imagery type for the map.
+ * The type can be one of:
+ * mxn.Mapstraction.ROAD
+ * mxn.Mapstraction.SATELLITE
+ * mxn.Mapstraction.HYBRID
+ * @name mxn.Mapstraction#getMapType
+ * @function
+ * @returns {Number}
+ */
+ 'getMapType',
+
+ /**
+ * Returns a ratio to turn distance into pixels based on current projection
+ * @name mxn.Mapstraction#getPixelRatio
+ * @function
+ * @returns {Float} ratio
+ */
+ 'getPixelRatio',
+
+ /**
+ * Returns the zoom level of the map
+ * @name mxn.Mapstraction#getZoom
+ * @function
+ * @returns {Integer} The zoom level of the map
+ */
+ 'getZoom',
+
+ /**
+ * Returns the best zoom level for bounds given
+ * @name mxn.Mapstraction#getZoomLevelForBoundingBox
+ * @function
+ * @param {BoundingBox} bbox The bounds to fit
+ * @returns {Integer} The closest zoom level that contains the bounding box
+ */
+ 'getZoomLevelForBoundingBox',
+
+ /**
+ * Displays the coordinates of the cursor in the HTML element
+ * @name mxn.Mapstraction#mousePosition
+ * @function
+ * @param {String} element ID of the HTML element to display the coordinates in
+ */
+ 'mousePosition',
+
+ /**
+ * Resize the current map to the specified width and height
+ * (since it is actually on a child div of the mapElement passed
+ * as argument to the Mapstraction constructor, the resizing of this
+ * mapElement may have no effect on the size of the actual map)
+ * @name mxn.Mapstraction#resizeTo
+ * @function
+ * @param {Integer} width The width the map should be.
+ * @param {Integer} height The width the map should be.
+ */
+ 'resizeTo',
+
+ /**
+ * Sets the map to the appropriate location and zoom for a given BoundingBox
+ * @name mxn.Mapstraction#setBounds
+ * @function
+ * @param {BoundingBox} bounds The bounding box you want the map to show
+ */
+ 'setBounds',
+
+ /**
+ * setCenter sets the central point of the map
+ * @name mxn.Mapstraction#setCenter
+ * @function
+ * @param {LatLonPoint} point The point at which to center the map
+ * @param {Object} options Optional parameters
+ * @param {Boolean} options.pan Whether the map should move to the locations using a pan or just jump straight there
+ */
+ 'setCenter',
+
+ /**
+ * Centers the map to some place and zoom level
+ * @name mxn.Mapstraction#setCenterAndZoom
+ * @function
+ * @param {LatLonPoint} point Where the center of the map should be
+ * @param {Integer} zoom The zoom level where 0 is all the way out.
+ */
+ 'setCenterAndZoom',
+
+ /**
+ * Sets the imagery type for the map
+ * The type can be one of:
+ * mxn.Mapstraction.ROAD
+ * mxn.Mapstraction.SATELLITE
+ * mxn.Mapstraction.HYBRID
+ * @name mxn.Mapstraction#setMapType
+ * @function
+ * @param {Number} type
+ */
+ 'setMapType',
+
+ /**
+ * Sets the zoom level for the map
+ * MS doesn't seem to do zoom=0, and Gg's sat goes closer than it's maps, and MS's sat goes closer than Y!'s
+ * TODO: Mapstraction.prototype.getZoomLevels or something.
+ * @name mxn.Mapstraction#setZoom
+ * @function
+ * @param {Number} zoom The (native to the map) level zoom the map to.
+ */
+ 'setZoom',
+
+ /**
+ * Turns a Tile Layer on or off
+ * @name mxn.Mapstraction#toggleTileLayer
+ * @function
+ * @param {tile_url} url of the tile layer that was created.
+ */
+ 'toggleTileLayer'
+]);
+
+/**
+ * Sets the current options to those specified in oOpts and applies them
+ * @param {Object} oOpts Hash of options to set
+ */
+Mapstraction.prototype.setOptions = function(oOpts){
+ mxn.util.merge(this.options, oOpts);
+ this.applyOptions();
+};
+
+/**
+ * Sets an option and applies it.
+ * @param {String} sOptName Option name
+ * @param vVal Option value
+ */
+Mapstraction.prototype.setOption = function(sOptName, vVal){
+ this.options[sOptName] = vVal;
+ this.applyOptions();
+};
+
+/**
+ * Enable scroll wheel zooming
+ * @deprecated Use setOption instead.
+ */
+Mapstraction.prototype.enableScrollWheelZoom = function() {
+ this.setOption('enableScrollWheelZoom', true);
+};
+
+/**
+ * Enable/disable dragging of the map
+ * @param {Boolean} on
+ * @deprecated Use setOption instead.
+ */
+Mapstraction.prototype.dragging = function(on) {
+ this.setOption('enableDragging', on);
+};
+
+/**
+ * Change the current api on the fly
+ * @param {String} api The API to swap to
+ * @param element
+ */
+Mapstraction.prototype.swap = function(element,api) {
+ if (this.api === api) {
+ return;
+ }
+
+ var center = this.getCenter();
+ var zoom = this.getZoom();
+
+ this.currentElement.style.visibility = 'hidden';
+ this.currentElement.style.display = 'none';
+
+ this.currentElement = $m(element);
+ this.currentElement.style.visibility = 'visible';
+ this.currentElement.style.display = 'block';
+
+ this.api = api;
+
+ if (this.maps[this.api] === undefined) {
+ init.apply(this);
+
+ this.setCenterAndZoom(center,zoom);
+
+ for (var i = 0; i < this.markers.length; i++) {
+ this.addMarker(this.markers[i], true);
+ }
+
+ for (var j = 0; j < this.polylines.length; j++) {
+ this.addPolyline( this.polylines[j], true);
+ }
+ }
+ else {
+
+ //sync the view
+ this.setCenterAndZoom(center,zoom);
+
+ //TODO synchronize the markers and polylines too
+ // (any overlays created after api instantiation are not sync'd)
+ }
+
+ this.addControls(this.addControlsArgs);
+
+};
+
+/**
+ * Returns the loaded state of a Map Provider
+ * @param {String} api Optional API to query for. If not specified, returns state of the originally created API
+ */
+Mapstraction.prototype.isLoaded = function(api){
+ if (api === null) {
+ api = this.api;
+ }
+ return this.loaded[api];
+};
+
+/**
+ * Set the debugging on or off - shows alert panels for functions that don't exist in Mapstraction
+ * @param {Boolean} debug true to turn on debugging, false to turn it off
+ */
+Mapstraction.prototype.setDebug = function(debug){
+ if(debug !== null) {
+ this.debug = debug;
+ }
+ return this.debug;
+};
+
+
+/////////////////////////
+//
+// Event Handling
+//
+// FIXME need to consolidate some of these handlers...
+//
+///////////////////////////
+
+// Click handler attached to native API
+Mapstraction.prototype.clickHandler = function(lat, lon, me) {
+ this.callEventListeners('click', {
+ location: new LatLonPoint(lat, lon)
+ });
+};
+
+// Move and zoom handler attached to native API
+Mapstraction.prototype.moveendHandler = function(me) {
+ this.callEventListeners('moveend', {});
+};
+
+/**
+ * Add a listener for an event.
+ * @param {String} type Event type to attach listener to
+ * @param {Function} func Callback function
+ * @param {Object} caller Callback object
+ */
+Mapstraction.prototype.addEventListener = function() {
+ var listener = {};
+ listener.event_type = arguments[0];
+ listener.callback_function = arguments[1];
+
+ // added the calling object so we can retain scope of callback function
+ if(arguments.length == 3) {
+ listener.back_compat_mode = false;
+ listener.callback_object = arguments[2];
+ }
+ else {
+ listener.back_compat_mode = true;
+ listener.callback_object = null;
+ }
+ this.eventListeners.push(listener);
+};
+
+/**
+ * Call listeners for a particular event.
+ * @param {String} sEventType Call listeners of this event type
+ * @param {Object} oEventArgs Event args object to pass back to the callback
+ */
+Mapstraction.prototype.callEventListeners = function(sEventType, oEventArgs) {
+ oEventArgs.source = this;
+ for(var i = 0; i < this.eventListeners.length; i++) {
+ var evLi = this.eventListeners[i];
+ if(evLi.event_type == sEventType) {
+ // only two cases for this, click and move
+ if(evLi.back_compat_mode) {
+ if(evLi.event_type == 'click') {
+ evLi.callback_function(oEventArgs.location);
+ }
+ else {
+ evLi.callback_function();
+ }
+ }
+ else {
+ var scope = evLi.callback_object || this;
+ evLi.callback_function.call(scope, oEventArgs);
+ }
+ }
+ }
+};
+
+
+////////////////////
+//
+// map manipulation
+//
+/////////////////////
+
+
+/**
+ * addControls adds controls to the map. You specify which controls to add in
+ * the associative array that is the only argument.
+ * addControls can be called multiple time, with different args, to dynamically change controls.
+ *
+ * args = {
+ * pan: true,
+ * zoom: 'large' || 'small',
+ * overview: true,
+ * scale: true,
+ * map_type: true,
+ * }
+ * @param {array} args Which controls to switch on
+ */
+Mapstraction.prototype.addControls = function( args ) {
+ this.addControlsArgs = args;
+ this.invoker.go('addControls', arguments);
+};
+
+/**
+ * Adds a marker pin to the map
+ * @param {Marker} marker The marker to add
+ * @param {Boolean} old If true, doesn't add this marker to the markers array. Used by the "swap" method
+ */
+Mapstraction.prototype.addMarker = function(marker, old) {
+ marker.mapstraction = this;
+ marker.api = this.api;
+ marker.location.api = this.api;
+ marker.map = this.maps[this.api];
+ var propMarker = this.invoker.go('addMarker', arguments);
+ marker.setChild(propMarker);
+ if (!old) {
+ this.markers.push(marker);
+ }
+ this.markerAdded.fire({'marker': marker});
+};
+
+/**
+ * addMarkerWithData will addData to the marker, then add it to the map
+ * @param {Marker} marker The marker to add
+ * @param {Object} data A data has to add
+ */
+Mapstraction.prototype.addMarkerWithData = function(marker, data) {
+ marker.addData(data);
+ this.addMarker(marker);
+};
+
+/**
+ * addPolylineWithData will addData to the polyline, then add it to the map
+ * @param {Polyline} polyline The polyline to add
+ * @param {Object} data A data has to add
+ */
+Mapstraction.prototype.addPolylineWithData = function(polyline, data) {
+ polyline.addData(data);
+ this.addPolyline(polyline);
+};
+
+/**
+ * removeMarker removes a Marker from the map
+ * @param {Marker} marker The marker to remove
+ */
+Mapstraction.prototype.removeMarker = function(marker) {
+ var current_marker;
+ for(var i = 0; i < this.markers.length; i++){
+ current_marker = this.markers[i];
+ if(marker == current_marker) {
+ this.invoker.go('removeMarker', arguments);
+ marker.onmap = false;
+ this.markers.splice(i, 1);
+ this.markerRemoved.fire({'marker': marker});
+ break;
+ }
+ }
+};
+
+/**
+ * removeAllMarkers removes all the Markers on a map
+ */
+Mapstraction.prototype.removeAllMarkers = function() {
+ var current_marker;
+ while(this.markers.length > 0) {
+ current_marker = this.markers.pop();
+ this.invoker.go('removeMarker', [current_marker]);
+ }
+};
+
+/**
+ * Declutter the markers on the map, group together overlapping markers.
+ * @param {Object} opts Declutter options
+ */
+Mapstraction.prototype.declutterMarkers = function(opts) {
+ if(this.loaded[this.api] === false) {
+ var me = this;
+ this.onload[this.api].push( function() {
+ me.declutterMarkers(opts);
+ } );
+ return;
+ }
+
+ var map = this.maps[this.api];
+
+ switch(this.api)
+ {
+ // case 'yahoo':
+ //
+ // break;
+ // case 'google':
+ //
+ // break;
+ // case 'openstreetmap':
+ //
+ // break;
+ // case 'microsoft':
+ //
+ // break;
+ // case 'openlayers':
+ //
+ // break;
+ case 'multimap':
+ /*
+ * Multimap supports quite a lot of decluttering options such as whether
+ * to use an accurate of fast declutter algorithm and what icon to use to
+ * represent a cluster. Using all this would mean abstracting all the enums
+ * etc so we're only implementing the group name function at the moment.
+ */
+ map.declutterGroup(opts.groupName);
+ break;
+ // case 'mapquest':
+ //
+ // break;
+ // case 'map24':
+ //
+ // break;
+ case ' dummy':
+ break;
+ default:
+ if(this.debug) {
+ alert(this.api + ' not supported by Mapstraction.declutterMarkers');
+ }
+ }
+};
+
+/**
+ * Add a polyline to the map
+ * @param {Polyline} polyline The Polyline to add to the map
+ * @param {Boolean} old If true replaces an existing Polyline
+ */
+Mapstraction.prototype.addPolyline = function(polyline, old) {
+ polyline.api = this.api;
+ polyline.map = this.maps[this.api];
+ var propPoly = this.invoker.go('addPolyline', arguments);
+ polyline.setChild(propPoly);
+ if(!old) {
+ this.polylines.push(polyline);
+ }
+ this.polylineAdded.fire({'polyline': polyline});
+};
+
+// Private remove implementation
+var removePolylineImpl = function(polyline) {
+ this.invoker.go('removePolyline', arguments);
+ polyline.onmap = false;
+ this.polylineRemoved.fire({'polyline': polyline});
+};
+
+/**
+ * Remove the polyline from the map
+ * @param {Polyline} polyline The Polyline to remove from the map
+ */
+Mapstraction.prototype.removePolyline = function(polyline) {
+ var current_polyline;
+ for(var i = 0; i < this.polylines.length; i++){
+ current_polyline = this.polylines[i];
+ if(polyline == current_polyline) {
+ this.polylines.splice(i, 1);
+ removePolylineImpl.call(this, polyline);
+ break;
+ }
+ }
+};
+
+/**
+ * Removes all polylines from the map
+ */
+Mapstraction.prototype.removeAllPolylines = function() {
+ var current_polyline;
+ while(this.polylines.length > 0) {
+ current_polyline = this.polylines.pop();
+ removePolylineImpl.call(this, current_polyline);
+ }
+};
+
+/**
+ * autoCenterAndZoom sets the center and zoom of the map to the smallest bounding box
+ * containing all markers
+ */
+Mapstraction.prototype.autoCenterAndZoom = function() {
+ var lat_max = -90;
+ var lat_min = 90;
+ var lon_max = -180;
+ var lon_min = 180;
+ var lat, lon;
+ var checkMinMax = function(){
+ if (lat > lat_max) {
+ lat_max = lat;
+ }
+ if (lat < lat_min) {
+ lat_min = lat;
+ }
+ if (lon > lon_max) {
+ lon_max = lon;
+ }
+ if (lon < lon_min) {
+ lon_min = lon;
+ }
+ };
+ for (var i = 0; i < this.markers.length; i++) {
+ lat = this.markers[i].location.lat;
+ lon = this.markers[i].location.lon;
+ checkMinMax();
+ }
+ for(i = 0; i < this.polylines.length; i++) {
+ for (var j = 0; j < this.polylines[i].points.length; j++) {
+ lat = this.polylines[i].points[j].lat;
+ lon = this.polylines[i].points[j].lon;
+ checkMinMax();
+ }
+ }
+ this.setBounds( new BoundingBox(lat_min, lon_min, lat_max, lon_max) );
+};
+
+/**
+ * centerAndZoomOnPoints sets the center and zoom of the map from an array of points
+ *
+ * This is useful if you don't want to have to add markers to the map
+ */
+Mapstraction.prototype.centerAndZoomOnPoints = function(points) {
+ var bounds = new BoundingBox(points[0].lat,points[0].lon,points[0].lat,points[0].lon);
+
+ for (var i=1, len = points.length ; i<len; i++) {
+ bounds.extend(points[i]);
+ }
+
+ this.setBounds(bounds);
+};
+
+/**
+ * Sets the center and zoom of the map to the smallest bounding box
+ * containing all visible markers and polylines
+ * will only include markers and polylines with an attribute of "visible"
+ */
+Mapstraction.prototype.visibleCenterAndZoom = function() {
+ var lat_max = -90;
+ var lat_min = 90;
+ var lon_max = -180;
+ var lon_min = 180;
+ var lat, lon;
+ var checkMinMax = function(){
+ if (lat > lat_max) {
+ lat_max = lat;
+ }
+ if (lat < lat_min) {
+ lat_min = lat;
+ }
+ if (lon > lon_max) {
+ lon_max = lon;
+ }
+ if (lon < lon_min) {
+ lon_min = lon;
+ }
+ };
+ for (var i=0; i<this.markers.length; i++) {
+ if (this.markers[i].getAttribute("visible")) {
+ lat = this.markers[i].location.lat;
+ lon = this.markers[i].location.lon;
+ checkMinMax();
+ }
+ }
+
+ for (i=0; i<this.polylines.length; i++){
+ if (this.polylines[i].getAttribute("visible")) {
+ for (j=0; j<this.polylines[i].points.length; j++) {
+ lat = this.polylines[i].points[j].lat;
+ lon = this.polylines[i].points[j].lon;
+ checkMinMax();
+ }
+ }
+ }
+
+ this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
+};
+
+/**
+ * Automatically sets center and zoom level to show all polylines
+ * Takes into account radious of polyline
+ * @param {Int} radius
+ */
+Mapstraction.prototype.polylineCenterAndZoom = function(radius) {
+ var lat_max = -90;
+ var lat_min = 90;
+ var lon_max = -180;
+ var lon_min = 180;
+
+ for (var i=0; i < mapstraction.polylines.length; i++)
+ {
+ for (var j=0; j<mapstraction.polylines[i].points.length; j++)
+ {
+ lat = mapstraction.polylines[i].points[j].lat;
+ lon = mapstraction.polylines[i].points[j].lon;
+
+ latConv = lonConv = radius;
+
+ if (radius > 0)
+ {
+ latConv = (radius / mapstraction.polylines[i].points[j].latConv());
+ lonConv = (radius / mapstraction.polylines[i].points[j].lonConv());
+ }
+
+ if ((lat + latConv) > lat_max) {
+ lat_max = (lat + latConv);
+ }
+ if ((lat - latConv) < lat_min) {
+ lat_min = (lat - latConv);
+ }
+ if ((lon + lonConv) > lon_max) {
+ lon_max = (lon + lonConv);
+ }
+ if ((lon - lonConv) < lon_min) {
+ lon_min = (lon - lonConv);
+ }
+ }
+ }
+
+ this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
+};
+
+/**
+ * addImageOverlay layers an georeferenced image over the map
+ * @param {id} unique DOM identifier
+ * @param {src} url of image
+ * @param {opacity} opacity 0-100
+ * @param {west} west boundary
+ * @param {south} south boundary
+ * @param {east} east boundary
+ * @param {north} north boundary
+ */
+Mapstraction.prototype.addImageOverlay = function(id, src, opacity, west, south, east, north) {
+
+ var b = document.createElement("img");
+ b.style.display = 'block';
+ b.setAttribute('id',id);
+ b.setAttribute('src',src);
+ b.style.position = 'absolute';
+ b.style.zIndex = 1;
+ b.setAttribute('west',west);
+ b.setAttribute('south',south);
+ b.setAttribute('east',east);
+ b.setAttribute('north',north);
+
+ var oContext = {
+ imgElm: b
+ };
+
+ this.invoker.go('addImageOverlay', arguments, { context: oContext });
+};
+
+Mapstraction.prototype.setImageOpacity = function(id, opacity) {
+ if (opacity < 0) {
+ opacity = 0;
+ }
+ if (opacity >= 100) {
+ opacity = 100;
+ }
+ var c = opacity / 100;
+ var d = document.getElementById(id);
+ if(typeof(d.style.filter)=='string'){
+ d.style.filter='alpha(opacity:'+opacity+')';
+ }
+ if(typeof(d.style.KHTMLOpacity)=='string'){
+ d.style.KHTMLOpacity=c;
+ }
+ if(typeof(d.style.MozOpacity)=='string'){
+ d.style.MozOpacity=c;
+ }
+ if(typeof(d.style.opacity)=='string'){
+ d.style.opacity=c;
+ }
+};
+
+Mapstraction.prototype.setImagePosition = function(id) {
+ var imgElement = document.getElementById(id);
+ var oContext = {
+ latLng: {
+ top: imgElement.getAttribute('north'),
+ left: imgElement.getAttribute('west'),
+ bottom: imgElement.getAttribute('south'),
+ right: imgElement.getAttribute('east')
+ },
+ pixels: { top: 0, right: 0, bottom: 0, left: 0 }
+ };
+
+ this.invoker.go('setImagePosition', arguments, { context: oContext });
+
+ imgElement.style.top = oContext.pixels.top.toString() + 'px';
+ imgElement.style.left = oContext.pixels.left.toString() + 'px';
+ imgElement.style.width = (oContext.pixels.right - oContext.pixels.left).toString() + 'px';
+ imgElement.style.height = (oContext.pixels.bottom - oContext.pixels.top).toString() + 'px';
+};
+
+Mapstraction.prototype.addJSON = function(json) {
+ var features;
+ if (typeof(json) == "string") {
+ features = eval('(' + json + ')');
+ } else {
+ features = json;
+ }
+ features = features.features;
+ var map = this.maps[this.api];
+ var html = "";
+ var item;
+ var polyline;
+ var marker;
+ var markers = [];
+
+ if(features.type == "FeatureCollection") {
+ this.addJSON(features.features);
+ }
+
+ for (var i = 0; i < features.length; i++) {
+ item = features[i];
+ switch(item.geometry.type) {
+ case "Point":
+ html = "<strong>" + item.title + "</strong><p>" + item.description + "</p>";
+ marker = new Marker(new LatLonPoint(item.geometry.coordinates[1],item.geometry.coordinates[0]));
+ markers.push(marker);
+ this.addMarkerWithData(marker,{
+ infoBubble : html,
+ label : item.title,
+ date : "new Date(\""+item.date+"\")",
+ iconShadow : item.icon_shadow,
+ marker : item.id,
+ iconShadowSize : item.icon_shadow_size,
+ icon : "http://boston.openguides.org/markers/AQUA.png",
+ iconSize : item.icon_size,
+ category : item.source_id,
+ draggable : false,
+ hover : false
+ });
+ break;
+ case "Polygon":
+ var points = [];
+ polyline = new Polyline(points);
+ mapstraction.addPolylineWithData(polyline,{
+ fillColor : item.poly_color,
+ date : "new Date(\""+item.date+"\")",
+ category : item.source_id,
+ width : item.line_width,
+ opacity : item.line_opacity,
+ color : item.line_color,
+ polygon : true
+ });
+ markers.push(polyline);
+ break;
+ default:
+ // console.log("Geometry: " + features.items[i].geometry.type);
+ }
+ }
+ return markers;
+};
+
+/**
+ * Adds a Tile Layer to the map
+ *
+ * Requires providing a parameterized tile url. Use {Z}, {X}, and {Y} to specify where the parameters
+ * should go in the URL.
+ *
+ * For example, the OpenStreetMap tiles are:
+ * m.addTileLayer("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png", 1.0, "OSM", 1, 19, true);
+ *
+ * @param {tile_url} template url of the tiles.
+ * @param {opacity} opacity of the tile layer - 0 is transparent, 1 is opaque. (default=0.6)
+ * @param {copyright_text} copyright text to use for the tile layer. (default=Mapstraction)
+ * @param {min_zoom} Minimum (furtherest out) zoom level that tiles are available (default=1)
+ * @param {max_zoom} Maximum (closest) zoom level that the tiles are available (default=18)
+ * @param {map_type} Should the tile layer be a selectable map type in the layers palette (default=false)
+ */
+Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
+ if(!tile_url) {
+ return;
+ }
+
+ this.tileLayers = this.tileLayers || [];
+ opacity = opacity || 0.6;
+ copyright_text = copyright_text || "Mapstraction";
+ min_zoom = min_zoom || 1;
+ max_zoom = max_zoom || 18;
+ map_type = map_type || false;
+
+ return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type] );
+};
+
+/**
+ * addFilter adds a marker filter
+ * @param {field} name of attribute to filter on
+ * @param {operator} presently only "ge" or "le"
+ * @param {value} the value to compare against
+ */
+Mapstraction.prototype.addFilter = function(field, operator, value) {
+ if (!this.filters) {
+ this.filters = [];
+ }
+ this.filters.push( [field, operator, value] );
+};
+
+/**
+ * Remove the specified filter
+ * @param {Object} field
+ * @param {Object} operator
+ * @param {Object} value
+ */
+Mapstraction.prototype.removeFilter = function(field, operator, value) {
+ if (!this.filters) {
+ return;
+ }
+
+ var del;
+ for (var f=0; f<this.filters.length; f++) {
+ if (this.filters[f][0] == field &&
+ (! operator || (this.filters[f][1] == operator && this.filters[f][2] == value))) {
+ this.filters.splice(f,1);
+ f--; //array size decreased
+ }
+ }
+};
+
+/**
+ * Delete the current filter if present; otherwise add it
+ * @param {Object} field
+ * @param {Object} operator
+ * @param {Object} value
+ */
+Mapstraction.prototype.toggleFilter = function(field, operator, value) {
+ if (!this.filters) {
+ this.filters = [];
+ }
+
+ var found = false;
+ for (var f = 0; f < this.filters.length; f++) {
+ if (this.filters[f][0] == field && this.filters[f][1] == operator && this.filters[f][2] == value) {
+ this.filters.splice(f,1);
+ f--; //array size decreased
+ found = true;
+ }
+ }
+
+ if (! found) {
+ this.addFilter(field, operator, value);
+ }
+};
+
+/**
+ * removeAllFilters
+ */
+Mapstraction.prototype.removeAllFilters = function() {
+ this.filters = [];
+};
+
+/**
+ * doFilter executes all filters added since last call
+ * Now supports a callback function for when a marker is shown or hidden
+ * @param {Function} showCallback
+ * @param {Function} hideCallback
+ * @returns {Int} count of visible markers
+ */
+Mapstraction.prototype.doFilter = function(showCallback, hideCallback) {
+ var map = this.maps[this.api];
+ var visibleCount = 0;
+ var f;
+ if (this.filters) {
+ switch (this.api) {
+ case 'multimap':
+ /* TODO polylines aren't filtered in multimap */
+ var mmfilters = [];
+ for (f=0; f<this.filters.length; f++) {
+ mmfilters.push( new MMSearchFilter( this.filters[f][0], this.filters[f][1], this.filters[f][2] ));
+ }
+ map.setMarkerFilters( mmfilters );
+ map.redrawMap();
+ break;
+ case ' dummy':
+ break;
+ default:
+ var vis;
+ for (var m=0; m<this.markers.length; m++) {
+ vis = true;
+ for (f = 0; f < this.filters.length; f++) {
+ if (! this.applyFilter(this.markers[m], this.filters[f])) {
+ vis = false;
+ }
+ }
+ if (vis) {
+ visibleCount ++;
+ if (showCallback){
+ showCallback(this.markers[m]);
+ }
+ else {
+ this.markers[m].show();
+ }
+ }
+ else {
+ if (hideCallback){
+ hideCallback(this.markers[m]);
+ }
+ else {
+ this.markers[m].hide();
+ }
+ }
+
+ this.markers[m].setAttribute("visible", vis);
+ }
+ break;
+ }
+ }
+ return visibleCount;
+};
+
+Mapstraction.prototype.applyFilter = function(o, f) {
+ var vis = true;
+ switch (f[1]) {
+ case 'ge':
+ if (o.getAttribute( f[0] ) < f[2]) {
+ vis = false;
+ }
+ break;
+ case 'le':
+ if (o.getAttribute( f[0] ) > f[2]) {
+ vis = false;
+ }
+ break;
+ case 'eq':
+ if (o.getAttribute( f[0] ) == f[2]) {
+ vis = false;
+ }
+ break;
+ }
+
+ return vis;
+};
+
+/**
+ * getAttributeExtremes returns the minimum/maximum of "field" from all markers
+ * @param {field} name of "field" to query
+ * @returns {array} of minimum/maximum
+ */
+Mapstraction.prototype.getAttributeExtremes = function(field) {
+ var min;
+ var max;
+ for (var m=0; m<this.markers.length; m++) {
+ if (! min || min > this.markers[m].getAttribute(field)) {
+ min = this.markers[m].getAttribute(field);
+ }
+ if (! max || max < this.markers[m].getAttribute(field)) {
+ max = this.markers[m].getAttribute(field);
+ }
+ }
+ for (var p=0; m<this.polylines.length; m++) {
+ if (! min || min > this.polylines[p].getAttribute(field)) {
+ min = this.polylines[p].getAttribute(field);
+ }
+ if (! max || max < this.polylines[p].getAttribute(field)) {
+ max = this.polylines[p].getAttribute(field);
+ }
+ }
+
+ return [min, max];
+};
+
+/**
+ * getMap returns the native map object that mapstraction is talking to
+ * @returns the native map object mapstraction is using
+ */
+Mapstraction.prototype.getMap = function() {
+ // FIXME in an ideal world this shouldn't exist right?
+ return this.maps[this.api];
+};
+
+
+//////////////////////////////
+//
+// LatLonPoint
+//
+/////////////////////////////
+
+/**
+ * LatLonPoint is a point containing a latitude and longitude with helper methods
+ * @name mxn.LatLonPoint
+ * @constructor
+ * @param {double} lat is the latitude
+ * @param {double} lon is the longitude
+ * @exports LatLonPoint as mxn.LatLonPoint
+ */
+var LatLonPoint = mxn.LatLonPoint = function(lat, lon) {
+ // TODO error if undefined?
+ // if (lat == undefined) alert('undefined lat');
+ // if (lon == undefined) alert('undefined lon');
+ this.lat = lat;
+ this.lon = lon;
+ this.lng = lon; // lets be lon/lng agnostic
+
+ this.invoker = new mxn.Invoker(this, 'LatLonPoint');
+};
+
+mxn.addProxyMethods(LatLonPoint, [
+ 'fromProprietary', 'toProprietary'
+], true);
+
+/**
+ * toString returns a string represntation of a point
+ * @returns a string like '51.23, -0.123'
+ * @type String
+ */
+LatLonPoint.prototype.toString = function() {
+ return this.lat + ', ' + this.lon;
+};
+
+/**
+ * distance returns the distance in kilometers between two points
+ * @param {LatLonPoint} otherPoint The other point to measure the distance from to this one
+ * @returns the distance between the points in kilometers
+ * @type double
+ */
+LatLonPoint.prototype.distance = function(otherPoint) {
+ // Uses Haversine formula from http://www.movable-type.co.uk
+ var rads = Math.PI / 180;
+ var diffLat = (this.lat-otherPoint.lat) * rads;
+ var diffLon = (this.lon-otherPoint.lon) * rads;
+ var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
+ Math.cos(this.lat*rads) * Math.cos(otherPoint.lat*rads) *
+ Math.sin(diffLon/2) * Math.sin(diffLon/2);
+ return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371; // Earth's mean radius in km
+};
+
+/**
+ * equals tests if this point is the same as some other one
+ * @param {LatLonPoint} otherPoint The other point to test with
+ * @returns true or false
+ * @type boolean
+ */
+LatLonPoint.prototype.equals = function(otherPoint) {
+ return this.lat == otherPoint.lat && this.lon == otherPoint.lon;
+};
+
+/**
+ * Returns latitude conversion based on current projection
+ * @returns {Float} conversion
+ */
+LatLonPoint.prototype.latConv = function() {
+ return this.distance(new LatLonPoint(this.lat + 0.1, this.lon))*10;
+};
+
+/**
+ * Returns longitude conversion based on current projection
+ * @returns {Float} conversion
+ */
+LatLonPoint.prototype.lonConv = function() {
+ return this.distance(new LatLonPoint(this.lat, this.lon + 0.1))*10;
+};
+
+
+//////////////////////////
+//
+// BoundingBox
+//
+//////////////////////////
+
+/**
+ * BoundingBox creates a new bounding box object
+ * @name mxn.BoundingBox
+ * @constructor
+ * @param {double} swlat the latitude of the south-west point
+ * @param {double} swlon the longitude of the south-west point
+ * @param {double} nelat the latitude of the north-east point
+ * @param {double} nelon the longitude of the north-east point
+ * @exports BoundingBox as mxn.BoundingBox
+ */
+var BoundingBox = mxn.BoundingBox = function(swlat, swlon, nelat, nelon) {
+ //FIXME throw error if box bigger than world
+ //alert('new bbox ' + swlat + ',' + swlon + ',' + nelat + ',' + nelon);
+ this.sw = new LatLonPoint(swlat, swlon);
+ this.ne = new LatLonPoint(nelat, nelon);
+};
+
+/**
+ * getSouthWest returns a LatLonPoint of the south-west point of the bounding box
+ * @returns the south-west point of the bounding box
+ * @type LatLonPoint
+ */
+BoundingBox.prototype.getSouthWest = function() {
+ return this.sw;
+};
+
+/**
+ * getNorthEast returns a LatLonPoint of the north-east point of the bounding box
+ * @returns the north-east point of the bounding box
+ * @type LatLonPoint
+ */
+BoundingBox.prototype.getNorthEast = function() {
+ return this.ne;
+};
+
+/**
+ * isEmpty finds if this bounding box has zero area
+ * @returns whether the north-east and south-west points of the bounding box are the same point
+ * @type boolean
+ */
+BoundingBox.prototype.isEmpty = function() {
+ return this.ne == this.sw; // is this right? FIXME
+};
+
+/**
+ * contains finds whether a given point is within a bounding box
+ * @param {LatLonPoint} point the point to test with
+ * @returns whether point is within this bounding box
+ * @type boolean
+ */
+BoundingBox.prototype.contains = function(point){
+ return point.lat >= this.sw.lat && point.lat <= this.ne.lat && point.lon >= this.sw.lon && point.lon <= this.ne.lon;
+};
+
+/**
+ * toSpan returns a LatLonPoint with the lat and lon as the height and width of the bounding box
+ * @returns a LatLonPoint containing the height and width of this bounding box
+ * @type LatLonPoint
+ */
+BoundingBox.prototype.toSpan = function() {
+ return new LatLonPoint( Math.abs(this.sw.lat - this.ne.lat), Math.abs(this.sw.lon - this.ne.lon) );
+};
+
+/**
+ * extend extends the bounding box to include the new point
+ */
+BoundingBox.prototype.extend = function(point) {
+ if(this.sw.lat > point.lat) {
+ this.sw.lat = point.lat;
+ }
+ if(this.sw.lon > point.lon) {
+ this.sw.lon = point.lon;
+ }
+ if(this.ne.lat < point.lat) {
+ this.ne.lat = point.lat;
+ }
+ if(this.ne.lon < point.lon) {
+ this.ne.lon = point.lon;
+ }
+ return;
+};
+
+//////////////////////////////
+//
+// Marker
+//
+///////////////////////////////
+
+/**
+ * Marker create's a new marker pin
+ * @name mxn.Marker
+ * @constructor
+ * @param {LatLonPoint} point the point on the map where the marker should go
+ * @exports Marker as mxn.Marker
+ */
+var Marker = mxn.Marker = function(point) {
+ this.api = null;
+ this.location = point;
+ this.onmap = false;
+ this.proprietary_marker = false;
+ this.attributes = [];
+ this.invoker = new mxn.Invoker(this, 'Marker', function(){return this.api;});
+ mxn.addEvents(this, [
+ 'openInfoBubble', // Info bubble opened
+ 'closeInfoBubble', // Info bubble closed
+ 'click' // Marker clicked
+ ]);
+};
+
+mxn.addProxyMethods(Marker, [
+ 'fromProprietary',
+ 'hide',
+ 'openBubble',
+ 'show',
+ 'toProprietary',
+ 'update'
+]);
+
+Marker.prototype.setChild = function(some_proprietary_marker) {
+ this.proprietary_marker = some_proprietary_marker;
+ some_proprietary_marker.mapstraction_marker = this;
+ this.onmap = true;
+};
+
+Marker.prototype.setLabel = function(labelText) {
+ this.labelText = labelText;
+};
+
+/**
+ * addData conviniently set a hash of options on a marker
+ */
+Marker.prototype.addData = function(options){
+ for(var sOptKey in options) {
+ if(options.hasOwnProperty(sOptKey)){
+ switch(sOptKey) {
+ case 'label':
+ this.setLabel(options.label);
+ break;
+ case 'infoBubble':
+ this.setInfoBubble(options.infoBubble);
+ break;
+ case 'icon':
+ if(options.iconSize && options.iconAnchor) {
+ this.setIcon(options.icon, options.iconSize, options.iconAnchor);
+ }
+ else if(options.iconSize) {
+ this.setIcon(options.icon, options.iconSize);
+ }
+ else {
+ this.setIcon(options.icon);
+ }
+ break;
+ case 'iconShadow':
+ if(options.iconShadowSize) {
+ this.setShadowIcon(options.iconShadow, [ options.iconShadowSize[0], options.iconShadowSize[1] ]);
+ }
+ else {
+ this.setIcon(options.iconShadow);
+ }
+ break;
+ case 'infoDiv':
+ this.setInfoDiv(options.infoDiv[0],options.infoDiv[1]);
+ break;
+ case 'draggable':
+ this.setDraggable(options.draggable);
+ break;
+ case 'hover':
+ this.setHover(options.hover);
+ this.setHoverIcon(options.hoverIcon);
+ break;
+ case 'hoverIcon':
+ this.setHoverIcon(options.hoverIcon);
+ break;
+ case 'openBubble':
+ this.openBubble();
+ break;
+ case 'groupName':
+ this.setGroupName(options.groupName);
+ break;
+ default:
+ // don't have a specific action for this bit of
+ // data so set a named attribute
+ this.setAttribute(sOptKey, options[sOptKey]);
+ break;
+ }
+ }
+ }
+};
+
+/**
+ * setInfoBubble sets the html/text content for a bubble popup for a marker
+ * @param {String} infoBubble the html/text you want displayed
+ */
+Marker.prototype.setInfoBubble = function(infoBubble) {
+ this.infoBubble = infoBubble;
+};
+
+/**
+ * setInfoDiv sets the text and the id of the div element where to the information
+ * useful for putting information in a div outside of the map
+ * @param {String} infoDiv the html/text you want displayed
+ * @param {String} div the element id to use for displaying the text/html
+ */
+Marker.prototype.setInfoDiv = function(infoDiv,div){
+ this.infoDiv = infoDiv;
+ this.div = div;
+};
+
+/**
+ * setIcon sets the icon for a marker
+ * @param {String} iconUrl The URL of the image you want to be the icon
+ */
+Marker.prototype.setIcon = function(iconUrl, iconSize, iconAnchor) {
+ this.iconUrl = iconUrl;
+ if(iconSize) {
+ this.iconSize = iconSize;
+ }
+ if(iconAnchor) {
+ this.iconAnchor = iconAnchor;
+ }
+};
+
+/**
+ * setIconSize sets the size of the icon for a marker
+ * @param {String} iconSize The array size in pixels of the marker image
+ */
+Marker.prototype.setIconSize = function(iconSize){
+ if(iconSize) {
+ this.iconSize = iconSize;
+ }
+};
+
+/**
+ * setIconAnchor sets the anchor point for a marker
+ * @param {String} iconAnchor The array offset of the anchor point
+ */
+Marker.prototype.setIconAnchor = function(iconAnchor){
+ if(iconAnchor) {
+ this.iconAnchor = iconAnchor;
+ }
+};
+
+/**
+ * setShadowIcon sets the icon for a marker
+ * @param {String} iconUrl The URL of the image you want to be the icon
+ */
+Marker.prototype.setShadowIcon = function(iconShadowUrl, iconShadowSize){
+ this.iconShadowUrl = iconShadowUrl;
+ if(iconShadowSize) {
+ this.iconShadowSize = iconShadowSize;
+ }
+};
+
+Marker.prototype.setHoverIcon = function(hoverIconUrl){
+ this.hoverIconUrl = hoverIconUrl;
+};
+
+/**
+ * setDraggable sets the draggable state of the marker
+ * @param {Bool} draggable set to true if marker should be draggable by the user
+ */
+Marker.prototype.setDraggable = function(draggable) {
+ this.draggable = draggable;
+};
+
+/**
+ * setHover sets that the marker info is displayed on hover
+ * @param {Bool} hover set to true if marker should display info on hover
+ */
+Marker.prototype.setHover = function(hover) {
+ this.hover = hover;
+};
+
+/**
+ * Markers are grouped up by this name. declutterGroup makes use of this.
+ */
+Marker.prototype.setGroupName = function(sGrpName) {
+ this.groupName = sGrpName;
+};
+
+/**
+ * setAttribute: set an arbitrary key/value pair on a marker
+ * @arg(String) key
+ * @arg value
+ */
+Marker.prototype.setAttribute = function(key,value) {
+ this.attributes[key] = value;
+};
+
+/**
+ * getAttribute: gets the value of "key"
+ * @arg(String) key
+ * @returns value
+ */
+Marker.prototype.getAttribute = function(key) {
+ return this.attributes[key];
+};
+
+
+///////////////
+// Polyline ///
+///////////////
+
+/**
+ * Instantiates a new Polyline.
+ * @name mxn.Polyline
+ * @constructor
+ * @param {Point[]} points Points that make up the Polyline.
+ * @exports Polyline as mxn.Polyline
+ */
+var Polyline = mxn.Polyline = function(points) {
+ this.api = null;
+ this.points = points;
+ this.attributes = [];
+ this.onmap = false;
+ this.proprietary_polyline = false;
+ this.pllID = "mspll-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
+ this.invoker = new mxn.Invoker(this, 'Polyline', function(){return this.api;});
+};
+
+mxn.addProxyMethods(Polyline, [
+ 'fromProprietary',
+ 'hide',
+ 'show',
+ 'toProprietary',
+ 'update'
+]);
+
+/**
+ * addData conviniently set a hash of options on a polyline
+ */
+Polyline.prototype.addData = function(options){
+ for(var sOpt in options) {
+ if(options.hasOwnProperty(sOpt)){
+ switch(sOpt) {
+ case 'color':
+ this.setColor(options.color);
+ break;
+ case 'width':
+ this.setWidth(options.width);
+ break;
+ case 'opacity':
+ this.setOpacity(options.opacity);
+ break;
+ case 'closed':
+ this.setClosed(options.closed);
+ break;
+ case 'fillColor':
+ this.setFillColor(options.fillColor);
+ break;
+ default:
+ this.setAttribute(sOpt, options[sOpt]);
+ break;
+ }
+ }
+ }
+};
+
+Polyline.prototype.setChild = function(some_proprietary_polyline) {
+ this.proprietary_polyline = some_proprietary_polyline;
+ this.onmap = true;
+};
+
+/**
+ * in the form: #RRGGBB
+ * Note map24 insists on upper case, so we convert it.
+ */
+Polyline.prototype.setColor = function(color){
+ this.color = (color.length==7 && color[0]=="#") ? color.toUpperCase() : color;
+};
+
+/**
+ * Stroke width of the polyline
+ * @param {Integer} width
+ */
+Polyline.prototype.setWidth = function(width){
+ this.width = width;
+};
+
+/**
+ * A float between 0.0 and 1.0
+ * @param {Float} opacity
+ */
+Polyline.prototype.setOpacity = function(opacity){
+ this.opacity = opacity;
+};
+
+/**
+ * Marks the polyline as a closed polygon
+ * @param {Boolean} bClosed
+ */
+Polyline.prototype.setClosed = function(bClosed){
+ this.closed = bClosed;
+};
+
+/**
+ * Fill color for a closed polyline as HTML color value e.g. #RRGGBB
+ * @param {String} sFillColor HTML color value #RRGGBB
+ */
+Polyline.prototype.setFillColor = function(sFillColor) {
+ this.fillColor = sFillColor;
+};
+
+
+/**
+ * setAttribute: set an arbitrary key/value pair on a polyline
+ * @arg(String) key
+ * @arg value
+ */
+Polyline.prototype.setAttribute = function(key,value) {
+ this.attributes[key] = value;
+};
+
+/**
+ * getAttribute: gets the value of "key"
+ * @arg(String) key
+ * @returns value
+ */
+Polyline.prototype.getAttribute = function(key) {
+ return this.attributes[key];
+};
+
+/**
+ * Simplifies a polyline, averaging and reducing the points
+ * @param {Integer} tolerance (1.0 is a good starting point)
+ */
+Polyline.prototype.simplify = function(tolerance) {
+ var reduced = [];
+
+ // First point
+ reduced[0] = this.points[0];
+
+ var markerPoint = 0;
+
+ for (var i = 1; i < this.points.length-1; i++){
+ if (this.points[i].distance(this.points[markerPoint]) >= tolerance)
+ {
+ reduced[reduced.length] = this.points[i];
+ markerPoint = i;
+ }
+ }
+
+ // Last point
+ reduced[reduced.length] = this.points[this.points.length-1];
+
+ // Revert
+ this.points = reduced;
+};
+
+///////////////
+// Radius //
+///////////////
+
+/**
+ * Creates a new radius object for drawing circles around a point, does a lot of initial calculation to increase load time
+ * @returns a new Radius
+ * @type Radius
+ * @constructor
+ * @classDescription Radius
+ * @param {Object} Center LatLonPoint of the radius
+ * @param {quality} Number of points that comprise the approximated circle (20 is a good starting point)
+ */
+var Radius = mxn.Radius = function(center, quality) {
+ this.center = center;
+ var latConv = center.latConv();
+ var lonConv = center.lonConv();
+
+ // Create Radian conversion constant
+ var rad = Math.PI / 180;
+ this.calcs = [];
+
+ for(var i = 0; i < 360; i += quality){
+ this.calcs.push([Math.cos(i * rad) / latConv, Math.sin(i * rad) / lonConv]);
+ }
+};
+
+/**
+ * Returns polyline of a circle around the point based on new radius
+ * @param {Radius} radius
+ * @param {Colour} colour
+ * @returns {Polyline} Polyline
+ */
+Radius.prototype.getPolyline = function(radius, colour) {
+ var points = [];
+
+ for(var i = 0; i < this.calcs.length; i++){
+ var point = new LatLonPoint(
+ this.center.lat + (radius * this.calcs[i][0]),
+ this.center.lon + (radius * this.calcs[i][1])
+ );
+ points.push(point);
+ }
+
+ // Add first point
+ points.push(points[0]);
+
+ var line = new Polyline(points);
+ line.setColor(colour);
+
+ return line;
+};
+
+
+})(); \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.geocommons.core.js b/plugins/Mapstraction/js/mxn.geocommons.core.js
new file mode 100644
index 000000000..08be811fa
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.geocommons.core.js
@@ -0,0 +1,233 @@
+mxn.register('geocommons', {
+
+ Mapstraction: {
+
+ init: function(element, api) {
+ var me = this;
+ this.element = element;
+ Maker.maker_host='http://maker.geocommons.com';
+ Maker.finder_host='http://finder.geocommons.com';
+ Maker.core_host='http://geocommons.com';
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ resizeTo: function(width, height){
+ var map = this.maps[this.api];
+ map.setSize(width,height);
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+ map.showControl("Zoom", args.zoom || false);
+ map.showControl("Layers", args.layers || false);
+ map.showControl("Styles", args.styles || false);
+ map.showControl("Basemap", args.map_type || false);
+ map.showControl("Legend", args.legend || false, "open");
+ // showControl("Legend", true, "close");
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ showControl("Zoom", args.zoom);
+ showControl("Legend", args.legend, "open");
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ showControl("Zoom", args.zoom);
+ showControl("Layers", args.layers);
+ showControl("Legend", args.legend, "open");
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ dragging: function(on) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ map.setCenterZoom(point.lat, point.lon,zoom);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var point = map.getCenterZoom()[0];
+ return mxn.LatLonPoint(point.lat,point.lon);
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ map.setCenter(point.lat, point.lon);
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.setZoom(zoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return map.getZoom();
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // NE and SW points from the bounding box.
+ var ne = bbox.getNorthEast();
+ var sw = bbox.getSouthWest();
+ var zoom;
+
+ // TODO: Add provider code
+
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ map.setMapProvider("OpenStreetMap (road)");
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ map.setMapProvider("BlueMarble");
+ break;
+ case mxn.Mapstraction.HYBRID:
+ map.setMapProvider("Google Hybrid");
+ break;
+ default:
+ map.setMapProvider(type);
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ switch(map.getMapProvider) {
+ case "OpenStreetMap (Road)":
+ retu
+
+ }
+ // TODO: Add provider code
+
+ //return mxn.Mapstraction.ROAD;
+ //return mxn.Mapstraction.SATELLITE;
+ //return mxn.Mapstraction.HYBRID;
+
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ var extent = map.getExtent();
+ return new mxn.BoundingBox( extent.northWest.lat, extent.southEast.lon, extent.southEast.lat, extent.northWest.lon);
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+ map.setExtent(ne.lat,sw.lat,ne.lon,sw.lon);
+
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+ var me = this;
+ Maker.load_map(this.element.id, url);
+ setTimeout(function() { me.maps[me.api] = swfobject.getObjectById(FlashMap.dom_id);}, 500);
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ toggleTileLayer: function(tile_url) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ mousePosition: function(element) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ }
+ },
+
+ LatLonPoint: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ fromProprietary: function(googlePoint) {
+ // TODO: Add provider code
+ }
+
+ },
+
+ Marker: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ openBubble: function() {
+ // TODO: Add provider code
+ },
+
+ hide: function() {
+ // TODO: Add provider code
+ },
+
+ show: function() {
+ // TODO: Add provider code
+ },
+
+ update: function() {
+ // TODO: Add provider code
+ }
+
+ },
+
+ Polyline: {
+
+ toProprietary: function() {
+ // TODO: Add provider code
+ },
+
+ show: function() {
+ // TODO: Add provider code
+ },
+
+ hide: function() {
+ // TODO: Add provider code
+ }
+
+ }
+
+}); \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.google.core.js b/plugins/Mapstraction/js/mxn.google.core.js
new file mode 100644
index 000000000..17e4415f5
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.google.core.js
@@ -0,0 +1,519 @@
+mxn.register('google', {
+
+Mapstraction: {
+
+ init: function(element,api) {
+ var me = this;
+ if (GMap2) {
+ if (GBrowserIsCompatible()) {
+ this.maps[api] = new GMap2(element);
+
+ GEvent.addListener(this.maps[api], 'click', function(marker,location) {
+
+ if ( marker && marker.mapstraction_marker ) {
+ marker.mapstraction_marker.click.fire();
+ }
+ else if ( location ) {
+ me.click.fire({'location': new mxn.LatLonPoint(location.y, location.x)});
+ }
+
+ // If the user puts their own Google markers directly on the map
+ // then there is no location and this event should not fire.
+ if ( location ) {
+ me.clickHandler(location.y,location.x,location,me);
+ }
+ });
+
+ GEvent.addListener(this.maps[api], 'moveend', function() {
+ me.moveendHandler(me);
+ me.endPan.fire();
+ });
+
+ GEvent.addListener(this.maps[api], 'zoomend', function() {
+ me.changeZoom.fire();
+ });
+
+ this.loaded[api] = true;
+ me.load.fire();
+ }
+ else {
+ alert('browser not compatible with Google Maps');
+ }
+ }
+ else {
+ alert(api + ' map script not imported');
+ }
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+
+ if(this.options.enableScrollWheelZoom){
+ map.enableContinuousZoom();
+ map.enableScrollWheelZoom();
+ }
+
+ if (this.options.enableDragging) {
+ map.enableDragging();
+ } else {
+ map.disableDragging();
+ }
+
+ },
+
+ resizeTo: function(width, height){
+ this.currentElement.style.width = width;
+ this.currentElement.style.height = height;
+ this.maps[this.api].checkResize();
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+
+ // remove old controls
+ if (this.controls) {
+ while ((ctl = this.controls.pop())) {
+ // Google specific method
+ map.removeControl(ctl);
+ }
+ } else {
+ this.controls = [];
+ }
+ c = this.controls;
+
+ // Google has a combined zoom and pan control.
+ if (args.zoom || args.pan) {
+ if (args.zoom == 'large'){
+ this.addLargeControls();
+ } else {
+ this.addSmallControls();
+ }
+ }
+
+ if (args.scale) {
+ this.controls.unshift(new GScaleControl());
+ map.addControl(this.controls[0]);
+ this.addControlsArgs.scale = true;
+ }
+
+ if (args.overview) {
+ c.unshift(new GOverviewMapControl());
+ map.addControl(c[0]);
+ this.addControlsArgs.overview = true;
+ }
+ if (args.map_type) {
+ this.addMapTypeControls();
+ }
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ this.controls.unshift(new GSmallMapControl());
+ map.addControl(this.controls[0]);
+ this.addControlsArgs.zoom = 'small';
+ this.addControlsArgs.pan = true;
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ this.controls.unshift(new GLargeMapControl());
+ map.addControl(this.controls[0]);
+ this.addControlsArgs.zoom = 'large';
+ this.addControlsArgs.pan = true;
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+ this.controls.unshift(new GMapTypeControl());
+ map.addControl(this.controls[0]);
+ this.addControlsArgs.map_type = true;
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.setCenter(pt, zoom);
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ var gpin = marker.toProprietary(this.api);
+ map.addOverlay(gpin);
+
+ GEvent.addListener(gpin, 'infowindowopen', function() {
+ marker.openInfoBubble.fire();
+ });
+ GEvent.addListener(gpin, 'infowindowclose', function() {
+ marker.closeInfoBubble.fire();
+ });
+ return gpin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+ map.removeOverlay(marker.proprietary_marker);
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+ // FIXME: got a feeling this doesn't only delete markers
+ map.clearOverlays();
+ },
+
+ declutterMarkers: function(opts) {
+ throw 'Not implemented';
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ gpolyline = polyline.toProprietary(this.api);
+ map.addOverlay(gpolyline);
+ return gpolyline;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ map.removeOverlay(polyline.proprietary_polyline);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var pt = map.getCenter();
+ var point = new mxn.LatLonPoint(pt.lat(),pt.lng());
+ return point;
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ if(options && options.pan) {
+ map.panTo(pt);
+ }
+ else {
+ map.setCenter(pt);
+ }
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.setZoom(zoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return map.getZoom();
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // NE and SW points from the bounding box.
+ var ne = bbox.getNorthEast();
+ var sw = bbox.getSouthWest();
+ var gbox = new GLatLngBounds( sw.toProprietary(this.api), ne.toProprietary(this.api) );
+ var zoom = map.getBoundsZoomLevel( gbox );
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ map.setMapType(G_NORMAL_MAP);
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ map.setMapType(G_SATELLITE_MAP);
+ break;
+ case mxn.Mapstraction.HYBRID:
+ map.setMapType(G_HYBRID_MAP);
+ break;
+ default:
+ map.setMapType(type || G_NORMAL_MAP);
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ var type = map.getCurrentMapType();
+ switch(type) {
+ case G_NORMAL_MAP:
+ return mxn.Mapstraction.ROAD;
+ case G_SATELLITE_MAP:
+ return mxn.Mapstraction.SATELLITE;
+ case G_HYBRID_MAP:
+ return mxn.Mapstraction.HYBRID;
+ default:
+ return null;
+ }
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ var ne, sw, nw, se;
+ var gbox = map.getBounds();
+ sw = gbox.getSouthWest();
+ ne = gbox.getNorthEast();
+ return new mxn.BoundingBox(sw.lat(), sw.lng(), ne.lat(), ne.lng());
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+ var gbounds = new GLatLngBounds(new GLatLng(sw.lat,sw.lon),new GLatLng(ne.lat,ne.lon));
+ map.setCenter(gbounds.getCenter(), map.getBoundsZoomLevel(gbounds));
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+ map.getPane(G_MAP_MAP_PANE).appendChild(oContext.imgElm);
+ this.setImageOpacity(id, opacity);
+ this.setImagePosition(id);
+ GEvent.bind(map, "zoomend", this, function() {
+ this.setImagePosition(id);
+ });
+ GEvent.bind(map, "moveend", this, function() {
+ this.setImagePosition(id);
+ });
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ topLeftPoint = map.fromLatLngToDivPixel( new GLatLng(oContext.latLng.top, oContext.latLng.left) );
+ bottomRightPoint = map.fromLatLngToDivPixel( new GLatLng(oContext.latLng.bottom, oContext.latLng.right) );
+
+ oContext.pixels.top = topLeftPoint.y;
+ oContext.pixels.left = topLeftPoint.x;
+ oContext.pixels.bottom = bottomRightPoint.y;
+ oContext.pixels.right = bottomRightPoint.x;
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+ var geoXML = new GGeoXml(url);
+ map.addOverlay(geoXML, function() {
+ if(autoCenterAndZoom) {
+ geoXML.gotoDefaultViewport(map);
+ }
+ });
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
+ var copyright = new GCopyright(1, new GLatLngBounds(new GLatLng(-90,-180), new GLatLng(90,180)), 0, "copyleft");
+ var copyrightCollection = new GCopyrightCollection(copyright_text);
+ copyrightCollection.addCopyright(copyright);
+ var tilelayers = [];
+ tilelayers[0] = new GTileLayer(copyrightCollection, min_zoom, max_zoom);
+ tilelayers[0].isPng = function() {
+ return true;
+ };
+ tilelayers[0].getOpacity = function() {
+ return opacity;
+ };
+ tilelayers[0].getTileUrl = function (a, b) {
+ url = tile_url;
+ url = url.replace(/\{Z\}/g,b);
+ url = url.replace(/\{X\}/g,a.x);
+ url = url.replace(/\{Y\}/g,a.y);
+ return url;
+ };
+ if(map_type) {
+ var tileLayerOverlay = new GMapType(tilelayers, new GMercatorProjection(19), copyright_text, {
+ errorMessage:"More "+copyright_text+" tiles coming soon"
+ });
+ this.maps[this.api].addMapType(tileLayerOverlay);
+ } else {
+ tileLayerOverlay = new GTileLayerOverlay(tilelayers[0]);
+ this.maps[this.api].addOverlay(tileLayerOverlay);
+ }
+ this.tileLayers.push( [tile_url, tileLayerOverlay, true] );
+ return tileLayerOverlay;
+ },
+
+ toggleTileLayer: function(tile_url) {
+ for (var f=0; f<this.tileLayers.length; f++) {
+ if(this.tileLayers[f][0] == tile_url) {
+ if(this.tileLayers[f][2]) {
+ this.maps[this.api].removeOverlay(this.tileLayers[f][1]);
+ this.tileLayers[f][2] = false;
+ }
+ else {
+ this.maps[this.api].addOverlay(this.tileLayers[f][1]);
+ this.tileLayers[f][2] = true;
+ }
+ }
+ }
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ var projection = G_NORMAL_MAP.getProjection();
+ var centerPoint = map.getCenter();
+ var zoom = map.getZoom();
+ var centerPixel = projection.fromLatLngToPixel(centerPoint, zoom);
+ // distance is the distance in metres for 5 pixels (3-4-5 triangle)
+ var distancePoint = projection.fromPixelToLatLng(new GPoint(centerPixel.x + 3, centerPixel.y + 4), zoom);
+ //*1000(km to m), /5 (pythag), *2 (radius to diameter)
+ return 10000/distancePoint.distanceFrom(centerPoint);
+
+ },
+
+ mousePosition: function(element) {
+ var locDisp = document.getElementById(element);
+ if (locDisp !== null) {
+ var map = this.maps[this.api];
+ GEvent.addListener(map, 'mousemove', function (point) {
+ var loc = point.lat().toFixed(4) + ' / ' + point.lng().toFixed(4);
+ locDisp.innerHTML = loc;
+ });
+ locDisp.innerHTML = '0.0000 / 0.0000';
+ }
+ }
+},
+
+LatLonPoint: {
+
+ toProprietary: function() {
+ return new GLatLng(this.lat,this.lon);
+ },
+
+ fromProprietary: function(googlePoint) {
+ this.lat = googlePoint.lat();
+ this.lon = googlePoint.lng();
+ }
+
+},
+
+Marker: {
+
+ toProprietary: function() {
+ var infoBubble, event_action, infoDiv, div;
+ var options = {};
+ if(this.labelText){
+ options.title = this.labelText;
+ }
+ if(this.iconUrl){
+ var icon = new GIcon(G_DEFAULT_ICON, this.iconUrl);
+ icon.printImage = icon.mozPrintImage = icon.image;
+ if(this.iconSize) {
+ icon.iconSize = new GSize(this.iconSize[0], this.iconSize[1]);
+ var anchor;
+ if(this.iconAnchor) {
+ anchor = new GPoint(this.iconAnchor[0], this.iconAnchor[1]);
+ }
+ else {
+ // FIXME: hard-coding the anchor point
+ anchor = new GPoint(this.iconSize[0]/2, this.iconSize[1]/2);
+ }
+ icon.iconAnchor = anchor;
+ }
+ if(typeof(this.iconShadowUrl) != 'undefined') {
+ icon.shadow = this.iconShadowUrl;
+ if(this.iconShadowSize) {
+ icon.shadowSize = new GSize(this.iconShadowSize[0], this.iconShadowSize[1]);
+ }
+ } else { // turn off shadow
+ icon.shadow = '';
+ icon.shadowSize = '';
+ }
+ if(this.transparent) {
+ icon.transparent = this.transparent;
+ }
+ if(this.imageMap) {
+ icon.imageMap = this.imageMap;
+ }
+ options.icon = icon;
+ }
+ if(this.draggable){
+ options.draggable = this.draggable;
+ }
+ var gmarker = new GMarker( this.location.toProprietary('google'),options);
+
+ if(this.infoBubble){
+ infoBubble = this.infoBubble;
+ if(this.hover) {
+ event_action = "mouseover";
+ }
+ else {
+ event_action = "click";
+ }
+ GEvent.addListener(gmarker, event_action, function() {
+ gmarker.openInfoWindowHtml(infoBubble, {
+ maxWidth: 100
+ });
+ });
+ }
+
+ if(this.hoverIconUrl){
+ GEvent.addListener(gmarker, "mouseover", function() {
+ gmarker.setImage(this.hoverIconUrl);
+ });
+ GEvent.addListener(gmarker, "mouseout", function() {
+ gmarker.setImage(this.iconUrl);
+ });
+ }
+
+ if(this.infoDiv){
+ infoDiv = this.infoDiv;
+ div = this.div;
+ if(this.hover) {
+ event_action = "mouseover";
+ }
+ else {
+ event_action = "click";
+ }
+ GEvent.addListener(gmarker, event_action, function() {
+ document.getElementById(div).innerHTML = infoDiv;
+ });
+ }
+
+ return gmarker;
+ },
+
+ openBubble: function() {
+ var gpin = this.proprietary_marker;
+ gpin.openInfoWindowHtml(this.infoBubble);
+ },
+
+ hide: function() {
+ this.proprietary_marker.hide();
+ },
+
+ show: function() {
+ this.proprietary_marker.show();
+ },
+
+ update: function() {
+ point = new mxn.LatLonPoint();
+ point.fromGoogle(this.proprietary_marker.getPoint());
+ this.location = point;
+ }
+
+},
+
+Polyline: {
+
+ toProprietary: function() {
+ var gpoints = [];
+ for (var i = 0, length = this.points.length ; i< length; i++){
+ gpoints.push(this.points[i].toProprietary('google'));
+ }
+ if (this.closed || gpoints[0].equals(gpoints[length-1])) {
+ return new GPolygon(gpoints, this.color, this.width, this.opacity, this.fillColor || "#5462E3", this.opacity || "0.3");
+ } else {
+ return new GPolyline(gpoints, this.color, this.width, this.opacity);
+ }
+ },
+
+ show: function() {
+ throw 'Not implemented';
+ },
+
+ hide: function() {
+ throw 'Not implemented';
+ }
+}
+
+});
diff --git a/plugins/Mapstraction/js/mxn.google.geocoder.js b/plugins/Mapstraction/js/mxn.google.geocoder.js
new file mode 100644
index 000000000..5119c00a1
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.google.geocoder.js
@@ -0,0 +1,179 @@
+/*
+ Copyright (c) 2007, Andrew Turner
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ * Neither the name of the Mapstraction nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+// Use http://jsdoc.sourceforge.net/ to generate documentation
+
+// TODO: add reverse geocoding support
+
+/**
+ * MapstractionGeocoder instantiates a geocoder with some API choice
+ * @param {Function} callback The function to call when a geocode request returns (function(waypoint))
+ * @param {String} api The API to use, currently only 'mapquest' is supported
+ * @param {Function} error_callback The optional function to call when a geocode request fails
+ * @constructor
+ */
+function MapstractionGeocoder(callback, api, error_callback) {
+ this.api = api;
+ this.callback = callback;
+ this.geocoders = new Object();
+ if(error_callback == null) {
+ this.error_callback = this.geocode_error
+ } else {
+ this.error_callback = error_callback;
+ }
+
+ // This is so that it is easy to tell which revision of this file
+ // has been copied into other projects.
+ this.svn_revision_string = '$Revision: 107 $';
+
+ this.addAPI(api);
+
+}
+
+
+/**
+ * Internal function to actually set the router specific parameters
+ */
+MapstractionGeocoder.prototype.addAPI = function(api) {
+
+ me = this;
+ switch (api) {
+ case 'google':
+ this.geocoders[api] = new GClientGeocoder();
+ break;
+ case 'mapquest':
+ //set up the connection to the geocode server
+ var proxyServerName = "";
+ var proxyServerPort = "";
+ var ProxyServerPath = "mapquest_proxy/JSReqHandler.php";
+
+ var serverName = "geocode.access.mapquest.com";
+ var serverPort = "80";
+ var serverPath = "mq";
+ this.geocoders[api] = new MQExec(serverName, serverPath, serverPort, proxyServerName,
+ ProxyServerPath, proxyServerPort );
+
+ break;
+ default:
+ alert(api + ' not supported by mapstraction-geocoder');
+ }
+}
+/**
+ * Change the Routing API to use
+ * @param {String} api The API to swap to
+ */
+MapstractionGeocoder.prototype.swap = function(api) {
+ if (this.api == api) { return; }
+
+ this.api = api;
+ if (this.geocoders[this.api] == undefined) {
+ this.addAPI($(element),api);
+ }
+}
+
+/**
+ * Default Geocode error function
+ */
+MapstractionGeocoder.prototype.geocode_error = function(response) {
+ alert("Sorry, we were unable to geocode that address");
+}
+
+/**
+ * Default handler for geocode request completion
+ */
+MapstractionGeocoder.prototype.geocode_callback = function(response, mapstraction_geocoder) {
+ var return_location = new Object();
+
+ // TODO: what if the api is switched during a geocode request?
+ // TODO: provide an option error callback
+ switch (mapstraction_geocoder.api) {
+ case 'google':
+ if (!response || response.Status.code != 200) {
+ mapstraction_geocoder.error_callback(response);
+ } else {
+ return_location.street = "";
+ return_location.locality = "";
+ return_location.region = "";
+ return_location.country = "";
+
+ var place = response.Placemark[0];
+ if(place.AddressDetails.Country.AdministrativeArea != null) {
+ return_location.region = place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName;
+
+ if(place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea != null) {
+ if(place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality != null) {
+ return_location.locality = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName;
+
+ if(place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare != null)
+ return_location.street = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName;
+ }
+
+ }
+
+ }
+ return_location.country = place.AddressDetails.Country.CountryNameCode;
+ return_location.address = place.address;
+
+ return_location.point = new mxn.LatLonPoint(place.Point.coordinates[1],
+ place.Point.coordinates[0]);
+ mapstraction_geocoder.callback(return_location);
+ }
+ break;
+ case 'mapquest':
+ break;
+ }
+}
+
+
+/**
+ * Performs a geocoding and then calls the specified callback function with the location
+ * @param {Object} address The address object to geocode
+ */
+ MapstractionGeocoder.prototype.geocode = function(address) {
+ var return_location = new Object();
+
+ // temporary variable for later using in function closure
+ var mapstraction_geocoder = this;
+
+ switch (this.api) {
+ case 'google':
+ if (address.address == null || address.address == "")
+ address.address = address.street + ", " + address.locality + ", " + address.region + ", " + address.country
+ this.geocoders[this.api].getLocations(address.address, function(response) { mapstraction_geocoder.geocode_callback(response, mapstraction_geocoder); });
+ break;
+ case 'mapquest':
+ var mqaddress = new MQAddress();
+ var gaCollection = new MQLocationCollection("MQGeoAddress");
+ //populate the address object with the information from the form
+ mqaddress.setStreet(address.street);
+ mqaddress.setCity(address.locality);
+ mqaddress.setState(address.region);
+ mqaddress.setPostalCode(address.postalcode);
+ mqaddress.setCountry(address.country);
+
+ this.geocoders[this.api].geocode(mqaddress, gaCollection);
+ var geoAddr = gaCollection.get(0);
+ var mqpoint = geoAddr.getMQLatLng();
+ return_location.street = geoAddr.getStreet();
+ return_location.locality = geoAddr.getCity();
+ return_location.region = geoAddr.getState();
+ return_location.country = geoAddr.getCountry();
+ return_location.point = new mxn.LatLonPoint(mqpoint.getLatitude(), mqpoint.getLongitude());
+ this.callback(return_location, this);
+ break;
+ default:
+ alert(api + ' not supported by mapstraction-geocoder');
+ break;
+ }
+ }
diff --git a/plugins/Mapstraction/js/mxn.googlev3.core.js b/plugins/Mapstraction/js/mxn.googlev3.core.js
new file mode 100644
index 000000000..7ca07ec7f
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.googlev3.core.js
@@ -0,0 +1,443 @@
+mxn.register('googlev3', {
+
+Mapstraction: {
+
+ init: function(element, api){
+ var me = this;
+ if ( google && google.maps ){
+ // by default no controls and road map
+ var myOptions = {
+ disableDefaultUI: true,
+ mapTypeId: google.maps.MapTypeId.ROADMAP
+ };
+ var map = new google.maps.Map(element, myOptions);
+
+ // deal with click
+ google.maps.event.addListener(map, 'click', function(location){
+ me.clickHandler(location.latLng.lat(),location.latLng.lng(),location,me);
+ });
+
+ // deal with zoom change
+ google.maps.event.addListener(map, 'zoom_changed', function(){
+ me.changeZoom.fire();
+ });
+ // deal with map movement
+ google.maps.event.addListener(map, 'dragend', function(){
+ me.moveendHandler(me);
+ me.endPan.fire();
+ });
+ this.maps[api] = map;
+ this.loaded[api] = true;
+ me.load.fire();
+ }
+ else {
+ alert(api + ' map script not imported');
+ }
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+ var myOptions = [];
+ if (this.options.enableDragging) {
+ myOptions.draggable = true;
+ }
+ if (this.options.enableScrollWheelZoom){
+ myOptions.scrollwheel = true;
+ }
+ map.setOptions(myOptions);
+ },
+
+ resizeTo: function(width, height){
+ this.currentElement.style.width = width;
+ this.currentElement.style.height = height;
+ var map = this.maps[this.api];
+ google.maps.event.trigger(map,'resize');
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+ // remove old controls
+
+ // Google has a combined zoom and pan control.
+ if (args.zoom || args.pan) {
+ if (args.zoom == 'large'){
+ this.addLargeControls();
+ } else {
+ this.addSmallControls();
+ }
+ }
+ if (args.scale){
+ var myOptions = {
+ scaleControl:true,
+ scaleControlOptions: {style:google.maps.ScaleControlStyle.DEFAULT}
+ };
+ map.setOptions(myOptions);
+ this.addControlsArgs.scale = true;
+ }
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ var myOptions = {
+ navigationControl: true,
+ navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL}
+ };
+ map.setOptions(myOptions);
+
+ this.addControlsArgs.pan = false;
+ this.addControlsArgs.scale = false;
+ this.addControlsArgs.zoom = 'small';
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ var myOptions = {
+ navigationControl:true,
+ navigationControlOptions: {style:google.maps.NavigationControlStyle.DEFAULT}
+ };
+ map.setOptions(myOptions);
+ this.addControlsArgs.pan = true;
+ this.addControlsArgs.zoom = 'large';
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+ var myOptions = {
+ mapTypeControl: true,
+ mapTypeControlOptions: {style: google.maps.MapTypeControlStyle.DEFAULT}
+ };
+ map.setOptions(myOptions);
+ this.addControlsArgs.map_type = true;
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.setCenter(pt);
+ map.setZoom(zoom);
+ },
+
+ addMarker: function(marker, old) {
+ return marker.toProprietary(this.api);
+ },
+
+ removeMarker: function(marker) {
+ // doesn't really remove them, just hides them
+ marker.hide();
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+ // TODO: Add provider code
+ },
+
+ declutterMarkers: function(opts) {
+ var map = this.maps[this.api];
+ // TODO: Add provider code
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ return polyline.toProprietary(this.api);
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ // TODO: Add provider code
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var pt = map.getCenter();
+ return new mxn.LatLonPoint(pt.lat(),pt.lng());
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ if(options && options.pan) {
+ map.panTo(pt);
+ }
+ else {
+ map.setCenter(pt);
+ }
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.setZoom(zoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return map.getZoom();
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ var sw = bbox.getSouthWest().toProprietary(this.api);
+ var ne = bbox.getNorthEast().toProprietary(this.api);
+ var gLatLngBounds = new google.maps.LatLngBounds(sw, ne);
+ map.fitBounds(gLatLngBounds);
+ return map.getZoom();
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ map.setMapTypeId(google.maps.MapTypeId.SATELLITE);
+ break;
+ case mxn.Mapstraction.HYBRID:
+ map.setMapTypeId(google.maps.MapTypeId.HYBRID);
+ break;
+ default:
+ map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ var type = map.getMapTypeId();
+ switch(type) {
+ case google.maps.MapTypeId.ROADMAP:
+ return mxn.Mapstraction.ROAD;
+ case google.maps.MapTypeId.SATELLITE:
+ return mxn.Mapstraction.SATELLITE;
+ case google.maps.MapTypeId.HYBRID:
+ return mxn.Mapstraction.HYBRID;
+ //case google.maps.MapTypeId.TERRAIN:
+ // return something;
+ default:
+ return null;
+ }
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ var gLatLngBounds = map.getBounds();
+ var sw = gLatLngBounds.getSouthWest();
+ var ne = gLatLngBounds.getNorthEast();
+ return new mxn.BoundingBox(sw.lat(), sw.lng(), ne.lat(), ne.lng());
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest().toProprietary(this.api);
+ var ne = bounds.getNorthEast().toProprietary(this.api);
+ var gLatLngBounds = new google.maps.LatLngBounds(sw, ne);
+ map.fitBounds(gLatLngBounds);
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ // TODO: Add provider code
+
+ //oContext.pixels.top = ...;
+ //oContext.pixels.left = ...;
+ //oContext.pixels.bottom = ...;
+ //oContext.pixels.right = ...;
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ toggleTileLayer: function(tile_url) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ mousePosition: function(element) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ }
+},
+
+LatLonPoint: {
+
+ toProprietary: function() {
+ return new google.maps.LatLng(this.lat, this.lon);
+ },
+
+ fromProprietary: function(googlePoint) {
+ this.lat = googlePoint.lat();
+ this.lon = googlePoint.lng();
+ }
+
+},
+
+Marker: {
+
+ toProprietary: function() {
+ var options = {};
+
+ // do we have an Anchor?
+ var ax = 0; // anchor x
+ var ay = 0; // anchor y
+
+ if (this.iconAnchor) {
+ ax = this.iconAnchor[0];
+ ay = this.iconAnchor[1];
+ }
+ var gAnchorPoint = new google.maps.Point(ax,ay);
+
+ if (this.iconUrl) {
+ options.icon = new google.maps.MarkerImage(
+ this.iconUrl,
+ new google.maps.Size(this.iconSize[0],
+ this.iconSize[1]),
+ new google.maps.Point(0,0),
+ gAnchorPoint
+ );
+
+ // do we have a Shadow?
+ if (this.iconShadowUrl) {
+ if (this.iconShadowSize) {
+ var x = this.iconShadowSize[0];
+ var y = this.iconShadowSize[1];
+ options.shadow = new google.maps.MarkerImage(
+ this.iconShadowUrl,
+ new google.maps.Size(x,y),
+ new google.maps.Point(0,0),
+ gAnchorPoint
+ );
+ }
+ else {
+ options.shadow = new google.maps.MarkerImage(this.iconShadowUrl);
+ }
+ }
+ }
+ if (this.draggable){
+ options.draggable = this.draggable;
+ }
+ if (this.labelText){
+ options.title = this.labelText;
+ }
+ if (this.imageMap){
+ options.shape = {
+ coord: this.imageMap,
+ type: 'poly'
+ };
+ }
+
+ options.position = this.location.toProprietary(this.api);
+ options.map = this.map;
+
+ var marker = new google.maps.Marker(options);
+
+ if (this.infoBubble){
+ var infowindow = new google.maps.InfoWindow({
+ content: this.infoBubble
+ });
+
+ var event_action = "click";
+ if (this.hover) {
+ event_action = "mouseover";
+ }
+ google.maps.event.addListener(marker, event_action, function() { infowindow.open(this.map,marker); });
+ }
+
+ if (this.hoverIconUrl){
+ var gSize = new google.maps.Size(this.iconSize[0],
+ this.iconSize[1]);
+ var zerozero = new google.maps.Point(0,0);
+ var hIcon = new google.maps.MarkerImage(
+ this.hoverIconUrl,
+ gSize,
+ zerozero,
+ gAnchorPoint
+ );
+ var Icon = new google.maps.MarkerImage(
+ this.iconUrl,
+ gSize,
+ zerozero,
+ gAnchorPoint
+ );
+ google.maps.event.addListener(
+ marker,
+ "mouseover",
+ function(){
+ marker.setIcon(hIcon);
+ }
+ );
+ google.maps.event.addListener(
+ marker,
+ "mouseout",
+ function(){ marker.setIcon(Icon); }
+ );
+ }
+
+ google.maps.event.addListener(marker, 'click', function() {
+ marker.mapstraction_marker.click.fire();
+ });
+
+ return marker;
+ },
+
+ openBubble: function() {
+ var infowindow = new google.maps.InfoWindow({
+ content: this.infoBubble
+ });
+ infowindow.open(this.map,this.proprietary_marker);
+ },
+
+ hide: function() {
+ this.proprietary_marker.setOptions({visible:false});
+ },
+
+ show: function() {
+ this.proprietary_marker.setOptions({visible:true});
+ },
+
+ update: function() {
+ // TODO: Add provider code
+ }
+
+},
+
+Polyline: {
+
+ toProprietary: function() {
+ throw 'Not implemented';
+ },
+
+ show: function() {
+ throw 'Not implemented';
+ },
+
+ hide: function() {
+ throw 'Not implemented';
+ }
+
+}
+
+});
diff --git a/plugins/Mapstraction/js/mxn.js b/plugins/Mapstraction/js/mxn.js
new file mode 100644
index 000000000..7ade41d51
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.js
@@ -0,0 +1,505 @@
+// Auto-load scripts
+//
+// specify which map providers to load by using
+// <script src="mxn.js?(provider1,provider2,[module1,module2])" ...
+// in your HTML
+//
+// for each provider mxn.provider.module.js and mxn.module.js will be loaded
+// module 'core' is always loaded
+//
+// NOTE: if you call without providers
+// <script src="mxn.js" ...
+// no scripts will be loaded at all and it is then up to you to load the scripts independently
+(function() {
+ var providers = null;
+ var modules = 'core';
+ var scriptBase;
+ var scripts = document.getElementsByTagName('script');
+
+ // Determine which scripts we need to load
+ for (var i = 0; i < scripts.length; i++) {
+ var match = scripts[i].src.replace(/%20/g , '').match(/^(.*?)mxn\.js(\?\(\[?(.*?)\]?\))?$/);
+ if (match != null) {
+ scriptBase = match[1];
+ if (match[3]) {
+ var settings = match[3].split(',[');
+ providers = settings[0].replace(']' , '');
+ if (settings[1]) modules += ',' + settings[1];
+ }
+ break;
+ }
+ }
+
+ if (providers == null || providers == 'none') return; // Bail out if no auto-load has been found
+ providers = providers.replace(/ /g, '').split(',');
+ modules = modules.replace(/ /g, '').split(',');
+
+ // Actually load the scripts
+ for (i = 0; i < modules.length; i++) {
+ document.write("<script type='text/javascript' src='" + scriptBase + 'mxn.' + modules[i] + '.js' + "'></script>");
+ for (var j = 0; j < providers.length; j++) document.write("<script type='text/javascript' src='" + scriptBase + 'mxn.' + providers[j] + '.' + modules[i] + '.js' + "'></script>");
+ }
+})();
+
+(function(){
+
+// holds all our implementing functions
+var apis = {};
+
+// Our special private methods
+/**
+ * Calls the API specific implementation of a particular method.
+ * @private
+ */
+var invoke = function(sApiId, sObjName, sFnName, oScope, args){
+ if(!hasImplementation(sApiId, sObjName, sFnName)) {
+ throw 'Method ' + sFnName + ' of object ' + sObjName + ' is not supported by API ' + sApiId + '. Are you missing a script tag?';
+ }
+ return apis[sApiId][sObjName][sFnName].apply(oScope, args);
+};
+
+/**
+ * Determines whether the specified API provides an implementation for the
+ * specified object and function name.
+ * @private
+ */
+var hasImplementation = function(sApiId, sObjName, sFnName){
+ if(typeof(apis[sApiId]) == 'undefined') {
+ throw 'API ' + sApiId + ' not loaded. Are you missing a script tag?';
+ }
+ if(typeof(apis[sApiId][sObjName]) == 'undefined') {
+ throw 'Object definition ' + sObjName + ' in API ' + sApiId + ' not loaded. Are you missing a script tag?';
+ }
+ return typeof(apis[sApiId][sObjName][sFnName]) == 'function';
+};
+
+/**
+ * @name mxn
+ * @namespace
+ */
+var mxn = window.mxn = /** @lends mxn */ {
+
+ /**
+ * Registers a set of provider specific implementation functions.
+ * @function
+ * @param {String} sApiId The API ID to register implementing functions for.
+ * @param {Object} oApiImpl An object containing the API implementation.
+ */
+ register: function(sApiId, oApiImpl){
+ if(!apis.hasOwnProperty(sApiId)){
+ apis[sApiId] = {};
+ }
+ mxn.util.merge(apis[sApiId], oApiImpl);
+ },
+
+ /**
+ * Adds a list of named proxy methods to the prototype of a
+ * specified constructor function.
+ * @function
+ * @param {Function} func Constructor function to add methods to
+ * @param {Array} aryMethods Array of method names to create
+ * @param {Boolean} bWithApiArg Optional. Whether the proxy methods will use an API argument
+ */
+ addProxyMethods: function(func, aryMethods, bWithApiArg){
+ for(var i = 0; i < aryMethods.length; i++) {
+ var sMethodName = aryMethods[i];
+ if(bWithApiArg){
+ func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments, { overrideApi: true } );');
+ }
+ else {
+ func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments);');
+ }
+ }
+ },
+
+ /*
+ checkLoad: function(funcDetails){
+ if(this.loaded[this.api] === false) {
+ var scope = this;
+ this.onload[this.api].push( function() { funcDetails.callee.apply(scope, funcDetails); } );
+ return true;
+ }
+ return false;
+ },
+ */
+
+ /**
+ * Bulk add some named events to an object.
+ * @function
+ * @param {Object} oEvtSrc The event source object.
+ * @param {String[]} aEvtNames Event names to add.
+ */
+ addEvents: function(oEvtSrc, aEvtNames){
+ for(var i = 0; i < aEvtNames.length; i++){
+ var sEvtName = aEvtNames[i];
+ if(sEvtName in oEvtSrc){
+ throw 'Event or method ' + sEvtName + ' already declared.';
+ }
+ oEvtSrc[sEvtName] = new mxn.Event(sEvtName, oEvtSrc);
+ }
+ }
+
+};
+
+/**
+ * Instantiates a new Event
+ * @constructor
+ * @param {String} sEvtName The name of the event.
+ * @param {Object} oEvtSource The source object of the event.
+ */
+mxn.Event = function(sEvtName, oEvtSource){
+ var handlers = [];
+ if(!sEvtName){
+ throw 'Event name must be provided';
+ }
+ /**
+ * Add a handler to the Event.
+ * @param {Function} fn The handler function.
+ * @param {Object} ctx The context of the handler function.
+ */
+ this.addHandler = function(fn, ctx){
+ handlers.push({context: ctx, handler: fn});
+ };
+ /**
+ * Remove a handler from the Event.
+ * @param {Function} fn The handler function.
+ * @param {Object} ctx The context of the handler function.
+ */
+ this.removeHandler = function(fn, ctx){
+ for(var i = 0; i < handlers.length; i++){
+ if(handlers[i].handler == fn && handlers[i].context == ctx){
+ handlers.splice(i, 1);
+ }
+ }
+ };
+ /**
+ * Remove all handlers from the Event.
+ */
+ this.removeAllHandlers = function(){
+ handlers = [];
+ };
+ /**
+ * Fires the Event.
+ * @param {Object} oEvtArgs Event arguments object to be passed to the handlers.
+ */
+ this.fire = function(oEvtArgs){
+ var args = [sEvtName, oEvtSource, oEvtArgs];
+ for(var i = 0; i < handlers.length; i++){
+ handlers[i].handler.apply(handlers[i].context, args);
+ }
+ };
+};
+
+/**
+ * Creates a new Invoker, a class which helps with on-the-fly
+ * invocation of the correct API methods.
+ * @constructor
+ * @param {Object} aobj The core object whose methods will make cals to go()
+ * @param {String} asClassName The name of the Mapstraction class to be invoked, normally the same name as aobj's constructor function
+ * @param {Function} afnApiIdGetter The function on object aobj which will return the active API ID
+ */
+mxn.Invoker = function(aobj, asClassName, afnApiIdGetter){
+ var obj = aobj;
+ var sClassName = asClassName;
+ var fnApiIdGetter = afnApiIdGetter;
+ var defOpts = {
+ overrideApi: false, // {Boolean} API ID is overridden by value in first argument
+ context: null, // {Object} Local vars can be passed from the body of the method to the API method within this object
+ fallback: null // {Function} If an API implementation doesn't exist this function is run instead
+ };
+
+ /**
+ * Invoke the API implementation of a specific method.
+ * @param {String} sMethodName The method name to invoke
+ * @param {Array} args Arguments to pass on
+ * @param {Object} oOptions Optional. Extra options for invocation
+ * @param {Boolean} oOptions.overrideApi When true the first argument is used as the API ID.
+ * @param {Object} oOptions.context A context object for passing extra information on to the provider implementation.
+ * @param {Function} oOptions.fallback A fallback function to run if the provider implementation is missing.
+ */
+ this.go = function(sMethodName, args, oOptions){
+
+ if(typeof(oOptions) == 'undefined'){
+ oOptions = defOpts;
+ }
+
+ var sApiId = oOptions.overrideApi ? args[0] : fnApiIdGetter.apply(obj);
+
+ if(typeof(sApiId) != 'string'){
+ throw 'API ID not available.';
+ }
+
+ if(typeof(oOptions.context) != 'undefined' && oOptions.context !== null){
+ // make sure args is an array
+ args = Array.prototype.slice.apply(args);
+ args.push(oOptions.context);
+ }
+
+ if(typeof(oOptions.fallback) == 'function' && !hasImplementation(sApiId, sClassName, sMethodName)){
+ // we've got no implementation but have got a fallback function
+ return oOptions.fallback.apply(obj, args);
+ }
+ else {
+ return invoke(sApiId, sClassName, sMethodName, obj, args);
+ }
+
+ };
+
+};
+
+/**
+ * @namespace
+ */
+mxn.util = {
+
+ /**
+ * Merges properties of one object into another recursively.
+ * @param {Object} oRecv The object receiveing properties
+ * @param {Object} oGive The object donating properties
+ */
+ merge: function(oRecv, oGive){
+ for (var sPropName in oGive){
+ if (oGive.hasOwnProperty(sPropName)) {
+ if(!oRecv.hasOwnProperty(sPropName)){
+ oRecv[sPropName] = oGive[sPropName];
+ }
+ else {
+ mxn.util.merge(oRecv[sPropName], oGive[sPropName]);
+ }
+ }
+ }
+ },
+
+ /**
+ * $m, the dollar function, elegantising getElementById()
+ * @return An HTML element or array of HTML elements
+ */
+ $m: function() {
+ var elements = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var element = arguments[i];
+ if (typeof(element) == 'string') {
+ element = document.getElementById(element);
+ }
+ if (arguments.length == 1) {
+ return element;
+ }
+ elements.push(element);
+ }
+ return elements;
+ },
+
+ /**
+ * loadScript is a JSON data fetcher
+ * @param {String} src URL to JSON file
+ * @param {Function} callback Callback function
+ */
+ loadScript: function(src, callback) {
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = src;
+ if (callback) {
+ if(script.addEventListener){
+ script.addEventListener('load', callback, true);
+ }
+ else if(script.attachEvent){
+ var done = false;
+ script.attachEvent("onreadystatechange",function(){
+ if ( !done && document.readyState === "complete" ) {
+ done = true;
+ callback();
+ }
+ });
+ }
+ }
+ var h = document.getElementsByTagName('head')[0];
+ h.appendChild( script );
+ return;
+ },
+
+ /**
+ *
+ * @param {Object} point
+ * @param {Object} level
+ */
+ convertLatLonXY_Yahoo: function(point, level) { //Mercator
+ var size = 1 << (26 - level);
+ var pixel_per_degree = size / 360.0;
+ var pixel_per_radian = size / (2 * Math.PI);
+ var origin = new YCoordPoint(size / 2 , size / 2);
+ var answer = new YCoordPoint();
+ answer.x = Math.floor(origin.x + point.lon * pixel_per_degree);
+ var sin = Math.sin(point.lat * Math.PI / 180.0);
+ answer.y = Math.floor(origin.y + 0.5 * Math.log((1 + sin) / (1 - sin)) * -pixel_per_radian);
+ return answer;
+ },
+
+ /**
+ * Load a stylesheet from a remote file.
+ * @param {String} href URL to the CSS file
+ */
+ loadStyle: function(href) {
+ var link = document.createElement('link');
+ link.type = 'text/css';
+ link.rel = 'stylesheet';
+ link.href = href;
+ document.getElementsByTagName('head')[0].appendChild(link);
+ return;
+ },
+
+ /**
+ * getStyle provides cross-browser access to css
+ * @param {Object} el HTML Element
+ * @param {String} prop Style property name
+ */
+ getStyle: function(el, prop) {
+ var y;
+ if (el.currentStyle) {
+ y = el.currentStyle[prop];
+ }
+ else if (window.getComputedStyle) {
+ y = window.getComputedStyle( el, '').getPropertyValue(prop);
+ }
+ return y;
+ },
+
+ /**
+ * Convert longitude to metres
+ * http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM
+ * "A degree of longitude at the equator is 111.2km... For other latitudes,
+ * multiply by cos(lat)"
+ * assumes the earth is a sphere but good enough for our purposes
+ * @param {Float} lon
+ * @param {Float} lat
+ */
+ lonToMetres: function(lon, lat) {
+ return lon * (111200 * Math.cos(lat * (Math.PI / 180)));
+ },
+
+ /**
+ * Convert metres to longitude
+ * @param {Object} m
+ * @param {Object} lat
+ */
+ metresToLon: function(m, lat) {
+ return m / (111200 * Math.cos(lat * (Math.PI / 180)));
+ },
+
+ /**
+ * Convert kilometres to miles
+ * @param {Float} km
+ * @returns {Float} miles
+ */
+ KMToMiles: function(km) {
+ return km / 1.609344;
+ },
+
+ /**
+ * Convert miles to kilometres
+ * @param {Float} miles
+ * @returns {Float} km
+ */
+ milesToKM: function(miles) {
+ return miles * 1.609344;
+ },
+
+ // stuff to convert google zoom levels to/from degrees
+ // assumes zoom 0 = 256 pixels = 360 degrees
+ // zoom 1 = 256 pixels = 180 degrees
+ // etc.
+
+ /**
+ *
+ * @param {Object} pixels
+ * @param {Object} zoom
+ */
+ getDegreesFromGoogleZoomLevel: function(pixels, zoom) {
+ return (360 * pixels) / (Math.pow(2, zoom + 8));
+ },
+
+ /**
+ *
+ * @param {Object} pixels
+ * @param {Object} degrees
+ */
+ getGoogleZoomLevelFromDegrees: function(pixels, degrees) {
+ return mxn.util.logN((360 * pixels) / degrees, 2) - 8;
+ },
+
+ /**
+ *
+ * @param {Object} number
+ * @param {Object} base
+ */
+ logN: function(number, base) {
+ return Math.log(number) / Math.log(base);
+ },
+
+ /**
+ * Returns array of loaded provider apis
+ * @returns {Array} providers
+ */
+ getAvailableProviders : function () {
+ var providers = [];
+ for (var propertyName in apis){
+ if (apis.hasOwnProperty(propertyName)) {
+ providers.push(propertyName);
+ }
+ }
+ return providers;
+ }
+
+};
+
+/**
+ * Class for converting between HTML and RGB integer color formats.
+ * Accepts either a HTML color string argument or three integers for R, G and B.
+ * @constructor
+ */
+mxn.util.Color = function() {
+ if(arguments.length == 3) {
+ this.red = arguments[0];
+ this.green = arguments[1];
+ this.blue = arguments[2];
+ }
+ else if(arguments.length == 1) {
+ this.setHexColor(arguments[0]);
+ }
+};
+
+mxn.util.Color.prototype.reHex = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
+
+/**
+ * Set the color from the supplied HTML hex string.
+ * @param {String} strHexColor A HTML hex color string e.g. '#00FF88'.
+ */
+mxn.util.Color.prototype.setHexColor = function(strHexColor) {
+ var match = strHexColor.match(this.reHex);
+ if(match) {
+ strHexColor = match[1];
+ }
+ else {
+ throw 'Invalid HEX color format, expected #000, 000, #000000 or 000000';
+ }
+ if(strHexColor.length == 3) {
+ strHexColor = strHexColor.replace(/\w/g, function(str){return str.concat(str);});
+ }
+ this.red = parseInt(strHexColor.substr(0,2), 16);
+ this.green = parseInt(strHexColor.substr(2,2), 16);
+ this.blue = parseInt(strHexColor.substr(4,2), 16);
+};
+
+/**
+ * Retrieve the color value as an HTML hex string.
+ * @returns {String} Format '00FF88' - note no preceding #.
+ */
+mxn.util.Color.prototype.getHexColor = function() {
+ var vals = [this.red.toString(16), this.green.toString(16), this.blue.toString(16)];
+ for(var i = 0; i < vals.length; i++) {
+ vals[i] = (vals[i].length == 1) ? '0' + vals[i] : vals[i];
+ vals[i] = vals[i].toUpperCase();
+ }
+ return vals.join('');
+};
+
+})(); \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.microsoft.core.js b/plugins/Mapstraction/js/mxn.microsoft.core.js
new file mode 100644
index 000000000..292e45d14
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.microsoft.core.js
@@ -0,0 +1,402 @@
+mxn.register('microsoft', {
+
+Mapstraction: {
+
+ init: function(element, api) {
+ var me = this;
+ if (VEMap){
+ this.maps[api] = new VEMap(element.id);
+
+
+
+ this.maps[api].AttachEvent('onclick', function(event){
+ me.clickHandler();
+ var x = event.mapX;
+ var y = event.mapY;
+ var pixel = new VEPixel(x,y);
+ me.click.fire({'location': new mxn.LatLonPoint(pixel.Latitude, pixel.Longitude)});
+
+
+ });
+ this.maps[api].AttachEvent('onendzoom', function(event){
+ me.moveendHandler(me);
+ me.changeZoom.fire();
+
+
+ });
+ this.maps[api].AttachEvent('onendpan', function(event){
+ me.moveendHandler(me);
+ me.endPan.fire();
+
+
+ });
+ this.maps[api].AttachEvent('onchangeview', function(event){
+ me.endPan.fire();
+
+
+ });
+ this.maps[api].LoadMap();
+ document.getElementById("MSVE_obliqueNotification").style.visibility = "hidden";
+
+ //removes the bird's eye pop-up
+ this.loaded[api] = true;
+ me.load.fire();
+ }
+ else{
+ alert(api + ' map script not imported')
+
+ }
+
+ },
+
+ applyOptions: function(){
+ var map = this.maps[this.api];
+ if(this.options.enableScrollWheelZoom){
+ map.enableContinuousZoom();
+ map.enableScrollWheelZoom();
+
+ }
+
+ },
+
+ resizeTo: function(width, height){
+ this.maps[this.api].Resize(width, height);
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+
+ if (args.pan) {
+ map.SetDashboardSize(VEDashboardSize.Normal);
+ }
+ else {
+ map.SetDashboardSize(VEDashboardSize.Tiny)
+ }
+
+ if (args.zoom == 'large') {
+ map.SetDashboardSize(VEDashboardSize.Small)
+ }
+ else if ( args.zoom == 'small' ) {
+ map.SetDashboardSize(VEDashboardSize.Tiny)
+ }
+ else {
+ map.HideDashboard();
+ map.HideScalebar();
+ }
+
+
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ map.SetDashboardSize(VEDashboardSize.Tiny);
+
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ map.SetDashboardSize(VEDashboardSize.Normal);
+ this.addControlsArgs.pan=true;
+ this.addControlsArgs.zoom = 'large';
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+ map.addTypeControl();
+
+ },
+
+ dragging: function(on) {
+ var map = this.maps[this.api];
+ if(on){
+ map.enableDragMap();
+ }
+ else{
+ map.disableDragMap();
+
+ }
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ var vzoom = zoom;
+ map.SetCenterAndZoom(new VELatLong(point.lat,point.lon), vzoom)
+
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ marker.pinID = "mspin-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
+ var pin = marker.toProprietary(this.api);
+ map.AddShape(pin);
+ //give onclick event
+ //give on double click event
+ //give on close window
+ //return the marker
+
+
+ return pin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+ var id = marker.proprietary_marker.GetID();
+ var microsoftShape = map.GetShapeByID(id);
+ map.DeleteShape(microsoftShape);
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ declutterMarkers: function(opts) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+ pl.HideIcon();//hide the icon VE automatically displays
+ map.AddShape(pl);
+
+ return pl;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ var id = polyline.proprietary_polyline.GetID();
+ var microsoftShape = map.GetShapeByID(id);
+ map.DeleteShape(microsoftShape);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var LL = map.GetCenter();
+ var point = new mxn.LatLonPoint(LL.Latitude, LL.Longitude);
+ return point;
+
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.SetCenter(new VELatLong(point.lat, point.lon));
+
+
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.SetZoomLevel(zoom);
+
+
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ var zoom = map.GetZoomLevel();
+
+ return zoom;
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // NE and SW points from the bounding box.
+ var ne = bbox.getNorthEast();
+ var sw = bbox.getSouthWest();
+ var zoom;
+
+ // TODO: Add provider code
+
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ map.SetMapStyle(VEMapStyle.Road);
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ map.SetMapStyle(VEMapStyle.Aerial);
+ break;
+ case mxn.Mapstraction.HYBRID:
+ map.SetMapStyle(VEMapStyle.Hybrid);
+ break;
+ default:
+ map.SetMapStyle(VEMapStyle.Road);
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ var mode = map.GetMapStyle();
+ switch(mode){
+ case VEMapStyle.Aerial:
+ return mxn.Mapstraction.SATELLITE;
+ case VEMapStyle.Road:
+ return mxn.Mapstraction.ROAD;
+ case VEMapStyle.Hybrid:
+ return mxn.Mapstraction.HYBRID;
+ default:
+ return null;
+
+ }
+
+
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ view = map.GetMapView();
+ var topleft = view.TopLeftLatLong;
+ var bottomright = view.BottomRightLatLong;
+
+ return new mxn.BoundingBox(bottomright.Latitude,topleft.Longitude,topleft.Latitude, bottomright.Longitude );
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+
+ var rec = new VELatLongRectangle(new VELatLong(ne.lat, ne.lon), new VELatLong(sw.lat, sw.lon));
+ map.SetMapView(rec);
+
+
+
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ // TODO: Add provider code
+
+ // oContext.pixels.top = ...;
+ // oContext.pixels.left = ...;
+ // oContext.pixels.bottom = ...;
+ // oContext.pixels.right = ...;
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+ var layer = new VEShapeLayer();
+ var mlayerspec = new VEShapeSourceSpecification(VEDataType.GeoRSS, url, layer);
+ map.AddShapeLayer(layer);
+
+
+
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ throw 'Not implemented';
+ },
+
+ toggleTileLayer: function(tile_url) {
+ throw 'Not implemented';
+ },
+
+ getPixelRatio: function() {
+ throw 'Not implemented';
+ },
+
+ mousePosition: function(element) {
+ var locDisp = document.getElementById(element);
+ if (locDisp != null) {
+ var map = this.maps[this.api];
+ map.AttachEvent("onmousemove", function(veEvent){
+ var latlon = map.PixelToLatLong(new VEPixel(veEvent.mapX, veEvent.mapY));
+ var loc = latlon.Latitude.toFixed(4) + " / " + latlon.Longitude.toFixed(4);
+ locDisp.innerHTML = loc;
+
+ });
+ locDisp.innerHTML = "0.0000 / 0.0000";
+ }
+ }
+},
+
+LatLonPoint: {
+
+ toProprietary: function() {
+ return new VELatLong(this.lat, this.lon);
+ },
+
+ fromProprietary: function(mpoint) {
+ this.lat =mpoint.Latitude;
+ this.lon =mpoint.Longitude;
+ }
+
+},
+
+Marker: {
+
+ toProprietary: function() {
+ var mmarker = new VEShape(VEShapeType.Pushpin, this.location.toProprietary('microsoft'));
+
+
+ return mmarker;
+
+ },
+
+ openBubble: function() {
+ var mmarker = this.proprietary_marker;
+ map.ClearInfoBoxStyles();
+ mmarker.SetTitle(this.infoBubble);
+ },
+
+ hide: function() {
+ this.proprietary_marker.hide();
+ },
+
+ show: function() {
+ this.proprietary_marker_unhide();
+ },
+
+ update: function() {
+ throw 'Not implemented';
+ }
+
+},
+
+Polyline: {
+
+ toProprietary: function() {
+ var mpoints =[];
+ for(var i =0, length = this.points.length; i < length; i++)
+ {
+ mpoints.push(this.points[i].toProprietary('microsoft'));
+ }
+ var mpolyline = new VEShape(VEShapeType.Polyline, mpoints);
+ if(this.color){
+ var color = new mxn.util.Color(this.color);
+ var opacity = (typeof(this.opacity) == 'undefined' || this.opacity === null) ? 1.0 : this.opacity;
+ var vecolor = new VEColor(color.red, color.green, color.blue, opacity);
+ mpolyline.SetLineColor(vecolor);
+ }
+ // TODO ability to change line width
+ return mpolyline;
+ },
+
+ show: function() {
+ this.proprietary_polyline.Show();
+ },
+
+ hide: function() {
+ this.proprietary_polyline.Hide();
+ }
+
+}
+
+});
diff --git a/plugins/Mapstraction/js/mxn.openlayers.core.js b/plugins/Mapstraction/js/mxn.openlayers.core.js
new file mode 100644
index 000000000..c04c3321b
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.openlayers.core.js
@@ -0,0 +1,513 @@
+mxn.register('openlayers', {
+
+ Mapstraction: {
+
+ init: function(element, api){
+ var me = this;
+ this.maps[api] = new OpenLayers.Map(
+ element.id,
+ {
+ maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
+ maxResolution:156543,
+ numZoomLevels:18,
+ units:'meters',
+ projection: "EPSG:41001"
+ }
+ );
+
+ this.layers['osmmapnik'] = new OpenLayers.Layer.TMS(
+ 'OSM Mapnik',
+ [
+ "http://a.tile.openstreetmap.org/",
+ "http://b.tile.openstreetmap.org/",
+ "http://c.tile.openstreetmap.org/"
+ ],
+ {
+ type:'png',
+ getURL: function (bounds) {
+ var res = this.map.getResolution();
+ var x = Math.round ((bounds.left - this.maxExtent.left) / (res * this.tileSize.w));
+ var y = Math.round ((this.maxExtent.top - bounds.top) / (res * this.tileSize.h));
+ var z = this.map.getZoom();
+ var limit = Math.pow(2, z);
+ if (y < 0 || y >= limit) {
+ return null;
+ } else {
+ x = ((x % limit) + limit) % limit;
+ var path = z + "/" + x + "/" + y + "." + this.type;
+ var url = this.url;
+ if (url instanceof Array) {
+ url = this.selectUrl(path, url);
+ }
+ return url + path;
+ }
+ },
+ displayOutsideMaxExtent: true
+ }
+ );
+
+ this.layers['osm'] = new OpenLayers.Layer.TMS(
+ 'OSM',
+ [
+ "http://a.tah.openstreetmap.org/Tiles/tile.php/",
+ "http://b.tah.openstreetmap.org/Tiles/tile.php/",
+ "http://c.tah.openstreetmap.org/Tiles/tile.php/"
+ ],
+ {
+ type:'png',
+ getURL: function (bounds) {
+ var res = this.map.getResolution();
+ var x = Math.round ((bounds.left - this.maxExtent.left) / (res * this.tileSize.w));
+ var y = Math.round ((this.maxExtent.top - bounds.top) / (res * this.tileSize.h));
+ var z = this.map.getZoom();
+ var limit = Math.pow(2, z);
+ if (y < 0 || y >= limit) {
+ return null;
+ } else {
+ x = ((x % limit) + limit) % limit;
+ var path = z + "/" + x + "/" + y + "." + this.type;
+ var url = this.url;
+ if (url instanceof Array) {
+ url = this.selectUrl(path, url);
+ }
+ return url + path;
+ }
+ },
+ displayOutsideMaxExtent: true
+ }
+ );
+
+ this.maps[api].addLayer(this.layers['osmmapnik']);
+ this.maps[api].addLayer(this.layers['osm']);
+ },
+
+ applyOptions: function(){
+ // var map = this.maps[this.api];
+ // var myOptions = [];
+ // if (this.options.enableDragging) {
+ // myOptions.draggable = true;
+ // }
+ // if (this.options.enableScrollWheelZoom){
+ // myOptions.scrollwheel = true;
+ // }
+ // map.setOptions(myOptions);
+ },
+
+ resizeTo: function(width, height){
+ this.currentElement.style.width = width;
+ this.currentElement.style.height = height;
+ this.maps[this.api].updateSize();
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+ // FIXME: OpenLayers has a bug removing all the controls says crschmidt
+ for (var i = map.controls.length; i>1; i--) {
+ map.controls[i-1].deactivate();
+ map.removeControl(map.controls[i-1]);
+ }
+ if ( args.zoom == 'large' ) {
+ map.addControl(new OpenLayers.Control.PanZoomBar());
+ }
+ else if ( args.zoom == 'small' ) {
+ map.addControl(new OpenLayers.Control.ZoomPanel());
+ if ( args.pan) {
+ map.addControl(new OpenLayers.Control.PanPanel());
+ }
+ }
+ else {
+ if ( args.pan){
+ map.addControl(new OpenLayers.Control.PanPanel());
+ }
+ }
+ if ( args.overview ) {
+ map.addControl(new OpenLayers.Control.OverviewMap());
+ }
+ if ( args.map_type ) {
+ map.addControl(new OpenLayers.Control.LayerSwitcher());
+ }
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ this.addControlsArgs.pan = false;
+ this.addControlsArgs.scale = false;
+ this.addControlsArgs.zoom = 'small';
+ map.addControl(new OpenLayers.Control.ZoomBox());
+ map.addControl(new OpenLayers.Control.LayerSwitcher({
+ 'ascending':false
+ }));
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ map.addControl(new OpenLayers.Control.PanZoomBar());
+ this.addControlsArgs.pan = true;
+ this.addControlsArgs.zoom = 'large';
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+ map.addControl( new OpenLayers.Control.LayerSwitcher({
+ 'ascending':false
+ }) );
+ this.addControlsArgs.map_type = true;
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.setCenter(point.toProprietary(this.api), zoom);
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ var pin = marker.toProprietary(this.api);
+ if (!this.layers['markers']) {
+ this.layers['markers'] = new OpenLayers.Layer.Markers('markers');
+ map.addLayer(this.layers['markers']);
+ }
+ this.layers['markers'].addMarker(pin);
+
+ return pin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+ var pin = marker.toProprietary(this.api);
+ this.layers['markers'].removeMarker(pin);
+ pin.destroy();
+
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ declutterMarkers: function(opts) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+
+ if (!this.layers['polylines']) {
+ this.layers['polylines'] = new OpenLayers.Layer.Vector('polylines');
+ map.addLayer(this.layers['polylines']);
+ }
+ polyline.setChild(pl);
+ this.layers['polylines'].addFeatures([pl]);
+ return pl;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+ this.layers['polylines'].removeFeatures([pl]);
+ },
+ removeAllPolylines: function() {
+ var olpolylines = [];
+ for(var i = 0, length = this.polylines.length; i < length; i++){
+ olpolylines.push(this.polylines[i].toProprietary(this.api));
+ }
+ if (this.layers['polylines']) this.layers['polylines'].removeFeatures(olpolylines);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ pt = map.getCenter();
+ return new mxn.LatLonPoint(pt.lat, pt.lon);
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.setCenter(pt);
+
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ map.zoomTo(zoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return map.zoom;
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ var map = this.maps[this.api];
+ // throw 'Not implemented';
+ return zoom;
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+ throw 'Not implemented (setMapType)';
+
+ // switch(type) {
+ // case mxn.Mapstraction.ROAD:
+ // map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
+ // break;
+ // case mxn.Mapstraction.SATELLITE:
+ // map.setMapTypeId(google.maps.MapTypeId.SATELLITE);
+ // break;
+ // case mxn.Mapstraction.HYBRID:
+ // map.setMapTypeId(google.maps.MapTypeId.HYBRID);
+ // break;
+ // default:
+ // map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
+ // }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ // TODO: implement actual layer support
+ return mxn.Mapstraction.ROAD;
+
+ // var type = map.getMapTypeId();
+ // switch(type) {
+ // case google.maps.MapTypeId.ROADMAP:
+ // return mxn.Mapstraction.ROAD;
+ // case google.maps.MapTypeId.SATELLITE:
+ // return mxn.Mapstraction.SATELLITE;
+ // case google.maps.MapTypeId.HYBRID:
+ // return mxn.Mapstraction.HYBRID;
+ // //case google.maps.MapTypeId.TERRAIN:
+ // // return something;
+ // default:
+ // return null;
+ // }
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ var olbox = map.calculateBounds();
+ return new mxn.BoundingBox(olbox.bottom, olbox.left, olbox.top, olbox.right);
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+
+ if(sw.lon > ne.lon) {
+ sw.lon -= 360;
+ }
+
+ var obounds = new OpenLayers.Bounds();
+
+ obounds.extend(new mxn.LatLonPoint(sw.lat,sw.lon).toProprietary(this.api));
+ obounds.extend(new mxn.LatLonPoint(ne.lat,ne.lon).toProprietary(this.api));
+ map.zoomToExtent(obounds);
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ setImagePosition: function(id, oContext) {
+ var map = this.maps[this.api];
+ var topLeftPoint; var bottomRightPoint;
+
+ // TODO: Add provider code
+
+ //oContext.pixels.top = ...;
+ //oContext.pixels.left = ...;
+ //oContext.pixels.bottom = ...;
+ //oContext.pixels.right = ...;
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ toggleTileLayer: function(tile_url) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ getPixelRatio: function() {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ },
+
+ mousePosition: function(element) {
+ var map = this.maps[this.api];
+
+ // TODO: Add provider code
+ }
+ },
+
+ LatLonPoint: {
+
+ toProprietary: function() {
+ var ollon = this.lon * 20037508.34 / 180;
+ var ollat = Math.log(Math.tan((90 + this.lat) * Math.PI / 360)) / (Math.PI / 180);
+ ollat = ollat * 20037508.34 / 180;
+ return new OpenLayers.LonLat(ollon, ollat);
+ },
+
+ fromProprietary: function(olPoint) {
+ var lon = (olPoint.lon / 20037508.34) * 180;
+ var lat = (olPoint.lat / 20037508.34) * 180;
+ lat = 180/Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);
+ this.lon = lon;
+ this.lat = lat;
+ }
+
+ },
+
+ Marker: {
+
+ toProprietary: function() {
+ var size, anchor, icon;
+ if(this.iconSize) {
+ size = new OpenLayers.Size(this.iconSize[0], this.iconSize[1]);
+ }
+ else {
+ size = new OpenLayers.Size(21,25);
+ }
+
+ if(this.iconAnchor) {
+ anchor = new OpenLayers.Pixel(this.iconAnchor[0], this.iconAnchor[1]);
+ }
+ else {
+ // FIXME: hard-coding the anchor point
+ anchor = new OpenLayers.Pixel(-(size.w/2), -size.h);
+ }
+
+ if(this.iconUrl) {
+ icon = new OpenLayers.Icon(this.iconUrl, size, anchor);
+ }
+ else {
+ icon = new OpenLayers.Icon('http://openlayers.org/dev/img/marker-gold.png', size, anchor);
+ }
+ var marker = new OpenLayers.Marker(this.location.toProprietary("openlayers"), icon);
+
+ if(this.infoBubble) {
+ var popup = new OpenLayers.Popup(null,
+ this.location.toProprietary("openlayers"),
+ new OpenLayers.Size(100,100),
+ this.infoBubble,
+ true);
+ popup.autoSize = true;
+ var theMap = this.map;
+ if(this.hover) {
+ marker.events.register("mouseover", marker, function(event) {
+ theMap.addPopup(popup);
+ popup.show();
+ });
+ marker.events.register("mouseout", marker, function(event) {
+ popup.hide();
+ theMap.removePopup(popup);
+ });
+ }
+ else {
+ var shown = false;
+ marker.events.register("mousedown", marker, function(event) {
+ if (shown) {
+ popup.hide();
+ theMap.removePopup(popup);
+ shown = false;
+ } else {
+ theMap.addPopup(popup);
+ popup.show();
+ shown = true;
+ }
+ });
+ }
+ }
+
+ if(this.hoverIconUrl) {
+ // TODO
+ }
+
+ if(this.infoDiv){
+ // TODO
+ }
+ return marker;
+ },
+
+ openBubble: function() {
+ // TODO: Add provider code
+ },
+
+ hide: function() {
+ this.proprietary_marker.setOptions({visible:false});
+ },
+
+ show: function() {
+ this.proprietary_marker.setOptions({visible:true});
+ },
+
+ update: function() {
+ // TODO: Add provider code
+ }
+
+ },
+
+ Polyline: {
+
+ toProprietary: function() {
+ var olpolyline;
+ var olpoints = [];
+ var ring;
+ var style = {
+ strokeColor: this.color || "#000000",
+ strokeOpacity: this.opacity || 1,
+ strokeWidth: this.width || 1,
+ fillColor: this.fillColor || "#000000",
+ fillOpacity: this.getAttribute('fillOpacity') || 0.2
+ };
+
+ //TODO Handle closed attribute
+
+ for (var i = 0, length = this.points.length ; i< length; i++){
+ olpoint = this.points[i].toProprietary("openlayers");
+ olpoints.push(new OpenLayers.Geometry.Point(olpoint.lon, olpoint.lat));
+ }
+
+ if (this.closed) {
+ // a closed polygon
+ ring = new OpenLayers.Geometry.LinearRing(olpoints);
+ } else {
+ // a line
+ ring = new OpenLayers.Geometry.LineString(olpoints);
+ }
+
+ olpolyline = new OpenLayers.Feature.Vector(ring, null, style);
+
+ return olpolyline;
+ },
+
+ show: function() {
+ throw 'Not implemented';
+ },
+
+ hide: function() {
+ throw 'Not implemented';
+ }
+
+ }
+
+}); \ No newline at end of file
diff --git a/plugins/Mapstraction/js/mxn.yahoo.core.js b/plugins/Mapstraction/js/mxn.yahoo.core.js
new file mode 100644
index 000000000..99e19584e
--- /dev/null
+++ b/plugins/Mapstraction/js/mxn.yahoo.core.js
@@ -0,0 +1,391 @@
+mxn.register('yahoo', {
+
+Mapstraction: {
+
+ init: function(element,api) {
+ var me = this;
+ if (YMap) {
+ this.maps[api] = new YMap(element);
+
+ YEvent.Capture(this.maps[api], EventsList.MouseClick, function(event,location) {
+ me.clickHandler(location.Lat, location.Lon, location, me);
+ me.click.fire({'location': new mxn.LatLonPoint(location.Lat, location.Lon)});
+ });
+ YEvent.Capture(this.maps[api], EventsList.changeZoom, function() {
+ me.moveendHandler(me);
+ me.changeZoom.fire();
+ });
+ YEvent.Capture(this.maps[api], EventsList.endPan, function() {
+ me.moveendHandler(me);
+ me.endPan.fire();
+ });
+ YEvent.Capture(this.maps[api], EventsList.endAutoPan, function() {
+ me.endPan.fire();
+ });
+
+ this.loaded[api] = true;
+ me.load.fire();
+ }
+ else {
+ alert(api + ' map script not imported');
+ }
+ },
+
+ applyOptions: function(){
+
+ /*
+ if (this.options.enableDragging) {
+ map.enableDragMap();
+ } else {
+ map.disableDragMap();
+ }*/
+
+ },
+
+ resizeTo: function(width, height){
+ this.maps[this.api].resizeTo(new YSize(width,height));
+ },
+
+ addControls: function( args ) {
+ var map = this.maps[this.api];
+
+ if (args.pan) {
+ map.addPanControl();
+ }
+ else {
+ // Yahoo doesn't check the pan control is there before trying to remove it
+ // so throws an exception :(
+ map.addPanControl();
+ map.removePanControl();
+ }
+
+ if (args.zoom == 'large') {
+ map.addZoomLong();
+ }
+ else if ( args.zoom == 'small' ) {
+ map.addZoomShort();
+ }
+ else {
+ map.removeZoomScale();
+ }
+ },
+
+ addSmallControls: function() {
+ var map = this.maps[this.api];
+ map.addPanControl();
+ map.addZoomShort();
+ this.addControlsArgs.pan = true;
+ this.addControlsArgs.zoom = 'small';
+ },
+
+ addLargeControls: function() {
+ var map = this.maps[this.api];
+ map.addPanControl();
+ map.addZoomLong();
+ this.addControlsArgs.pan = true; // keep the controls in case of swap
+ this.addControlsArgs.zoom = 'large';
+ },
+
+ addMapTypeControls: function() {
+ var map = this.maps[this.api];
+ map.addTypeControl();
+ },
+
+ dragging: function(on) {
+ var map = this.maps[this.api];
+ if (on) {
+ map.enableDragMap();
+ } else {
+ map.disableDragMap();
+ }
+ },
+
+ setCenterAndZoom: function(point, zoom) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+
+ var yzoom = 18 - zoom; // maybe?
+ map.drawZoomAndCenter(pt,yzoom);
+ },
+
+ addMarker: function(marker, old) {
+ var map = this.maps[this.api];
+ var pin = marker.toProprietary(this.api);
+ map.addOverlay(pin);
+ YEvent.Capture(pin, EventsList.MouseClick, function() {
+ marker.click.fire();
+ });
+ YEvent.Capture(pin, EventsList.openSmartWindow, function() {
+ marker.openInfoBubble.fire();
+ });
+ YEvent.Capture(pin, EventsList.closeSmartWindow, function() {
+ marker.closeInfoBubble.fire();
+ });
+ return pin;
+ },
+
+ removeMarker: function(marker) {
+ var map = this.maps[this.api];
+ map.removeOverlay(marker.proprietary_marker);
+ },
+
+ removeAllMarkers: function() {
+ var map = this.maps[this.api];
+ map.removeMarkersAll();
+ },
+
+ declutterMarkers: function(opts) {
+ throw 'Not implemented';
+ },
+
+ addPolyline: function(polyline, old) {
+ var map = this.maps[this.api];
+ var pl = polyline.toProprietary(this.api);
+ map.addOverlay(pl);
+ return pl;
+ },
+
+ removePolyline: function(polyline) {
+ var map = this.maps[this.api];
+ map.removeOverlay(polyline.proprietary_polyline);
+ },
+
+ getCenter: function() {
+ var map = this.maps[this.api];
+ var pt = map.getCenterLatLon();
+ var point = new mxn.LatLonPoint(pt.Lat, pt.Lon);
+ return point;
+ },
+
+ setCenter: function(point, options) {
+ var map = this.maps[this.api];
+ var pt = point.toProprietary(this.api);
+ map.panToLatLon(pt);
+ },
+
+ setZoom: function(zoom) {
+ var map = this.maps[this.api];
+ var yzoom = 18 - zoom; // maybe?
+ map.setZoomLevel(yzoom);
+ },
+
+ getZoom: function() {
+ var map = this.maps[this.api];
+ return 18 - map.getZoomLevel();
+ },
+
+ getZoomLevelForBoundingBox: function( bbox ) {
+ throw 'Not implemented';
+ },
+
+ setMapType: function(type) {
+ var map = this.maps[this.api];
+
+ switch(type) {
+ case mxn.Mapstraction.ROAD:
+ map.setMapType(YAHOO_MAP_REG);
+ break;
+ case mxn.Mapstraction.SATELLITE:
+ map.setMapType(YAHOO_MAP_SAT);
+ break;
+ case mxn.Mapstraction.HYBRID:
+ map.setMapType(YAHOO_MAP_HYB);
+ break;
+ default:
+ map.setMapType(YAHOO_MAP_REG);
+ }
+ },
+
+ getMapType: function() {
+ var map = this.maps[this.api];
+ var type = map.getCurrentMapType();
+ switch(type) {
+ case YAHOO_MAP_REG:
+ return mxn.Mapstraction.ROAD;
+ case YAHOO_MAP_SAT:
+ return mxn.Mapstraction.SATELLITE;
+ case YAHOO_MAP_HYB:
+ return mxn.Mapstraction.HYBRID;
+ default:
+ return null;
+ }
+ },
+
+ getBounds: function () {
+ var map = this.maps[this.api];
+ var ybox = map.getBoundsLatLon();
+ return new mxn.BoundingBox(ybox.LatMin, ybox.LonMin, ybox.LatMax, ybox.LonMax);
+ },
+
+ setBounds: function(bounds){
+ var map = this.maps[this.api];
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+
+ if(sw.lon > ne.lon) {
+ sw.lon -= 360;
+ }
+ var center = new YGeoPoint((sw.lat + ne.lat)/2, (ne.lon + sw.lon)/2);
+
+ var container = map.getContainerSize();
+ for(var zoom = 1 ; zoom <= 17 ; zoom++){
+ var sw_pix = mxn.util.convertLatLonXY_Yahoo(sw,zoom);
+ var ne_pix = mxn.util.convertLatLonXY_Yahoo(ne,zoom);
+ if(sw_pix.x > ne_pix.x) {
+ sw_pix.x -= (1 << (26 - zoom)); //earth circumference in pixel
+ }
+ if(Math.abs(ne_pix.x - sw_pix.x) <= container.width
+ && Math.abs(ne_pix.y - sw_pix.y) <= container.height){
+ map.drawZoomAndCenter(center, zoom); //Call drawZoomAndCenter here: OK if called multiple times anyway
+ break;
+ }
+ }
+ },
+
+ addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
+ throw 'Not implemented';
+ },
+
+ setImagePosition: function(id) {
+ throw 'Not implemented';
+ },
+
+ addOverlay: function(url, autoCenterAndZoom) {
+ var map = this.maps[this.api];
+ map.addOverlay(new YGeoRSS(url));
+ },
+
+ addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
+ throw 'Not implemented';
+ },
+
+ toggleTileLayer: function(tile_url) {
+ throw 'Not implemented';
+ },
+
+ getPixelRatio: function() {
+ throw 'Not implemented';
+ },
+
+ mousePosition: function(element) {
+ throw 'Not implemented';
+ }
+
+},
+
+LatLonPoint: {
+
+ toProprietary: function() {
+ return new YGeoPoint(this.lat,this.lon);
+ },
+
+ fromProprietary: function(yahooPoint) {
+ this.lat = yahooPoint.Lat;
+ this.lon = yahooPoint.Lon;
+ }
+
+},
+
+Marker: {
+
+ toProprietary: function() {
+ var ymarker, size;
+ var infoBubble, event_action, infoDiv, div;
+
+ if(this.iconSize) {
+ size = new YSize(this.iconSize[0], this.iconSize[1]);
+ }
+ if(this.iconUrl) {
+ if(this.iconSize){
+ ymarker = new YMarker(this.location.toProprietary('yahoo'), new YImage(this.iconUrl, size));
+ }
+ else {
+ ymarker = new YMarker(this.location.toProprietary('yahoo'), new YImage(this.iconUrl));
+ }
+ }
+ else {
+ if(this.iconSize) {
+ ymarker = new YMarker(this.location.toProprietary('yahoo'), null, size);
+ }
+ else {
+ ymarker = new YMarker(this.location.toProprietary('yahoo'));
+ }
+ }
+
+ if(this.labelText) {
+ ymarker.addLabel(this.labelText);
+ }
+
+ if(this.infoBubble) {
+ infoBubble = this.infoBubble;
+ if(this.hover) {
+ event_action = EventsList.MouseOver;
+ }
+ else {
+ event_action = EventsList.MouseClick;
+ }
+ YEvent.Capture(ymarker, event_action, function() {
+ ymarker.openSmartWindow(infoBubble);
+ });
+
+ }
+
+ if(this.infoDiv) {
+ infoDiv = this.infoDiv;
+ div = this.div;
+ if(this.hover) {
+ event_action = EventsList.MouseOver;
+ }
+ else {
+ event_action = EventsList.MouseClick;
+ }
+ YEvent.Capture(ymarker, event_action, function() {
+ document.getElementById(div).innerHTML = infoDiv;
+ });
+ }
+
+ return ymarker;
+ },
+
+ openBubble: function() {
+ var ypin = this.proprietary_marker;
+ ypin.openSmartWindow(this.infoBubble);
+ },
+
+ hide: function() {
+ this.proprietary_marker.hide();
+ },
+
+ show: function() {
+ this.proprietary_marker.unhide();
+ },
+
+ update: function() {
+ throw 'Not implemented';
+ }
+
+},
+
+Polyline: {
+
+ toProprietary: function() {
+ var ypolyline;
+ var ypoints = [];
+ for (var i = 0, length = this.points.length ; i< length; i++){
+ ypoints.push(this.points[i].toProprietary('yahoo'));
+ }
+ ypolyline = new YPolyline(ypoints,this.color,this.width,this.opacity);
+ return ypolyline;
+ },
+
+ show: function() {
+ throw 'Not implemented';
+ },
+
+ hide: function() {
+ throw 'Not implemented';
+ }
+
+}
+
+}); \ No newline at end of file
diff --git a/plugins/Mapstraction/usermap.js b/plugins/Mapstraction/usermap.js
new file mode 100644
index 000000000..270b7efea
--- /dev/null
+++ b/plugins/Mapstraction/usermap.js
@@ -0,0 +1,45 @@
+$(document).ready(function() {
+ var mapstraction = new mxn.Mapstraction("map_canvas", _provider);
+
+ var minLat = 181.0;
+ var maxLat = -181.0;
+ var minLon = 181.0;
+ var maxLon = -181.0;
+
+ for (var i in _notices)
+ {
+ var n = _notices[i];
+
+ var lat = n['geo']['coordinates'][0];
+ var lon = n['geo']['coordinates'][1];
+
+ if (lat < minLat) {
+ minLat = lat;
+ }
+
+ if (lat > maxLat) {
+ maxLat = lat;
+ }
+
+ if (lon < minLon) {
+ minLon = lon;
+ }
+
+ if (lon > maxLon) {
+ maxLon = lon;
+ }
+
+ pt = new mxn.LatLonPoint(lat, lon);
+ mkr = new mxn.Marker(pt);
+
+ mkr.setIcon(n['user']['profile_image_url']);
+ mkr.setInfoBubble('<a href="'+ n['user']['profile_url'] + '">' + n['user']['screen_name'] + '</a>' + ' ' + n['html'] +
+ '<br/><a href="'+ n['url'] + '">'+ n['created_at'] + '</a>');
+
+ mapstraction.addMarker(mkr);
+ }
+
+ bounds = new mxn.BoundingBox(minLat, minLon, maxLat, maxLon);
+
+ mapstraction.setBounds(bounds);
+});
diff --git a/plugins/Mapstraction/usermap.php b/plugins/Mapstraction/usermap.php
new file mode 100644
index 000000000..fbf1469c3
--- /dev/null
+++ b/plugins/Mapstraction/usermap.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show a map of user's notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Mapstraction
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Show a map of user's notices
+ *
+ * @category Mapstraction
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class UsermapAction extends OwnerDesignAction
+{
+ var $profile = null;
+ var $page = null;
+ var $notices = null;
+
+ public $plugin = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $nickname_arg = $this->arg('nickname');
+ $nickname = common_canonical_nickname($nickname_arg);
+
+ // Permanent redirect on non-canonical nickname
+
+ if ($nickname_arg != $nickname) {
+ $args = array('nickname' => $nickname);
+ if ($this->arg('page') && $this->arg('page') != 1) {
+ $args['page'] = $this->arg['page'];
+ }
+ common_redirect(common_local_url($this->trimmed('action'), $args), 301);
+ return false;
+ }
+
+ $this->user = User::staticGet('nickname', $nickname);
+
+ if (!$this->user) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+
+ $this->profile = $this->user->getProfile();
+
+ if (!$this->profile) {
+ $this->serverError(_('User has no profile.'));
+ return false;
+ }
+
+ $page = $this->trimmed('page');
+
+ if (!empty($page) && Validate::number($page)) {
+ $this->page = $page+0;
+ } else {
+ $this->page = 1;
+ }
+
+ $this->notices = $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+
+ return true;
+ }
+
+ function title()
+ {
+ if (!empty($this->profile->fullname)) {
+ $base = $this->profile->fullname . ' (' . $this->user->nickname . ') ';
+ } else {
+ $base = $this->user->nickname;
+ }
+
+ if ($this->page == 1) {
+ return $base;
+ } else {
+ return sprintf(_("%s map, page %d"),
+ $base,
+ $this->page);
+ }
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ function showContent()
+ {
+ $this->element('div', array('id' => 'map_canvas',
+ 'class' => 'gray smallmap',
+ 'style' => "width: 100%; height: 400px"));
+ }
+} \ No newline at end of file
diff --git a/plugins/Mobile/WAP20Plugin.php b/plugins/Mobile/WAP20Plugin.php
new file mode 100644
index 000000000..aae48a520
--- /dev/null
+++ b/plugins/Mobile/WAP20Plugin.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Superclass for WAP 2.0 support
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+
+/**
+ * Superclass for plugin to output XHTML Mobile Profile
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class WAP20Plugin extends Plugin
+{
+
+ function onStartShowHTML($action)
+ {
+
+ }
+
+}
+
+
+?>
diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php
new file mode 100644
index 000000000..c257f3fd8
--- /dev/null
+++ b/plugins/MobileProfile/MobileProfilePlugin.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * XHTML Mobile Profile plugin that uses WAP 2.0 Plugin
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+define('PAGE_TYPE_PREFS',
+ 'application/vnd.wap.xhtml+xml, application/xhtml+xml, text/html;q=0.9');
+
+require_once INSTALLDIR.'/plugins/Mobile/WAP20Plugin.php';
+
+
+/**
+ * Superclass for plugin to output XHTML Mobile Profile
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class MobileProfilePlugin extends WAP20Plugin
+{
+ public $DTD = null;
+ public $serveMobile = false;
+ public $mobileFeatures = array();
+
+ function __construct($DTD='http://www.wapforum.org/DTD/xhtml-mobile10.dtd')
+ {
+ $this->DTD = $DTD;
+
+ parent::__construct();
+ }
+
+
+ function onStartShowHTML($action)
+ {
+ // XXX: This should probably graduate to WAP20Plugin
+
+ // If they are on the mobile site, serve them MP
+ if ((common_config('site', 'mobileserver').'/'.
+ common_config('site', 'path').'/' ==
+ $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])) {
+
+ $this->serveMobile = true;
+ } else {
+ // If they like the WAP 2.0 mimetype, serve them MP
+ if (strstr('application/vnd.wap.xhtml+xml', $type) !== false) {
+ $this->serveMobile = true;
+ } else {
+ // If they are a mobile device that supports WAP 2.0,
+ // serve them MP
+
+ // XXX: Browser sniffing sucks
+
+ // I really don't like going through this every page,
+ // perhaps use $_SESSION or cookies
+
+ // May be better to group the devices in terms of
+ // low,mid,high-end
+
+ // Or, detect the mobile devices based on their support for
+ // MP 1.0, 1.1, or 1.2 may be ideal. Possible?
+
+ $this->mobiledevices = array(
+ 'alcatel',
+ 'android',
+ 'audiovox',
+ 'au-mic,',
+ 'avantgo',
+ 'blackberry',
+ 'blazer',
+ 'cldc-',
+ 'danger',
+ 'epoc',
+ 'ericsson',
+ 'ericy',
+ 'iphone',
+ 'ipaq',
+ 'ipod',
+ 'j2me',
+ 'lg',
+ 'midp-',
+ 'mobile',
+ 'mot',
+ 'netfront',
+ 'nitro',
+ 'nokia',
+ 'opera mini',
+ 'palm',
+ 'palmsource',
+ 'panasonic',
+ 'philips',
+ 'pocketpc',
+ 'portalmmm',
+ 'rover',
+ 'samsung',
+ 'sanyo',
+ 'series60',
+ 'sharp',
+ 'sie-',
+ 'smartphone',
+ 'sony',
+ 'symbian',
+ 'up.browser',
+ 'up.link',
+ 'up.link',
+ 'vodafone',
+ 'wap1',
+ 'wap2',
+ 'windows ce'
+ );
+
+ $httpuseragent = strtolower($_SERVER['HTTP_USER_AGENT']);
+
+ foreach ($this->mobiledevices as $md) {
+ if (strstr($httpuseragent, $md) !== false) {
+ $this->setMobileFeatures($httpuseragent);
+
+ $this->serveMobile = true;
+ break;
+ }
+ }
+ }
+
+ // If they are okay with MP, and the site has a mobile server,
+ // redirect there
+ if ($this->serveMobile &&
+ common_config('site', 'mobileserver') !== false &&
+ (common_config('site', 'mobileserver') !=
+ common_config('site', 'server'))) {
+
+ // FIXME: Redirect to equivalent page on mobile site instead
+ common_redirect($this->_common_path(''), 302);
+ }
+ }
+
+ if (!$this->serveMobile) {
+ return true;
+ }
+
+ if (!$type) {
+ $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ?
+ $_SERVER['HTTP_ACCEPT'] : null;
+
+ $cp = common_accept_to_prefs($httpaccept);
+ $sp = common_accept_to_prefs(PAGE_TYPE_PREFS);
+
+ $type = common_negotiate_type($cp, $sp);
+
+ if (!$type) {
+ throw new ClientException(_('This page is not available in a '.
+ 'media type you accept'), 406);
+ }
+ }
+
+ header('Content-Type: '.$type);
+
+ $action->extraHeaders();
+ if (preg_match("/.*\/.*xml/", $type)) {
+ // Required for XML documents
+ $action->xw->startDocument('1.0', 'UTF-8');
+ }
+ $action->xw->writeDTD('html',
+ '-//WAPFORUM//DTD XHTML Mobile 1.0//EN',
+ $this->DTD);
+
+ $language = $action->getLanguage();
+
+ $action->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
+ 'xml:lang' => $language));
+
+ return false;
+ }
+
+
+ function setMobileFeatures($useragent)
+ {
+ $mobiledeviceInputFileType = array(
+ 'nokia'
+ );
+
+ $this->mobileFeatures['inputfiletype'] = false;
+
+ foreach ($mobiledeviceInputFileType as $md) {
+ if (strstr($useragent, $md) !== false) {
+ $this->mobileFeatures['inputfiletype'] = true;
+ break;
+ }
+ }
+ }
+
+
+ function onStartShowHeadElements($action)
+ {
+ if (!$action->serveMobile) {
+ return true;
+ }
+
+ $action->showTitle();
+ $action->showShortcutIcon();
+ $action->showStylesheets();
+ $action->showFeeds();
+ $action->showDescription();
+ $action->extraHead();
+ }
+
+
+ function onStartShowStatusNetStyles($action)
+ {
+ if (!$this->serveMobile) {
+ return true;
+ }
+
+ if (file_exists(Theme::file('css/mp-screen.css'))) {
+ $action->cssLink('css/mp-screen.css', null, 'screen');
+ } else {
+ $action->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => common_path('plugins/MobileProfile/mp-screen.css') . '?version=' . STATUSNET_VERSION,
+ 'media' => 'screen'));
+ }
+
+ if (file_exists(Theme::file('css/mp-handheld.css'))) {
+ $action->cssLink('css/mp-handheld.css', null, 'handheld');
+ } else {
+ $action->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => common_path('plugins/MobileProfile/mp-handheld.css') . '?version=' . STATUSNET_VERSION,
+ 'media' => 'handheld'));
+ }
+
+ return false;
+ }
+
+
+ function onStartShowHeader($action)
+ {
+ if (!$this->serveMobile) {
+ return true;
+ }
+
+ $action->elementStart('div', array('id' => 'header'));
+ $this->_showLogo($action);
+ $this->_showPrimaryNav($action);
+ if (common_logged_in()) {
+ $action->showNoticeForm();
+ }
+ $action->elementEnd('div');
+
+ return false;
+ }
+
+
+ function _showLogo($action)
+ {
+ $action->elementStart('address', 'vcard');
+ $action->elementStart('a', array('class' => 'url home bookmark',
+ 'href' => common_local_url('public')));
+ if (common_config('site', 'mobilelogo') ||
+ file_exists(Theme::file('logo.png')) ||
+ file_exists(Theme::file('mobilelogo.png'))) {
+
+ $action->element('img', array('class' => 'photo',
+ 'src' => (common_config('site', 'mobilelogo')) ? common_config('site', 'mobilelogo') :
+ ((file_exists(Theme::file('mobilelogo.png'))) ? (Theme::path('mobilelogo.png')) : Theme::path('logo.png')),
+ 'alt' => common_config('site', 'name')));
+ }
+ $action->element('span', array('class' => 'fn org'), common_config('site', 'name'));
+ $action->elementEnd('a');
+ $action->elementEnd('address');
+ }
+
+
+ function _showPrimaryNav($action)
+ {
+ $user = common_current_user();
+ $connect = '';
+ if (common_config('xmpp', 'enabled')) {
+ $connect = 'imsettings';
+ } else if (common_config('sms', 'enabled')) {
+ $connect = 'smssettings';
+ } else if (common_config('twitter', 'enabled')) {
+ $connect = 'twittersettings';
+ }
+
+ $action->elementStart('ul', array('id' => 'site_nav_global_primary'));
+ if ($user) {
+ $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+ _('Home'));
+ $action->menuItem(common_local_url('profilesettings'),
+ _('Account'));
+ if ($connect) {
+ $action->menuItem(common_local_url($connect),
+ _('Connect'));
+ }
+ if (common_config('invite', 'enabled')) {
+ $action->menuItem(common_local_url('invite'),
+ _('Invite'));
+ }
+ $action->menuItem(common_local_url('logout'),
+ _('Logout'));
+ } else {
+ if (!common_config('site', 'closed')) {
+ $action->menuItem(common_local_url('register'),
+ _('Register'));
+ }
+ $action->menuItem(common_local_url('login'),
+ _('Login'));
+ }
+ if ($user || !common_config('site', 'private')) {
+ $action->menuItem(common_local_url('peoplesearch'),
+ _('Search'));
+ }
+ $action->elementEnd('ul');
+ }
+
+
+ function onStartShowNoticeFormData($form)
+ {
+ if (!$this->serveMobile) {
+ return true;
+ }
+
+ $form->out->element('textarea', array('id' => 'notice_data-text',
+ 'cols' => 15,
+ 'rows' => 4,
+ 'name' => 'status_textarea'),
+ ($form->content) ? $form->content : '');
+
+ $contentLimit = Notice::maxContent();
+
+ $form->out->element('script', array('type' => 'text/javascript'),
+ 'maxLength = ' . $contentLimit . ';');
+
+ if ($contentLimit > 0) {
+ $form->out->element('div', array('id' => 'notice_text-count'),
+ $contentLimit);
+ }
+
+ if (common_config('attachments', 'uploads')) {
+ if ($this->mobileFeatures['inputfiletype']) {
+ $form->out->element('label', array('for' => 'notice_data-attach'), _('Attach'));
+ $form->out->element('input', array('id' => 'notice_data-attach',
+ 'type' => 'file',
+ 'name' => 'attach',
+ 'title' => _('Attach a file')));
+ $form->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota'));
+ }
+ }
+ if ($form->action) {
+ $form->out->hidden('notice_return-to', $form->action, 'returnto');
+ }
+ $form->out->hidden('notice_in-reply-to', $form->inreplyto, 'inreplyto');
+
+ return false;
+ }
+
+
+ function onStartShowAside($action)
+ {
+ if ($this->serveMobile) {
+ return false;
+ }
+ }
+
+
+ function onStartShowScripts($action)
+ {
+
+ }
+
+
+ function _common_path($relative, $ssl=false)
+ {
+ $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : '';
+
+ if (($ssl && (common_config('site', 'ssl') === 'sometimes'))
+ || common_config('site', 'ssl') === 'always') {
+ $proto = 'https';
+ if (is_string(common_config('site', 'sslserver')) &&
+ mb_strlen(common_config('site', 'sslserver')) > 0) {
+ $serverpart = common_config('site', 'sslserver');
+ } else {
+ $serverpart = common_config('site', 'mobileserver');
+ }
+ } else {
+ $proto = 'http';
+ $serverpart = common_config('site', 'mobileserver');
+ }
+
+ return $proto.'://'.$serverpart.'/'.$pathpart.$relative;
+ }
+}
+
+
+?>
diff --git a/plugins/MobileProfile/mp-handheld.css b/plugins/MobileProfile/mp-handheld.css
new file mode 100644
index 000000000..e0ea823d5
--- /dev/null
+++ b/plugins/MobileProfile/mp-handheld.css
@@ -0,0 +1 @@
+@import url(mp-screen.css);
diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css
new file mode 100644
index 000000000..1bb0248ec
--- /dev/null
+++ b/plugins/MobileProfile/mp-screen.css
@@ -0,0 +1,248 @@
+@import url(../../theme/base/css/display.css);
+@import url(../../theme/identica/css/display.css);
+
+#wrap {
+min-width:0;
+max-width:100%;
+}
+
+#header {
+margin:0;
+padding:0.7em 2%;
+width:96%;
+}
+
+address {
+margin:1em 0 0 0;
+float:left;
+width:100%;
+}
+address .vcard .photo {
+margin-right:0;
+}
+
+address img + .fn {
+display:block;
+margin-top:1em;
+float:left;
+}
+
+.vcard .photo {
+margin-right:7px;
+}
+
+
+.form_settings fieldset {
+margin-bottom:7px;
+}
+
+.form_settings label {
+width:auto;
+display:block;
+float:none;
+}
+.form_settings .form_data li {
+margin-bottom:7px;
+}
+
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:0;
+display:block;
+}
+.form_settings .form_data textarea {
+width:96.41%;
+}
+
+.form_settings .form_data label {
+float:none;
+}
+
+.form_settings .form_data p.form_guide {
+width:auto;
+margin-left:0;
+}
+
+
+
+#site_nav_global_primary {
+margin:0;
+width:100%;
+list-style-type:none;
+position:absolute;
+top:0;
+left:0;
+}
+#site_nav_global_primary li {
+margin-left:0;
+margin-right:4%;
+float:left;
+font-size:0.9em;
+}
+
+
+#form_notice {
+width:100%;
+}
+
+#form_notice textarea {
+width:60%;
+height:20px;
+}
+
+#notice_text-count {
+position:absolute;
+bottom:2px;
+right:40%;
+z-index:9;
+}
+
+/*input type=file no good in
+iPhone/iPod Touch, Android, Opera Mini Simulator
+*/
+#form_notice #notice_text-count + label,
+#form_notice label[for="notice_data-attach"] {
+display:none;
+}
+#form_notice #notice_data-attach {
+top:auto;
+bottom:0;
+left:0;
+right:auto;
+opacity:1;
+z-index:9;
+width:65%;
+}
+
+#form_notice #notice_action-submit {
+width:20%;
+right:2%;
+text-align:center;
+}
+
+
+#site_nav_local_views li {
+margin-left:0;
+margin-right:0;
+}
+#site_nav_local_views li:first-child {
+margin-left:0;
+}
+#site_nav_local_views a {
+padding:1px 3px;
+display:block;
+font-size:0.9em;
+}
+#site_nav_local_views .current a {
+text-shadow:none;
+}
+#site_nav_local_views li {
+-moz-box-shadow:none;
+-webkit-box-shadow:none;
+box-shadow:none;
+}
+
+
+#content {
+width:96.41%;
+min-height:auto;
+}
+#content,
+#site_nav_local_views a,
+#aside_primary {
+border:0;
+}
+
+.instructions p,
+.instructions ul {
+margin-bottom:4px;
+}
+
+h1 {
+margin-bottom:0;
+}
+
+.notice,
+.profile {
+padding-top:4px;
+padding-bottom:4px;
+}
+.notice .entry-title {
+
+}
+.notice div.entry-content {
+margin-left:0;
+width:65%;
+}
+.notice-options {
+width:30%;
+margin-right:2%;
+}
+
+
+
+.entity_profile {
+width:auto;
+}
+
+
+
+.entity_actions {
+margin-right:0;
+margin-left:0;
+clear:both;
+float:none;
+width:100%;
+max-width:9999px;
+}
+
+.entity_profile {
+margin-bottom:7px;
+min-height:0;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags,
+.entity_profile .entity_aliases {
+line-height:1.4;
+margin-left:0;
+}
+
+.entity_profile .entity_depiction {
+margin-bottom:1%;
+margin-right:7px;
+}
+
+.entity_actions {
+margin-bottom:1%;
+float:left;
+width:100%;
+}
+
+.entity_actions li {
+float:left;
+margin-right:1.5%;
+margin-bottom:0;
+height:29px;
+width:40%;
+}
+
+.user_in .entity_actions .entity_subscribe {
+margin-bottom:47px;
+width:auto;
+height:auto;
+margin-right:5%;
+}
+
+
+
+#footer {
+width:96%;
+padding:2%;
+}
+
diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php
new file mode 100644
index 000000000..6dd8a3f5a
--- /dev/null
+++ b/plugins/OpenID/OpenIDPlugin.php
@@ -0,0 +1,295 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin for OpenID authentication and identity
+ *
+ * This class enables consumer support for OpenID, the distributed authentication
+ * and identity system.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ * @link http://openid.net/
+ */
+
+class OpenIDPlugin extends Plugin
+{
+ /**
+ * Initializer for the plugin.
+ */
+
+ function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Add OpenID-related paths to the router table
+ *
+ * Hook for RouterInitialized event.
+ *
+ * @return boolean hook return
+ */
+
+ function onStartInitializeRouter($m)
+ {
+ $m->connect('main/openid', array('action' => 'openidlogin'));
+ $m->connect('main/openidtrust', array('action' => 'openidtrust'));
+ $m->connect('settings/openid', array('action' => 'openidsettings'));
+ $m->connect('index.php?action=finishopenidlogin', array('action' => 'finishopenidlogin'));
+ $m->connect('index.php?action=finishaddopenid', array('action' => 'finishaddopenid'));
+ $m->connect('main/openidserver', array('action' => 'openidserver'));
+
+ return true;
+ }
+
+ function onEndPublicXRDS($action, &$xrdsOutputter)
+ {
+ $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
+ 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+ 'version' => '2.0'));
+ $xrdsOutputter->element('Type', null, 'xri://$xrds*simple');
+ //consumer
+ foreach (array('finishopenidlogin', 'finishaddopenid') as $finish) {
+ $xrdsOutputter->showXrdsService(Auth_OpenID_RP_RETURN_TO_URL_TYPE,
+ common_local_url($finish));
+ }
+ //provider
+ $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/server',
+ common_local_url('openidserver'),
+ null,
+ null,
+ 'http://specs.openid.net/auth/2.0/identifier_select');
+ $xrdsOutputter->elementEnd('XRD');
+ }
+
+ function onEndUserXRDS($action, &$xrdsOutputter)
+ {
+ $xrdsOutputter->elementStart('XRD', array('xmlns' => 'xri://$xrd*($v*2.0)',
+ 'xml:id' => 'openid',
+ 'xmlns:simple' => 'http://xrds-simple.net/core/1.0',
+ 'version' => '2.0'));
+ $xrdsOutputter->element('Type', null, 'xri://$xrds*simple');
+
+ //consumer
+ $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/return_to',
+ common_local_url('finishopenidlogin'));
+
+ //provider
+ $xrdsOutputter->showXrdsService('http://specs.openid.net/auth/2.0/signon',
+ common_local_url('openidserver'),
+ null,
+ null,
+ common_profile_url($action->user->nickname));
+ $xrdsOutputter->elementEnd('XRD');
+ }
+
+ function onEndLoginGroupNav(&$action)
+ {
+ $action_name = $action->trimmed('action');
+
+ $action->menuItem(common_local_url('openidlogin'),
+ _('OpenID'),
+ _('Login or register with OpenID'),
+ $action_name === 'openidlogin');
+
+ return true;
+ }
+
+ function onEndAccountSettingsNav(&$action)
+ {
+ $action_name = $action->trimmed('action');
+
+ $action->menuItem(common_local_url('openidsettings'),
+ _('OpenID'),
+ _('Add or remove OpenIDs'),
+ $action_name === 'openidsettings');
+
+ return true;
+ }
+
+ function onAutoload($cls)
+ {
+ switch ($cls)
+ {
+ case 'OpenidloginAction':
+ case 'FinishopenidloginAction':
+ case 'FinishaddopenidAction':
+ case 'XrdsAction':
+ case 'PublicxrdsAction':
+ case 'OpenidsettingsAction':
+ case 'OpenidserverAction':
+ case 'OpenidtrustAction':
+ require_once(INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
+ return false;
+ case 'User_openid':
+ require_once(INSTALLDIR.'/plugins/OpenID/User_openid.php');
+ return false;
+ case 'User_openid_trustroot':
+ require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php');
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ function onSensitiveAction($action, &$ssl)
+ {
+ switch ($action)
+ {
+ case 'finishopenidlogin':
+ case 'finishaddopenid':
+ $ssl = true;
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ function onLoginAction($action, &$login)
+ {
+ switch ($action)
+ {
+ case 'openidlogin':
+ case 'finishopenidlogin':
+ case 'openidserver':
+ $login = true;
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * We include a <meta> element linking to the publicxrds page, for OpenID
+ * client-side authentication.
+ *
+ * @return void
+ */
+
+ function onEndShowHeadElements($action)
+ {
+ if($action instanceof ShowstreamAction){
+ $action->element('link', array('rel' => 'openid2.provider',
+ 'href' => common_local_url('openidserver')));
+ $action->element('link', array('rel' => 'openid2.local_id',
+ 'href' => $action->profile->profileurl));
+ $action->element('link', array('rel' => 'openid.server',
+ 'href' => common_local_url('openidserver')));
+ $action->element('link', array('rel' => 'openid.delegate',
+ 'href' => $action->profile->profileurl));
+ }
+ return true;
+ }
+
+ /**
+ * Redirect to OpenID login if they have an OpenID
+ *
+ * @return boolean whether to continue
+ */
+
+ function onRedirectToLogin($action, $user)
+ {
+ if (!empty($user) && User_openid::hasOpenID($user->id)) {
+ common_redirect(common_local_url('openidlogin'), 303);
+ return false;
+ }
+ return true;
+ }
+
+ function onEndShowPageNotice($action)
+ {
+ $name = $action->trimmed('action');
+
+ switch ($name)
+ {
+ case 'register':
+ $instr = '(Have an [OpenID](http://openid.net/)? ' .
+ 'Try our [OpenID registration]'.
+ '(%%action.openidlogin%%)!)';
+ break;
+ case 'login':
+ $instr = '(Have an [OpenID](http://openid.net/)? ' .
+ 'Try our [OpenID login]'.
+ '(%%action.openidlogin%%)!)';
+ break;
+ default:
+ return true;
+ }
+
+ $output = common_markup_to_html($instr);
+ $action->raw($output);
+ return true;
+ }
+
+ function onStartLoadDoc(&$title, &$output)
+ {
+ if ($title == 'openid')
+ {
+ $filename = INSTALLDIR.'/plugins/OpenID/doc-src/openid';
+
+ $c = file_get_contents($filename);
+ $output = common_markup_to_html($c);
+ return false; // success!
+ }
+
+ return true;
+ }
+
+ function onEndLoadDoc($title, &$output)
+ {
+ if ($title == 'help')
+ {
+ $menuitem = '* [OpenID](%%doc.openid%%) - what OpenID is and how to use it with this service';
+
+ $output .= common_markup_to_html($menuitem);
+ }
+
+ return true;
+ }
+
+ function onCheckSchema() {
+ $schema = Schema::get();
+ $schema->ensureDataObject('User_openid');
+ $schema->ensureDataObject('User_openid_trustroot');
+ return true;
+ }
+
+ function onUserDeleteRelated($user, &$tables)
+ {
+ $tables[] = 'User_openid';
+ $tables[] = 'User_openid_trustroot';
+ return true;
+ }
+}
diff --git a/plugins/OpenID/User_openid.php b/plugins/OpenID/User_openid.php
new file mode 100644
index 000000000..c3624118e
--- /dev/null
+++ b/plugins/OpenID/User_openid.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Table Definition for user_openid
+ */
+require_once INSTALLDIR.'/classes/Plugin_DataObject.php';
+
+class User_openid extends Plugin_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'user_openid'; // table name
+ public $canonical; // varchar(255) primary_key not_null
+ public $display; // varchar(255) unique_key not_null
+ public $user_id; // int(4) not_null
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=null)
+ { return Memcached_DataObject::staticGet('User_openid',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ static function hasOpenID($user_id)
+ {
+ $oid = new User_openid();
+
+ $oid->user_id = $user_id;
+
+ $cnt = $oid->find();
+
+ return ($cnt > 0);
+ }
+
+ /**
+ * Get the TableDef object that represents the table backing this class
+ * @return TableDef TableDef instance
+ */
+ function tableDef()
+ {
+ return new TableDef($this->__table,
+ array(new ColumnDef('canonical', 'varchar',
+ '255', false, 'PRI'),
+ new ColumnDef('display', 'varchar',
+ '255', false),
+ new ColumnDef('user_id', 'integer',
+ null, false, 'MUL'),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'timestamp')));
+ }
+}
diff --git a/plugins/OpenID/User_openid_trustroot.php b/plugins/OpenID/User_openid_trustroot.php
new file mode 100644
index 000000000..b208dddfd
--- /dev/null
+++ b/plugins/OpenID/User_openid_trustroot.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Table Definition for user_openid_trustroot
+ */
+require_once INSTALLDIR.'/classes/Plugin_DataObject.php';
+
+class User_openid_trustroot extends Plugin_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'user_openid_trustroot'; // table name
+ public $trustroot; // varchar(255) primary_key not_null
+ public $user_id; // int(4) primary_key not_null
+ public $created; // datetime() not_null
+ public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=null)
+ { return Memcached_DataObject::staticGet('User_openid_trustroot',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('User_openid_trustroot', $kv);
+ }
+
+ /**
+ * Get the TableDef object that represents the table backing this class
+ * @return TableDef TableDef instance
+ */
+ function tableDef()
+ {
+ return new TableDef($this->__table,
+ array(new ColumnDef('trustroot', 'varchar',
+ '255', false, 'PRI'),
+ new ColumnDef('user_id', 'integer',
+ null, false, 'PRI'),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'timestamp')));
+ }
+}
diff --git a/plugins/OpenID/doc-src/openid b/plugins/OpenID/doc-src/openid
new file mode 100644
index 000000000..c741e3674
--- /dev/null
+++ b/plugins/OpenID/doc-src/openid
@@ -0,0 +1,11 @@
+%%site.name%% supports the [OpenID](http://openid.net/) standard for single signon between Web sites. OpenID lets you log into many different Web sites without using a different password for each. (See [Wikipedia's OpenID article](http://en.wikipedia.org/wiki/OpenID) for more information.)
+
+If you already have an account on %%site.name%%, you can [login](%%action.login%%) with your username and password as usual.
+To use OpenID in the future, you can [add an OpenID to your account](%%action.openidsettings%%) after you have logged in normally.
+
+There are many [Public OpenID providers](http://wiki.openid.net/Public_OpenID_providers), and you may already have an OpenID-enabled account on another service.
+
+* On wikis: If you have an account on an OpenID-enabled wiki, like [Wikitravel](http://wikitravel.org/), [wikiHow](http://www.wikihow.com/), [Vinismo](http://vinismo.com/), [AboutUs](http://aboutus.org/) or [Keiki](http://kei.ki/), you can log in to %%site.name%% by entering the **full URL** of your user page on that other wiki in the box above. For example, *http://kei.ki/en/User:Evan*.
+* [Yahoo!](http://openid.yahoo.com/) : If you have an account with Yahoo!, you can log in to this site by entering your Yahoo!-provided OpenID in the box above. Yahoo! OpenID URLs have the form *https://me.yahoo.com/yourusername*.
+* [AOL](http://dev.aol.com/aol-and-63-million-openids) : If you have an account with [AOL](http://www.aol.com/), like an [AIM](http://www.aim.com/) account, you can log in to %%site.name%% by entering your AOL-provided OpenID in the box above. AOL OpenID URLs have the form *http://openid.aol.com/yourusername*. Your username should be all lowercase, no spaces.
+* [Blogger](http://bloggerindraft.blogspot.com/2008/01/new-feature-blogger-as-openid-provider.html), [Wordpress.com](http://faq.wordpress.com/2007/03/06/what-is-openid/), [LiveJournal](http://www.livejournal.com/openid/about.bml), [Vox](http://bradfitz.vox.com/library/post/openid-for-vox.html) : If you have a blog on any of these services, enter your blog URL in the box above. For example, *http://yourusername.blogspot.com/*, *http://yourusername.wordpress.com/*, *http://yourusername.livejournal.com/*, or *http://yourusername.vox.com/*.
diff --git a/plugins/OpenID/finishaddopenid.php b/plugins/OpenID/finishaddopenid.php
new file mode 100644
index 000000000..6e889205d
--- /dev/null
+++ b/plugins/OpenID/finishaddopenid.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Complete adding an OpenID
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+
+/**
+ * Complete adding an OpenID
+ *
+ * Handle the return from an OpenID verification
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class FinishaddopenidAction extends Action
+{
+ var $msg = null;
+
+ /**
+ * Handle the redirect back from OpenID confirmation
+ *
+ * Check to see if the user's logged in, and then try
+ * to use the OpenID login system.
+ *
+ * @param array $args $_REQUEST arguments
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'));
+ } else {
+ $this->tryLogin();
+ }
+ }
+
+ /**
+ * Try to log in using OpenID
+ *
+ * Check the OpenID for validity; potentially store it.
+ *
+ * @return void
+ */
+
+ function tryLogin()
+ {
+ $consumer =& oid_consumer();
+
+ $response = $consumer->complete(common_local_url('finishaddopenid'));
+
+ if ($response->status == Auth_OpenID_CANCEL) {
+ $this->message(_('OpenID authentication cancelled.'));
+ return;
+ } else if ($response->status == Auth_OpenID_FAILURE) {
+ // Authentication failed; display the error message.
+ $this->message(sprintf(_('OpenID authentication failed: %s'),
+ $response->message));
+ } else if ($response->status == Auth_OpenID_SUCCESS) {
+
+ $display = $response->getDisplayIdentifier();
+ $canonical = ($response->endpoint && $response->endpoint->canonicalID) ?
+ $response->endpoint->canonicalID : $display;
+
+ $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
+
+ if ($sreg_resp) {
+ $sreg = $sreg_resp->contents();
+ }
+
+ $cur =& common_current_user();
+
+ $other = oid_get_user($canonical);
+
+ if ($other) {
+ if ($other->id == $cur->id) {
+ $this->message(_('You already have this OpenID!'));
+ } else {
+ $this->message(_('Someone else already has this OpenID.'));
+ }
+ return;
+ }
+
+ // start a transaction
+
+ $cur->query('BEGIN');
+
+ $result = oid_link_user($cur->id, $canonical, $display);
+
+ if (!$result) {
+ $this->message(_('Error connecting user.'));
+ return;
+ }
+ if ($sreg) {
+ if (!oid_update_user($cur, $sreg)) {
+ $this->message(_('Error updating profile'));
+ return;
+ }
+ }
+
+ // success!
+
+ $cur->query('COMMIT');
+
+ oid_set_last($display);
+
+ common_redirect(common_local_url('openidsettings'), 303);
+ }
+ }
+
+ /**
+ * Show a failure message
+ *
+ * Something went wrong. Save the message, and show the page.
+ *
+ * @param string $msg Error message to show
+ *
+ * @return void
+ */
+
+ function message($msg)
+ {
+ $this->message = $msg;
+ $this->showPage();
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title
+ */
+
+ function title()
+ {
+ return _('OpenID Login');
+ }
+
+ /**
+ * Show error message
+ *
+ * @return void
+ */
+
+ function showPageNotice()
+ {
+ if ($this->message) {
+ $this->element('p', 'error', $this->message);
+ }
+ }
+}
diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php
new file mode 100644
index 000000000..ff0b451d3
--- /dev/null
+++ b/plugins/OpenID/finishopenidlogin.php
@@ -0,0 +1,495 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+
+class FinishopenidloginAction extends Action
+{
+ var $error = null;
+ var $username = null;
+ var $message = null;
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if (common_is_real_login()) {
+ $this->clientError(_('Already logged in.'));
+ } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+ if ($this->arg('create')) {
+ if (!$this->boolean('license')) {
+ $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
+ $this->trimmed('newname'));
+ return;
+ }
+ $this->createNewUser();
+ } else if ($this->arg('connect')) {
+ $this->connectUser();
+ } else {
+ common_debug(print_r($this->args, true), __FILE__);
+ $this->showForm(_('Something weird happened.'),
+ $this->trimmed('newname'));
+ }
+ } else {
+ $this->tryLogin();
+ }
+ }
+
+ function showPageNotice()
+ {
+ if ($this->error) {
+ $this->element('div', array('class' => 'error'), $this->error);
+ } else {
+ $this->element('div', 'instructions',
+ sprintf(_('This is the first time you\'ve logged into %s so we must connect your OpenID to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
+ }
+ }
+
+ function title()
+ {
+ return _('OpenID Account Setup');
+ }
+
+ function showForm($error=null, $username=null)
+ {
+ $this->error = $error;
+ $this->username = $username;
+
+ $this->showPage();
+ }
+
+ function showContent()
+ {
+ if (!empty($this->message_text)) {
+ $this->element('div', array('class' => 'error'), $this->message_text);
+ return;
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'account_connect',
+ 'action' => common_local_url('finishopenidlogin')));
+ $this->hidden('token', common_session_token());
+ $this->element('h2', null,
+ _('Create new account'));
+ $this->element('p', null,
+ _('Create a new user with this nickname.'));
+ $this->input('newname', _('New nickname'),
+ ($this->username) ? $this->username : '',
+ _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+ $this->elementStart('p');
+ $this->element('input', array('type' => 'checkbox',
+ 'id' => 'license',
+ 'name' => 'license',
+ 'value' => 'true'));
+ $this->text(_('My text and files are available under '));
+ $this->element('a', array('href' => common_config('license', 'url')),
+ common_config('license', 'title'));
+ $this->text(_(' except this private data: password, email address, IM address, phone number.'));
+ $this->elementEnd('p');
+ $this->submit('create', _('Create'));
+ $this->element('h2', null,
+ _('Connect existing account'));
+ $this->element('p', null,
+ _('If you already have an account, login with your username and password to connect it to your OpenID.'));
+ $this->input('nickname', _('Existing nickname'));
+ $this->password('password', _('Password'));
+ $this->submit('connect', _('Connect'));
+ $this->elementEnd('form');
+ }
+
+ function tryLogin()
+ {
+ $consumer = oid_consumer();
+
+ $response = $consumer->complete(common_local_url('finishopenidlogin'));
+
+ if ($response->status == Auth_OpenID_CANCEL) {
+ $this->message(_('OpenID authentication cancelled.'));
+ return;
+ } else if ($response->status == Auth_OpenID_FAILURE) {
+ // Authentication failed; display the error message.
+ $this->message(sprintf(_('OpenID authentication failed: %s'), $response->message));
+ } else if ($response->status == Auth_OpenID_SUCCESS) {
+ // This means the authentication succeeded; extract the
+ // identity URL and Simple Registration data (if it was
+ // returned).
+ $display = $response->getDisplayIdentifier();
+ $canonical = ($response->endpoint->canonicalID) ?
+ $response->endpoint->canonicalID : $response->getDisplayIdentifier();
+
+ $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
+
+ if ($sreg_resp) {
+ $sreg = $sreg_resp->contents();
+ }
+
+ $user = oid_get_user($canonical);
+
+ if ($user) {
+ oid_set_last($display);
+ # XXX: commented out at @edd's request until better
+ # control over how data flows from OpenID provider.
+ # oid_update_user($user, $sreg);
+ common_set_user($user);
+ common_real_login(true);
+ if (isset($_SESSION['openid_rememberme']) && $_SESSION['openid_rememberme']) {
+ common_rememberme($user);
+ }
+ unset($_SESSION['openid_rememberme']);
+ $this->goHome($user->nickname);
+ } else {
+ $this->saveValues($display, $canonical, $sreg);
+ $this->showForm(null, $this->bestNewNickname($display, $sreg));
+ }
+ }
+ }
+
+ function message($msg)
+ {
+ $this->message_text = $msg;
+ $this->showPage();
+ }
+
+ function saveValues($display, $canonical, $sreg)
+ {
+ common_ensure_session();
+ $_SESSION['openid_display'] = $display;
+ $_SESSION['openid_canonical'] = $canonical;
+ $_SESSION['openid_sreg'] = $sreg;
+ }
+
+ function getSavedValues()
+ {
+ return array($_SESSION['openid_display'],
+ $_SESSION['openid_canonical'],
+ $_SESSION['openid_sreg']);
+ }
+
+ function createNewUser()
+ {
+ # FIXME: save invite code before redirect, and check here
+
+ if (common_config('site', 'closed')) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = null;
+
+ if (common_config('site', 'inviteonly')) {
+ $code = $_SESSION['invitecode'];
+ if (empty($code)) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = Invitation::staticGet($code);
+
+ if (empty($invite)) {
+ $this->clientError(_('Not a valid invitation code.'));
+ return;
+ }
+ }
+
+ $nickname = $this->trimmed('newname');
+
+ if (!Validate::string($nickname, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
+ return;
+ }
+
+ if (!User::allowed_nickname($nickname)) {
+ $this->showForm(_('Nickname not allowed.'));
+ return;
+ }
+
+ if (User::staticGet('nickname', $nickname)) {
+ $this->showForm(_('Nickname already in use. Try another one.'));
+ return;
+ }
+
+ list($display, $canonical, $sreg) = $this->getSavedValues();
+
+ if (!$display || !$canonical) {
+ $this->serverError(_('Stored OpenID not found.'));
+ return;
+ }
+
+ # Possible race condition... let's be paranoid
+
+ $other = oid_get_user($canonical);
+
+ if ($other) {
+ $this->serverError(_('Creating new account for OpenID that already has a user.'));
+ return;
+ }
+
+ $location = '';
+ if (!empty($sreg['country'])) {
+ if ($sreg['postcode']) {
+ # XXX: use postcode to get city and region
+ # XXX: also, store postcode somewhere -- it's valuable!
+ $location = $sreg['postcode'] . ', ' . $sreg['country'];
+ } else {
+ $location = $sreg['country'];
+ }
+ }
+
+ if (!empty($sreg['fullname']) && mb_strlen($sreg['fullname']) <= 255) {
+ $fullname = $sreg['fullname'];
+ } else {
+ $fullname = '';
+ }
+
+ if (!empty($sreg['email']) && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
+ $email = $sreg['email'];
+ } else {
+ $email = '';
+ }
+
+ # XXX: add language
+ # XXX: add timezone
+
+ $args = array('nickname' => $nickname,
+ 'email' => $email,
+ 'fullname' => $fullname,
+ 'location' => $location);
+
+ if (!empty($invite)) {
+ $args['code'] = $invite->code;
+ }
+
+ $user = User::register($args);
+
+ $result = oid_link_user($user->id, $canonical, $display);
+
+ oid_set_last($display);
+ common_set_user($user);
+ common_real_login(true);
+ if (isset($_SESSION['openid_rememberme']) && $_SESSION['openid_rememberme']) {
+ common_rememberme($user);
+ }
+ unset($_SESSION['openid_rememberme']);
+ common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
+ 303);
+ }
+
+ function connectUser()
+ {
+ $nickname = $this->trimmed('nickname');
+ $password = $this->trimmed('password');
+
+ if (!common_check_user($nickname, $password)) {
+ $this->showForm(_('Invalid username or password.'));
+ return;
+ }
+
+ # They're legit!
+
+ $user = User::staticGet('nickname', $nickname);
+
+ list($display, $canonical, $sreg) = $this->getSavedValues();
+
+ if (!$display || !$canonical) {
+ $this->serverError(_('Stored OpenID not found.'));
+ return;
+ }
+
+ $result = oid_link_user($user->id, $canonical, $display);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to OpenID.'));
+ return;
+ }
+
+ oid_update_user($user, $sreg);
+ oid_set_last($display);
+ common_set_user($user);
+ common_real_login(true);
+ if (isset($_SESSION['openid_rememberme']) && $_SESSION['openid_rememberme']) {
+ common_rememberme($user);
+ }
+ unset($_SESSION['openid_rememberme']);
+ $this->goHome($user->nickname);
+ }
+
+ function goHome($nickname)
+ {
+ $url = common_get_returnto();
+ if ($url) {
+ # We don't have to return to it again
+ common_set_returnto(null);
+ } else {
+ $url = common_local_url('all',
+ array('nickname' =>
+ $nickname));
+ }
+ common_redirect($url, 303);
+ }
+
+ function bestNewNickname($display, $sreg)
+ {
+
+ # Try the passed-in nickname
+
+ if (!empty($sreg['nickname'])) {
+ $nickname = $this->nicknamize($sreg['nickname']);
+ if ($this->isNewNickname($nickname)) {
+ return $nickname;
+ }
+ }
+
+ # Try the full name
+
+ if (!empty($sreg['fullname'])) {
+ $fullname = $this->nicknamize($sreg['fullname']);
+ if ($this->isNewNickname($fullname)) {
+ return $fullname;
+ }
+ }
+
+ # Try the URL
+
+ $from_url = $this->openidToNickname($display);
+
+ if ($from_url && $this->isNewNickname($from_url)) {
+ return $from_url;
+ }
+
+ # XXX: others?
+
+ return null;
+ }
+
+ function isNewNickname($str)
+ {
+ if (!Validate::string($str, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ return false;
+ }
+ if (!User::allowed_nickname($str)) {
+ return false;
+ }
+ if (User::staticGet('nickname', $str)) {
+ return false;
+ }
+ return true;
+ }
+
+ function openidToNickname($openid)
+ {
+ if (Auth_Yadis_identifierScheme($openid) == 'XRI') {
+ return $this->xriToNickname($openid);
+ } else {
+ return $this->urlToNickname($openid);
+ }
+ }
+
+ # We try to use an OpenID URL as a legal StatusNet user name in this order
+ # 1. Plain hostname, like http://evanp.myopenid.com/
+ # 2. One element in path, like http://profile.typekey.com/EvanProdromou/
+ # or http://getopenid.com/evanprodromou
+
+ function urlToNickname($openid)
+ {
+ static $bad = array('query', 'user', 'password', 'port', 'fragment');
+
+ $parts = parse_url($openid);
+
+ # If any of these parts exist, this won't work
+
+ foreach ($bad as $badpart) {
+ if (array_key_exists($badpart, $parts)) {
+ return null;
+ }
+ }
+
+ # We just have host and/or path
+
+ # If it's just a host...
+ if (array_key_exists('host', $parts) &&
+ (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
+ {
+ $hostparts = explode('.', $parts['host']);
+
+ # Try to catch common idiom of nickname.service.tld
+
+ if ((count($hostparts) > 2) &&
+ (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
+ (strcmp($hostparts[0], 'www') != 0))
+ {
+ return $this->nicknamize($hostparts[0]);
+ } else {
+ # Do the whole hostname
+ return $this->nicknamize($parts['host']);
+ }
+ } else {
+ if (array_key_exists('path', $parts)) {
+ # Strip starting, ending slashes
+ $path = preg_replace('@/$@', '', $parts['path']);
+ $path = preg_replace('@^/@', '', $path);
+ if (strpos($path, '/') === false) {
+ return $this->nicknamize($path);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function xriToNickname($xri)
+ {
+ $base = $this->xriBase($xri);
+
+ if (!$base) {
+ return null;
+ } else {
+ # =evan.prodromou
+ # or @gratis*evan.prodromou
+ $parts = explode('*', substr($base, 1));
+ return $this->nicknamize(array_pop($parts));
+ }
+ }
+
+ function xriBase($xri)
+ {
+ if (substr($xri, 0, 6) == 'xri://') {
+ return substr($xri, 6);
+ } else {
+ return $xri;
+ }
+ }
+
+ # Given a string, try to make it work as a nickname
+
+ function nicknamize($str)
+ {
+ $str = preg_replace('/\W/', '', $str);
+ return strtolower($str);
+ }
+}
diff --git a/plugins/OpenID/openid.php b/plugins/OpenID/openid.php
new file mode 100644
index 000000000..dd628e773
--- /dev/null
+++ b/plugins/OpenID/openid.php
@@ -0,0 +1,293 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+require_once(INSTALLDIR.'/plugins/OpenID/User_openid.php');
+
+require_once('Auth/OpenID.php');
+require_once('Auth/OpenID/Consumer.php');
+require_once('Auth/OpenID/Server.php');
+require_once('Auth/OpenID/SReg.php');
+require_once('Auth/OpenID/MySQLStore.php');
+
+# About one year cookie expiry
+
+define('OPENID_COOKIE_EXPIRY', round(365.25 * 24 * 60 * 60));
+define('OPENID_COOKIE_KEY', 'lastusedopenid');
+
+function oid_store()
+{
+ static $store = null;
+ if (!$store) {
+ # Can't be called statically
+ $user = new User();
+ $conn = $user->getDatabaseConnection();
+ $store = new Auth_OpenID_MySQLStore($conn);
+ }
+ return $store;
+}
+
+function oid_consumer()
+{
+ $store = oid_store();
+ $consumer = new Auth_OpenID_Consumer($store);
+ return $consumer;
+}
+
+function oid_server()
+{
+ $store = oid_store();
+ $server = new Auth_OpenID_Server($store, common_local_url('openidserver'));
+ return $server;
+}
+
+function oid_clear_last()
+{
+ oid_set_last('');
+}
+
+function oid_set_last($openid_url)
+{
+ common_set_cookie(OPENID_COOKIE_KEY,
+ $openid_url,
+ time() + OPENID_COOKIE_EXPIRY);
+}
+
+function oid_get_last()
+{
+ if (empty($_COOKIE[OPENID_COOKIE_KEY])) {
+ return null;
+ }
+ $openid_url = $_COOKIE[OPENID_COOKIE_KEY];
+ if ($openid_url && strlen($openid_url) > 0) {
+ return $openid_url;
+ } else {
+ return null;
+ }
+}
+
+function oid_link_user($id, $canonical, $display)
+{
+
+ $oid = new User_openid();
+ $oid->user_id = $id;
+ $oid->canonical = $canonical;
+ $oid->display = $display;
+ $oid->created = DB_DataObject_Cast::dateTime();
+
+ if (!$oid->insert()) {
+ $err = PEAR::getStaticProperty('DB_DataObject','lastError');
+ common_debug('DB error ' . $err->code . ': ' . $err->message, __FILE__);
+ return false;
+ }
+
+ return true;
+}
+
+function oid_get_user($openid_url)
+{
+ $user = null;
+ $oid = User_openid::staticGet('canonical', $openid_url);
+ if ($oid) {
+ $user = User::staticGet('id', $oid->user_id);
+ }
+ return $user;
+}
+
+function oid_check_immediate($openid_url, $backto=null)
+{
+ if (!$backto) {
+ $action = $_REQUEST['action'];
+ $args = common_copy_args($_GET);
+ unset($args['action']);
+ $backto = common_local_url($action, $args);
+ }
+ common_debug('going back to "' . $backto . '"', __FILE__);
+
+ common_ensure_session();
+
+ $_SESSION['openid_immediate_backto'] = $backto;
+ common_debug('passed-in variable is "' . $backto . '"', __FILE__);
+ common_debug('session variable is "' . $_SESSION['openid_immediate_backto'] . '"', __FILE__);
+
+ oid_authenticate($openid_url,
+ 'finishimmediate',
+ true);
+}
+
+function oid_authenticate($openid_url, $returnto, $immediate=false)
+{
+
+ $consumer = oid_consumer();
+
+ if (!$consumer) {
+ common_server_error(_('Cannot instantiate OpenID consumer object.'));
+ return false;
+ }
+
+ common_ensure_session();
+
+ $auth_request = $consumer->begin($openid_url);
+
+ // Handle failure status return values.
+ if (!$auth_request) {
+ return _('Not a valid OpenID.');
+ } else if (Auth_OpenID::isFailure($auth_request)) {
+ return sprintf(_('OpenID failure: %s'), $auth_request->message);
+ }
+
+ $sreg_request = Auth_OpenID_SRegRequest::build(// Required
+ array(),
+ // Optional
+ array('nickname',
+ 'email',
+ 'fullname',
+ 'language',
+ 'timezone',
+ 'postcode',
+ 'country'));
+
+ if ($sreg_request) {
+ $auth_request->addExtension($sreg_request);
+ }
+
+ $trust_root = common_root_url(true);
+ $process_url = common_local_url($returnto);
+
+ if ($auth_request->shouldSendRedirect()) {
+ $redirect_url = $auth_request->redirectURL($trust_root,
+ $process_url,
+ $immediate);
+ if (!$redirect_url) {
+ } else if (Auth_OpenID::isFailure($redirect_url)) {
+ return sprintf(_('Could not redirect to server: %s'), $redirect_url->message);
+ } else {
+ common_redirect($redirect_url, 303);
+ }
+ } else {
+ // Generate form markup and render it.
+ $form_id = 'openid_message';
+ $form_html = $auth_request->formMarkup($trust_root, $process_url,
+ $immediate, array('id' => $form_id));
+
+ # XXX: This is cheap, but things choke if we don't escape ampersands
+ # in the HTML attributes
+
+ $form_html = preg_replace('/&/', '&amp;', $form_html);
+
+ // Display an error if the form markup couldn't be generated;
+ // otherwise, render the HTML.
+ if (Auth_OpenID::isFailure($form_html)) {
+ common_server_error(sprintf(_('Could not create OpenID form: %s'), $form_html->message));
+ } else {
+ $action = new AutosubmitAction(); // see below
+ $action->form_html = $form_html;
+ $action->form_id = $form_id;
+ $action->prepare(array('action' => 'autosubmit'));
+ $action->handle(array('action' => 'autosubmit'));
+ }
+ }
+}
+
+# Half-assed attempt at a module-private function
+
+function _oid_print_instructions()
+{
+ common_element('div', 'instructions',
+ _('This form should automatically submit itself. '.
+ 'If not, click the submit button to go to your '.
+ 'OpenID provider.'));
+}
+
+# update a user from sreg parameters
+
+function oid_update_user(&$user, &$sreg)
+{
+
+ $profile = $user->getProfile();
+
+ $orig_profile = clone($profile);
+
+ if ($sreg['fullname'] && strlen($sreg['fullname']) <= 255) {
+ $profile->fullname = $sreg['fullname'];
+ }
+
+ if ($sreg['country']) {
+ if ($sreg['postcode']) {
+ # XXX: use postcode to get city and region
+ # XXX: also, store postcode somewhere -- it's valuable!
+ $profile->location = $sreg['postcode'] . ', ' . $sreg['country'];
+ } else {
+ $profile->location = $sreg['country'];
+ }
+ }
+
+ # XXX save language if it's passed
+ # XXX save timezone if it's passed
+
+ if (!$profile->update($orig_profile)) {
+ common_server_error(_('Error saving the profile.'));
+ return false;
+ }
+
+ $orig_user = clone($user);
+
+ if ($sreg['email'] && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
+ $user->email = $sreg['email'];
+ }
+
+ if (!$user->update($orig_user)) {
+ common_server_error(_('Error saving the user.'));
+ return false;
+ }
+
+ return true;
+}
+
+class AutosubmitAction extends Action
+{
+ var $form_html = null;
+ var $form_id = null;
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $this->showPage();
+ }
+
+ function title()
+ {
+ return _('OpenID Auto-Submit');
+ }
+
+ function showContent()
+ {
+ $this->raw($this->form_html);
+ }
+
+ function showScripts()
+ {
+ parent::showScripts();
+ $this->element('script', null,
+ '$(document).ready(function() { ' .
+ ' $(\'#'. $this->form_id .'\').submit(); '.
+ '});');
+ }
+}
diff --git a/plugins/OpenID/openidlogin.php b/plugins/OpenID/openidlogin.php
new file mode 100644
index 000000000..29e89234e
--- /dev/null
+++ b/plugins/OpenID/openidlogin.php
@@ -0,0 +1,137 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+
+class OpenidloginAction extends Action
+{
+ function handle($args)
+ {
+ parent::handle($args);
+ if (common_is_real_login()) {
+ $this->clientError(_('Already logged in.'));
+ } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $openid_url = $this->trimmed('openid_url');
+
+ # CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. Try again, please.'), $openid_url);
+ return;
+ }
+
+ $rememberme = $this->boolean('rememberme');
+
+ common_ensure_session();
+
+ $_SESSION['openid_rememberme'] = $rememberme;
+
+ $result = oid_authenticate($openid_url,
+ 'finishopenidlogin');
+
+ if (is_string($result)) { # error message
+ unset($_SESSION['openid_rememberme']);
+ $this->showForm($result, $openid_url);
+ }
+ } else {
+ $openid_url = oid_get_last();
+ $this->showForm(null, $openid_url);
+ }
+ }
+
+ function getInstructions()
+ {
+ if (common_logged_in() && !common_is_real_login() &&
+ common_get_returnto()) {
+ // rememberme logins have to reauthenticate before
+ // changing any profile settings (cookie-stealing protection)
+ return _('For security reasons, please re-login with your ' .
+ '[OpenID](%%doc.openid%%) ' .
+ 'before changing your settings.');
+ } else {
+ return _('Login with an [OpenID](%%doc.openid%%) account.');
+ }
+ }
+
+ function showPageNotice()
+ {
+ if ($this->error) {
+ $this->element('div', array('class' => 'error'), $this->error);
+ } else {
+ $instr = $this->getInstructions();
+ $output = common_markup_to_html($instr);
+ $this->elementStart('div', 'instructions');
+ $this->raw($output);
+ $this->elementEnd('div');
+ }
+ }
+
+ function showScripts()
+ {
+ parent::showScripts();
+ $this->autofocus('openid_url');
+ }
+
+ function title()
+ {
+ return _('OpenID Login');
+ }
+
+ function showForm($error=null, $openid_url)
+ {
+ $this->error = $error;
+ $this->openid_url = $openid_url;
+ $this->showPage();
+ }
+
+ function showContent() {
+ $formaction = common_local_url('openidlogin');
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_openid_login',
+ 'class' => 'form_settings',
+ 'action' => $formaction));
+ $this->elementStart('fieldset');
+ $this->element('legend', null, _('OpenID login'));
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('openid_url', _('OpenID URL'),
+ $this->openid_url,
+ _('Your OpenID URL'));
+ $this->elementEnd('li');
+ $this->elementStart('li', array('id' => 'settings_rememberme'));
+ $this->checkbox('rememberme', _('Remember me'), false,
+ _('Automatically login in the future; ' .
+ 'not for shared computers!'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('submit', _('Login'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function showLocalNav()
+ {
+ $nav = new LoginGroupNav($this);
+ $nav->show();
+ }
+}
diff --git a/plugins/OpenID/openidserver.php b/plugins/OpenID/openidserver.php
new file mode 100644
index 000000000..dab97c93e
--- /dev/null
+++ b/plugins/OpenID/openidserver.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Settings for OpenID
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/action.php';
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php');
+
+/**
+ * Settings for OpenID
+ *
+ * Lets users add, edit and delete OpenIDs from their account
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class OpenidserverAction extends Action
+{
+ var $oserver;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->oserver = oid_server();
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ $request = $this->oserver->decodeRequest();
+ if (in_array($request->mode, array('checkid_immediate',
+ 'checkid_setup'))) {
+ $user = common_current_user();
+ if(!$user){
+ if($request->immediate){
+ //cannot prompt the user to login in immediate mode, so answer false
+ $response = $this->generateDenyResponse($request);
+ }else{
+ /* Go log in, and then come back. */
+ common_set_returnto($_SERVER['REQUEST_URI']);
+ common_redirect(common_local_url('login'));
+ return;
+ }
+ }else if(common_profile_url($user->nickname) == $request->identity || $request->idSelect()){
+ $user_openid_trustroot = User_openid_trustroot::pkeyGet(
+ array('user_id'=>$user->id, 'trustroot'=>$request->trust_root));
+ if(empty($user_openid_trustroot)){
+ if($request->immediate){
+ //cannot prompt the user to trust this trust root in immediate mode, so answer false
+ $response = $this->generateDenyResponse($request);
+ }else{
+ common_ensure_session();
+ $_SESSION['openid_trust_root'] = $request->trust_root;
+ $allowResponse = $this->generateAllowResponse($request, $user);
+ $this->oserver->encodeResponse($allowResponse); //sign the response
+ $denyResponse = $this->generateDenyResponse($request);
+ $this->oserver->encodeResponse($denyResponse); //sign the response
+ $_SESSION['openid_allow_url'] = $allowResponse->encodeToUrl();
+ $_SESSION['openid_deny_url'] = $denyResponse->encodeToUrl();
+ //ask the user to trust this trust root
+ common_redirect(common_local_url('openidtrust'));
+ return;
+ }
+ }else{
+ //user has previously authorized this trust root
+ $response = $this->generateAllowResponse($request, $user);
+ //$response = $request->answer(true, null, common_profile_url($user->nickname));
+ }
+ } else if ($request->immediate) {
+ $response = $this->generateDenyResponse($request);
+ } else {
+ //invalid
+ $this->clientError(sprintf(_('You are not authorized to use the identity %s'),$request->identity),$code=403);
+ }
+ } else {
+ $response = $this->oserver->handleRequest($request);
+ }
+
+ if($response){
+ $response = $this->oserver->encodeResponse($response);
+ if ($response->code != AUTH_OPENID_HTTP_OK) {
+ header(sprintf("HTTP/1.1 %d ", $response->code),
+ true, $response->code);
+ }
+
+ if($response->headers){
+ foreach ($response->headers as $k => $v) {
+ header("$k: $v");
+ }
+ }
+ $this->raw($response->body);
+ }else{
+ $this->clientError(_('Just an OpenID provider. Nothing to see here, move along...'),$code=500);
+ }
+ }
+
+ function generateAllowResponse($request, $user){
+ $response = $request->answer(true, null, common_profile_url($user->nickname));
+
+ $profile = $user->getProfile();
+ $sreg_data = array(
+ 'fullname' => $profile->fullname,
+ 'nickname' => $user->nickname,
+ 'email' => $user->email,
+ 'language' => $user->language,
+ 'timezone' => $user->timezone);
+ $sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest($request);
+ $sreg_response = Auth_OpenID_SRegResponse::extractResponse(
+ $sreg_request, $sreg_data);
+ $sreg_response->toMessage($response->fields);
+ return $response;
+ }
+
+ function generateDenyResponse($request){
+ $response = $request->answer(false);
+ return $response;
+ }
+}
diff --git a/plugins/OpenID/openidsettings.php b/plugins/OpenID/openidsettings.php
new file mode 100644
index 000000000..3ad46f5f5
--- /dev/null
+++ b/plugins/OpenID/openidsettings.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Settings for OpenID
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/accountsettingsaction.php';
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+
+/**
+ * Settings for OpenID
+ *
+ * Lets users add, edit and delete OpenIDs from their account
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class OpenidsettingsAction extends AccountSettingsAction
+{
+ /**
+ * Title of the page
+ *
+ * @return string Page title
+ */
+
+ function title()
+ {
+ return _('OpenID settings');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return string Instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('[OpenID](%%doc.openid%%) lets you log into many sites' .
+ ' with the same user account.'.
+ ' Manage your associated OpenIDs from here.');
+ }
+
+ function showScripts()
+ {
+ parent::showScripts();
+ $this->autofocus('openid_url');
+ }
+
+ /**
+ * Show the form for OpenID management
+ *
+ * We have one form with a few different submit buttons to do different things.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $user = common_current_user();
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_openid_add',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('openidsettings')));
+ $this->elementStart('fieldset', array('id' => 'settings_openid_add'));
+ $this->element('legend', null, _('Add OpenID'));
+ $this->hidden('token', common_session_token());
+ $this->element('p', 'form_guide',
+ _('If you want to add an OpenID to your account, ' .
+ 'enter it in the box below and click "Add".'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->element('label', array('for' => 'openid_url'),
+ _('OpenID URL'));
+ $this->element('input', array('name' => 'openid_url',
+ 'type' => 'text',
+ 'id' => 'openid_url'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->element('input', array('type' => 'submit',
+ 'id' => 'settings_openid_add_action-submit',
+ 'name' => 'add',
+ 'class' => 'submit',
+ 'value' => _('Add')));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+
+ $oid = new User_openid();
+
+ $oid->user_id = $user->id;
+
+ $cnt = $oid->find();
+
+ if ($cnt > 0) {
+
+ $this->element('h2', null, _('Remove OpenID'));
+
+ if ($cnt == 1 && !$user->password) {
+
+ $this->element('p', 'form_guide',
+ _('Removing your only OpenID '.
+ 'would make it impossible to log in! ' .
+ 'If you need to remove it, '.
+ 'add another OpenID first.'));
+
+ if ($oid->fetch()) {
+ $this->elementStart('p');
+ $this->element('a', array('href' => $oid->canonical),
+ $oid->display);
+ $this->elementEnd('p');
+ }
+
+ } else {
+
+ $this->element('p', 'form_guide',
+ _('You can remove an OpenID from your account '.
+ 'by clicking the button marked "Remove".'));
+ $idx = 0;
+
+ while ($oid->fetch()) {
+ $this->elementStart('form',
+ array('method' => 'POST',
+ 'id' => 'form_settings_openid_delete' . $idx,
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('openidsettings')));
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->element('a', array('href' => $oid->canonical),
+ $oid->display);
+ $this->element('input', array('type' => 'hidden',
+ 'id' => 'openid_url'.$idx,
+ 'name' => 'openid_url',
+ 'value' => $oid->canonical));
+ $this->element('input', array('type' => 'submit',
+ 'id' => 'remove'.$idx,
+ 'name' => 'remove',
+ 'class' => 'submit remove',
+ 'value' => _('Remove')));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ $idx++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle a POST request
+ *
+ * Muxes to different sub-functions based on which button was pushed
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('add')) {
+ $result = oid_authenticate($this->trimmed('openid_url'),
+ 'finishaddopenid');
+ if (is_string($result)) { // error message
+ $this->showForm($result);
+ }
+ } else if ($this->arg('remove')) {
+ $this->removeOpenid();
+ } else {
+ $this->showForm(_('Something weird happened.'));
+ }
+ }
+
+ /**
+ * Handles a request to remove an OpenID from the user's account
+ *
+ * Validates input and, if everything is OK, deletes the OpenID.
+ * Reloads the form with a success or error notification.
+ *
+ * @return void
+ */
+
+ function removeOpenid()
+ {
+ $openid_url = $this->trimmed('openid_url');
+
+ $oid = User_openid::staticGet('canonical', $openid_url);
+
+ if (!$oid) {
+ $this->showForm(_('No such OpenID.'));
+ return;
+ }
+ $cur = common_current_user();
+ if (!$cur || $oid->user_id != $cur->id) {
+ $this->showForm(_('That OpenID does not belong to you.'));
+ return;
+ }
+ $oid->delete();
+ $this->showForm(_('OpenID removed.'), true);
+ return;
+ }
+}
diff --git a/plugins/OpenID/openidtrust.php b/plugins/OpenID/openidtrust.php
new file mode 100644
index 000000000..29c7bdc23
--- /dev/null
+++ b/plugins/OpenID/openidtrust.php
@@ -0,0 +1,142 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/plugins/OpenID/openid.php';
+require_once(INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php');
+
+class OpenidtrustAction extends Action
+{
+ var $trust_root;
+ var $allowUrl;
+ var $denyUrl;
+ var $user;
+
+ /**
+ * Is this a read-only action?
+ *
+ * @return boolean false
+ */
+
+ function isReadOnly($args)
+ {
+ return false;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title of the page
+ */
+
+ function title()
+ {
+ return _('OpenID Identity Verification');
+ }
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ common_ensure_session();
+ $this->user = common_current_user();
+ if(empty($this->user)){
+ /* Go log in, and then come back. */
+ common_set_returnto($_SERVER['REQUEST_URI']);
+ common_redirect(common_local_url('login'));
+ return;
+ }
+ $this->trust_root = $_SESSION['openid_trust_root'];
+ $this->allowUrl = $_SESSION['openid_allow_url'];
+ $this->denyUrl = $_SESSION['openid_deny_url'];
+ if(empty($this->trust_root) || empty($this->allowUrl) || empty($this->denyUrl)){
+ $this->clientError(_('This page should only be reached during OpenID processing, not directly.'));
+ return;
+ }
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if($_SERVER['REQUEST_METHOD'] == 'POST'){
+ $this->handleSubmit();
+ }else{
+ $this->showPage();
+ }
+ }
+
+ function handleSubmit()
+ {
+ unset($_SESSION['openid_trust_root']);
+ unset($_SESSION['openid_allow_url']);
+ unset($_SESSION['openid_deny_url']);
+ if($this->arg('allow'))
+ {
+ //save to database
+ $user_openid_trustroot = new User_openid_trustroot();
+ $user_openid_trustroot->user_id = $this->user->id;
+ $user_openid_trustroot->trustroot = $this->trust_root;
+ $user_openid_trustroot->created = DB_DataObject_Cast::dateTime();
+ if (!$user_openid_trustroot->insert()) {
+ $err = PEAR::getStaticProperty('DB_DataObject','lastError');
+ common_debug('DB error ' . $err->code . ': ' . $err->message, __FILE__);
+ }
+ common_redirect($this->allowUrl, $code=302);
+ }else{
+ common_redirect($this->denyUrl, $code=302);
+ }
+ }
+
+ /**
+ * Show page notice
+ *
+ * Display a notice for how to use the page, or the
+ * error if it exists.
+ *
+ * @return void
+ */
+
+ function showPageNotice()
+ {
+ $this->element('p',null,sprintf(_('%s has asked to verify your identity. Click Continue to verify your identity and login without creating a new password.'),$this->trust_root));
+ }
+
+ /**
+ * Core of the display code
+ *
+ * Shows the login form.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_openidtrust',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('openidtrust')));
+ $this->elementStart('fieldset');
+ $this->submit('allow', _('Continue'));
+ $this->submit('deny', _('Cancel'));
+
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+}
diff --git a/plugins/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php
new file mode 100644
index 000000000..ba87b266a
--- /dev/null
+++ b/plugins/Orbited/OrbitedPlugin.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to do "real time" updates using Orbited + STOMP
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php';
+
+/**
+ * Plugin to do realtime updates using Orbited + STOMP
+ *
+ * This plugin pushes data to a STOMP server which is then served to the
+ * browser by the Orbited server.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class OrbitedPlugin extends RealtimePlugin
+{
+ public $webserver = null;
+ public $webport = null;
+ public $channelbase = null;
+ public $stompserver = null;
+ public $stompport = null;
+ public $username = null;
+ public $password = null;
+ public $webuser = null;
+ public $webpass = null;
+
+ protected $con = null;
+
+ function onStartShowHeadElements($action)
+ {
+ // See http://orbited.org/wiki/Deployment#Cross-SubdomainDeployment
+ $action->element('script', null, ' document.domain = document.domain; ');
+ }
+
+ function _getScripts()
+ {
+ $scripts = parent::_getScripts();
+
+ $port = (is_null($this->webport)) ? 8000 : $this->webport;
+
+ $server = (is_null($this->webserver)) ? common_config('site', 'server') : $this->webserver;
+
+ $root = 'http://'.$server.(($port == 80) ? '':':'.$port);
+
+ $scripts[] = $root.'/static/Orbited.js';
+ $scripts[] = common_path('plugins/Orbited/orbitedextra.js');
+ $scripts[] = $root.'/static/protocols/stomp/stomp.js';
+ $scripts[] = common_path('plugins/Orbited/orbitedupdater.js');
+
+ return $scripts;
+ }
+
+ function _updateInitialize($timeline, $user_id)
+ {
+ $script = parent::_updateInitialize($timeline, $user_id);
+
+ $server = $this->_getStompServer();
+ $port = $this->_getStompPort();
+
+ return $script." OrbitedUpdater.init(\"$server\", $port, ".
+ "\"{$timeline}\", \"{$this->webuser}\", \"{$this->webpass}\");";
+ }
+
+ function _connect()
+ {
+ require_once(INSTALLDIR.'/extlib/Stomp.php');
+
+ $url = $this->_getStompUrl();
+
+ $this->con = new Stomp($url);
+
+ if ($this->con->connect($this->username, $this->password)) {
+ $this->log(LOG_INFO, "Connected.");
+ } else {
+ $this->log(LOG_ERR, 'Failed to connect to queue server');
+ throw new ServerException('Failed to connect to queue server');
+ }
+ }
+
+ function _publish($channel, $message)
+ {
+ $result = $this->con->send($channel,
+ json_encode($message));
+
+ return $result;
+ // TODO: parse and deal with result
+ }
+
+ function _disconnect()
+ {
+ $this->con->disconnect();
+ }
+
+ function _pathToChannel($path)
+ {
+ if (!empty($this->channelbase)) {
+ array_unshift($path, $this->channelbase);
+ }
+ return '/' . implode('/', $path);
+ }
+
+ function _getStompServer()
+ {
+ return (!is_null($this->stompserver)) ? $this->stompserver :
+ (!is_null($this->webserver)) ? $this->webserver :
+ common_config('site', 'server');
+ }
+
+ function _getStompPort()
+ {
+ return (!is_null($this->stompport)) ? $this->stompport : 61613;
+ }
+
+ function _getStompUrl()
+ {
+ $server = $this->_getStompServer();
+ $port = $this->_getStompPort();
+ return "tcp://$server:$port/";
+ }
+}
diff --git a/plugins/Orbited/orbitedextra.js b/plugins/Orbited/orbitedextra.js
new file mode 100644
index 000000000..47e5c0c80
--- /dev/null
+++ b/plugins/Orbited/orbitedextra.js
@@ -0,0 +1,2 @@
+TCPSocket = Orbited.TCPSocket;
+
diff --git a/plugins/Orbited/orbitedupdater.js b/plugins/Orbited/orbitedupdater.js
new file mode 100644
index 000000000..8c5ab3b73
--- /dev/null
+++ b/plugins/Orbited/orbitedupdater.js
@@ -0,0 +1,24 @@
+// Update the local timeline from a Orbited server
+
+var OrbitedUpdater = function()
+{
+ return {
+
+ init: function(server, port, timeline, username, password)
+ {
+ // set up stomp client.
+ stomp = new STOMPClient();
+
+ stomp.onmessageframe = function(frame) {
+ RealtimeUpdate.receive(JSON.parse(frame.body));
+ };
+
+ stomp.onconnectedframe = function() {
+ stomp.subscribe(timeline);
+ }
+
+ stomp.connect(server, port, username, password);
+ }
+ }
+}();
+
diff --git a/plugins/PtitUrl/PtitUrlPlugin.php b/plugins/PtitUrl/PtitUrlPlugin.php
new file mode 100644
index 000000000..ef453e96d
--- /dev/null
+++ b/plugins/PtitUrl/PtitUrlPlugin.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
+
+class PtitUrlPlugin extends UrlShortenerPlugin
+{
+ public $serviceUrl;
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->serviceUrl)){
+ throw new Exception("must specify a serviceUrl");
+ }
+ }
+
+ protected function shorten($url)
+ {
+ $response = $this->http_get(sprintf($this->serviceUrl,urlencode($url)));
+ if (!$response) return;
+ $response = $this->tidy($response);
+ $y = @simplexml_load_string($response);
+ if (!isset($y->body)) return;
+ $xml = $y->body->center->table->tr->td->pre->a->attributes();
+ if (isset($xml['href'])) return $xml['href'];
+ }
+}
+
diff --git a/plugins/PubSubHubBub/PubSubHubBubPlugin.php b/plugins/PubSubHubBub/PubSubHubBubPlugin.php
new file mode 100644
index 000000000..d15a869cb
--- /dev/null
+++ b/plugins/PubSubHubBub/PubSubHubBubPlugin.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+define('DEFAULT_HUB','http://pubsubhubbub.appspot.com');
+
+require_once(INSTALLDIR.'/plugins/PubSubHubBub/publisher.php');
+
+class PubSubHubBubPlugin extends Plugin
+{
+ private $hub;
+
+ function __construct()
+ {
+ parent::__construct();
+ }
+
+ function onInitializePlugin(){
+ $this->hub = common_config('PubSubHubBub', 'hub');
+ if(empty($this->hub)){
+ $this->hub = DEFAULT_HUB;
+ }
+ }
+
+ function onStartApiAtom($action){
+ $action->element('link',array('rel'=>'hub','href'=>$this->hub),null);
+ }
+
+ function onStartApiRss($action){
+ $action->element('atom:link',array('rel'=>'hub','href'=>$this->hub),null);
+ }
+
+ function onHandleQueuedNotice($notice){
+ $publisher = new Publisher($this->hub);
+
+ $feeds = array();
+
+ //public timeline feeds
+ $feeds[]=common_local_url('ApiTimelinePublic',array('format' => 'rss'));
+ $feeds[]=common_local_url('ApiTimelinePublic',array('format' => 'atom'));
+
+ //author's own feeds
+ $user = User::staticGet('id',$notice->profile_id);
+ $feeds[]=common_local_url('ApiTimelineUser',array('id' => $user->nickname, 'format'=>'rss'));
+ $feeds[]=common_local_url('ApiTimelineUser',array('id' => $user->nickname, 'format'=>'atom'));
+
+ //tag feeds
+ $tag = new Notice_tag();
+ $tag->notice_id = $notice->id;
+ if ($tag->find()) {
+ while ($tag->fetch()) {
+ $feeds[]=common_local_url('ApiTimelineTag',array('tag'=>$tag->tag, 'format'=>'rss'));
+ $feeds[]=common_local_url('ApiTimelineTag',array('tag'=>$tag->tag, 'format'=>'atom'));
+ }
+ }
+
+ //group feeds
+ $group_inbox = new Group_inbox();
+ $group_inbox->notice_id = $notice->id;
+ if ($group_inbox->find()) {
+ while ($group_inbox->fetch()) {
+ $group = User_group::staticGet('id',$group_inbox->group_id);
+ $feeds[]=common_local_url('ApiTimelineGroup',array('id' => $group->nickname,'format'=>'rss'));
+ $feeds[]=common_local_url('ApiTimelineGroup',array('id' => $group->nickname,'format'=>'atom'));
+ }
+ }
+
+ //feed of each user that subscribes to the notice's author
+ $notice_inbox = new Notice_inbox();
+ $notice_inbox->notice_id = $notice->id;
+ if ($notice_inbox->find()) {
+ while ($notice_inbox->fetch()) {
+ $user = User::staticGet('id',$notice_inbox->user_id);
+ $feeds[]=common_local_url('ApiTimelineUser',array('id' => $user->nickname, 'format'=>'rss'));
+ $feeds[]=common_local_url('ApiTimelineUser',array('id' => $user->nickname, 'format'=>'atom'));
+ }
+ }
+
+ //feed of user replied to
+ if($notice->reply_to){
+ $user = User::staticGet('id',$notice->reply_to);
+ $feeds[]=common_local_url('ApiTimelineMentions',array('id' => $user->nickname,'format'=>'rss'));
+ $feeds[]=common_local_url('ApiTimelineMentions',array('id' => $user->nickname,'format'=>'atom'));
+ }
+
+ foreach(array_unique($feeds) as $feed){
+ if(! $publisher->publish_update($feed)){
+ common_log_line(LOG_WARNING,$feed.' was not published to hub at '.$this->hub.':'.$publisher->last_response());
+ }
+ }
+ }
+}
diff --git a/plugins/PubSubHubBub/publisher.php b/plugins/PubSubHubBub/publisher.php
new file mode 100644
index 000000000..f176a9b8a
--- /dev/null
+++ b/plugins/PubSubHubBub/publisher.php
@@ -0,0 +1,86 @@
+<?php
+
+// a PHP client library for pubsubhubbub
+// as defined at http://code.google.com/p/pubsubhubbub/
+// written by Josh Fraser | joshfraser.com | josh@eventvue.com
+// Released under Apache License 2.0
+
+class Publisher {
+
+ protected $hub_url;
+ protected $last_response;
+
+ // create a new Publisher
+ public function __construct($hub_url) {
+
+ if (!isset($hub_url))
+ throw new Exception('Please specify a hub url');
+
+ if (!preg_match("|^https?://|i",$hub_url))
+ throw new Exception('The specified hub url does not appear to be valid: '.$hub_url);
+
+ $this->hub_url = $hub_url;
+ }
+
+ // accepts either a single url or an array of urls
+ public function publish_update($topic_urls, $http_function = false) {
+ if (!isset($topic_urls))
+ throw new Exception('Please specify a topic url');
+
+ // check that we're working with an array
+ if (!is_array($topic_urls)) {
+ $topic_urls = array($topic_urls);
+ }
+
+ // set the mode to publish
+ $post_string = "hub.mode=publish";
+ // loop through each topic url
+ foreach ($topic_urls as $topic_url) {
+
+ // lightweight check that we're actually working w/ a valid url
+ if (!preg_match("|^https?://|i",$topic_url))
+ throw new Exception('The specified topic url does not appear to be valid: '.$topic_url);
+
+ // append the topic url parameters
+ $post_string .= "&hub.url=".urlencode($topic_url);
+ }
+
+ // make the http post request and return true/false
+ // easy to over-write to use your own http function
+ if ($http_function)
+ return $http_function($this->hub_url,$post_string);
+ else
+ return $this->http_post($this->hub_url,$post_string);
+ }
+
+ // returns any error message from the latest request
+ public function last_response() {
+ return $this->last_response;
+ }
+
+ // default http function that uses curl to post to the hub endpoint
+ private function http_post($url, $post_string) {
+
+ // add any additional curl options here
+ $options = array(CURLOPT_URL => $url,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $post_string,
+ CURLOPT_USERAGENT => "PubSubHubbub-Publisher-PHP/1.0");
+
+ $ch = curl_init();
+ curl_setopt_array($ch, $options);
+
+ $response = curl_exec($ch);
+ $this->last_response = $response;
+ $info = curl_getinfo($ch);
+
+ curl_close($ch);
+
+ // all good
+ if ($info['http_code'] == 204)
+ return true;
+ return false;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php
index a21c33b10..198cb5ad7 100644
--- a/plugins/Realtime/RealtimePlugin.php
+++ b/plugins/Realtime/RealtimePlugin.php
@@ -101,8 +101,8 @@ class RealtimePlugin extends Plugin
$realtimeUI = ' RealtimeUpdate.initPopupWindow();';
}
else {
- $iconurl = common_path('plugins/Realtime/icon_external.gif');
- $realtimeUI = ' RealtimeUpdate.addPopup("'.$url.'", "'.$timeline.'", "'. $iconurl .'");';
+ $pluginPath = common_path('plugins/Realtime/');
+ $realtimeUI = ' RealtimeUpdate.initActions("'.$url.'", "'.$timeline.'", "'. $pluginPath .'");';
}
$action->elementStart('script', array('type' => 'text/javascript'));
@@ -118,7 +118,14 @@ class RealtimePlugin extends Plugin
return true;
}
- function onEndNoticeSave($notice)
+ function onEndShowStatusNetStyles($action)
+ {
+ $action->cssLink(common_path('plugins/Realtime/realtimeupdate.css'),
+ null, 'screen, projection, tv');
+ return true;
+ }
+
+ function onHandleQueuedNotice($notice)
{
$paths = array();
diff --git a/plugins/Realtime/icon_pause.gif b/plugins/Realtime/icon_pause.gif
new file mode 100644
index 000000000..ced0b6440
--- /dev/null
+++ b/plugins/Realtime/icon_pause.gif
Binary files differ
diff --git a/plugins/Realtime/icon_play.gif b/plugins/Realtime/icon_play.gif
new file mode 100644
index 000000000..794ec85b6
--- /dev/null
+++ b/plugins/Realtime/icon_play.gif
Binary files differ
diff --git a/plugins/Realtime/realtimeupdate.css b/plugins/Realtime/realtimeupdate.css
new file mode 100644
index 000000000..0ab5dd32b
--- /dev/null
+++ b/plugins/Realtime/realtimeupdate.css
@@ -0,0 +1,49 @@
+#notices_primary {
+position:relative;
+}
+
+#realtime_actions {
+position: absolute;
+top: -20px;
+right: 0;
+margin: 0 0 11px 0;
+}
+
+#realtime_actions li {
+margin-left: 18px;
+list-style-type: none;
+float: left;
+}
+
+#realtime_actions button {
+width: 16px;
+height: 16px;
+display: block;
+border: none;
+cursor: pointer;
+text-indent: -9999px;
+float: left;
+}
+
+#realtime_play {
+background: url(icon_play.gif) no-repeat 47% 47%;
+margin-left: 4px;
+}
+
+#realtime_pause {
+background: url(icon_pause.gif) no-repeat 47% 47%;
+}
+
+#realtime_popup {
+background: url(icon_external.gif) no-repeat 0 30%;
+}
+
+#queued_counter {
+float:left;
+line-height:1.2;
+}
+
+#showstream #notices_primary {
+margin-top: 18px;
+}
+
diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js
index fa0fefed4..a2c4da113 100644
--- a/plugins/Realtime/realtimeupdate.js
+++ b/plugins/Realtime/realtimeupdate.js
@@ -34,9 +34,11 @@ RealtimeUpdate = {
_favorurl: '',
_deleteurl: '',
_updatecounter: 0,
- _updatedelay: 500,
_maxnotices: 50,
- _windowhasfocus: false,
+ _windowhasfocus: true,
+ _documenttitle: '',
+ _paused:false,
+ _queuedNotices:[],
init: function(userid, replyurl, favorurl, deleteurl)
{
@@ -45,23 +47,17 @@ RealtimeUpdate = {
RealtimeUpdate._favorurl = favorurl;
RealtimeUpdate._deleteurl = deleteurl;
- DT = document.title;
+ RealtimeUpdate._documenttitle = document.title;
$(window).bind('focus', function(){ RealtimeUpdate._windowhasfocus = true; });
$(window).bind('blur', function() {
- $('#notices_primary .notice').css({
- 'border-top-color':$('#notices_primary .notice:last').css('border-top-color'),
- 'border-top-style':'dotted'
- });
+ $('#notices_primary .notice').removeClass('mark-top');
- $('#notices_primary .notice:first').css({
- 'border-top-color':'#AAAAAA',
- 'border-top-style':'solid'
- });
+ $('#notices_primary .notice:first').addClass('mark-top');
RealtimeUpdate._updatecounter = 0;
- document.title = DT;
+ document.title = RealtimeUpdate._documenttitle;
RealtimeUpdate._windowhasfocus = false;
return false;
@@ -70,34 +66,49 @@ RealtimeUpdate = {
receive: function(data)
{
- setTimeout(function() {
- id = data.id;
+ if (RealtimeUpdate._paused === false) {
+ RealtimeUpdate.purgeLastNoticeItem();
- // Don't add it if it already exists
- if ($("#notice-"+id).length > 0) {
- return;
- }
+ RealtimeUpdate.insertNoticeItem(data);
+ }
+ else {
+ RealtimeUpdate._queuedNotices.push(data);
+
+ RealtimeUpdate.updateQueuedCounter();
+ }
+
+ RealtimeUpdate.updateWindowCounter();
+ },
+
+ insertNoticeItem: function(data) {
+ // Don't add it if it already exists
+ if ($("#notice-"+data.id).length > 0) {
+ return;
+ }
- var noticeItem = RealtimeUpdate.makeNoticeItem(data);
- $("#notices_primary .notices").prepend(noticeItem);
- $("#notices_primary .notice:first").css({display:"none"});
- $("#notices_primary .notice:first").fadeIn(1000);
+ var noticeItem = RealtimeUpdate.makeNoticeItem(data);
+ $("#notices_primary .notices").prepend(noticeItem);
+ $("#notices_primary .notice:first").css({display:"none"});
+ $("#notices_primary .notice:first").fadeIn(1000);
- if ($('#notices_primary .notice').length > RealtimeUpdate._maxnotices) {
- $("#notices_primary .notice:last .form_disfavor").unbind('submit');
- $("#notices_primary .notice:last .form_favor").unbind('submit');
- $("#notices_primary .notice:last .notice_reply").unbind('click');
- $("#notices_primary .notice:last").remove();
- }
+ SN.U.NoticeReply();
+ SN.U.NoticeFavor();
+ },
- NoticeFavors();
- NoticeReply();
+ purgeLastNoticeItem: function() {
+ if ($('#notices_primary .notice').length > RealtimeUpdate._maxnotices) {
+ $("#notices_primary .notice:last .form_disfavor").unbind('submit');
+ $("#notices_primary .notice:last .form_favor").unbind('submit');
+ $("#notices_primary .notice:last .notice_reply").unbind('click');
+ $("#notices_primary .notice:last").remove();
+ }
+ },
- if (RealtimeUpdate._windowhasfocus === false) {
- RealtimeUpdate._updatecounter += 1;
- document.title = '('+RealtimeUpdate._updatecounter+') ' + DT;
- }
- }, RealtimeUpdate._updatedelay);
+ updateWindowCounter: function() {
+ if (RealtimeUpdate._windowhasfocus === false) {
+ RealtimeUpdate._updatecounter += 1;
+ document.title = '('+RealtimeUpdate._updatecounter+') ' + RealtimeUpdate._documenttitle;
+ }
},
makeNoticeItem: function(data)
@@ -178,30 +189,86 @@ RealtimeUpdate = {
return dl;
},
- addPopup: function(url, timeline, iconurl)
+ initActions: function(url, timeline, path)
{
- $('#notices_primary').css({'position':'relative'});
- $('#notices_primary').prepend('<button id="realtime_timeline" title="Pop up in a window">Pop up</button>');
-
- $('#realtime_timeline').css({
- 'margin':'0 0 11px 0',
- 'background':'transparent url('+ iconurl + ') no-repeat 0% 30%',
- 'padding':'0 0 0 20px',
- 'display':'block',
- 'position':'absolute',
- 'top':'-20px',
- 'right':'0',
- 'border':'none',
- 'cursor':'pointer',
- 'color':$("a").css("color"),
- 'font-weight':'bold',
- 'font-size':'1em'
- });
+ var NP = $('#notices_primary');
+ NP.prepend('<ul id="realtime_actions"><li id="realtime_playpause"></li><li id="realtime_timeline"></li></ul>');
+
+ RealtimeUpdate._pluginPath = path;
+
+ RealtimeUpdate.initPlayPause();
+ RealtimeUpdate.initAddPopup(url, timeline, RealtimeUpdate._pluginPath);
+ },
+
+ initPlayPause: function()
+ {
+ RealtimeUpdate.showPause();
+ },
+
+ showPause: function()
+ {
+ RT_PP = $('#realtime_playpause');
+ RT_PP.empty();
+ RT_PP.append('<button id="realtime_pause" class="pause" title="Pause">Pause</button>');
+
+ RT_P = $('#realtime_pause');
+ RT_P.bind('click', function() {
+ RealtimeUpdate._paused = true;
+
+ RealtimeUpdate.showPlay();
+ return false;
+ });
+ },
+
+ showPlay: function()
+ {
+ RT_PP = $('#realtime_playpause');
+ RT_PP.empty();
+ RT_PP.append('<span id="queued_counter"></span> <button id="realtime_play" class="play" title="Play">Play</button>');
+
+ RT_P = $('#realtime_play');
+ RT_P.bind('click', function() {
+ RealtimeUpdate._paused = false;
+
+ RealtimeUpdate.showPause();
+
+ RealtimeUpdate.showQueuedNotices();
+
+ return false;
+ });
+ },
+
+ showQueuedNotices: function()
+ {
+ $.each(RealtimeUpdate._queuedNotices, function(i, n) {
+ RealtimeUpdate.insertNoticeItem(n);
+ });
+
+ RealtimeUpdate._queuedNotices = [];
- $('#realtime_timeline').click(function() {
+ RealtimeUpdate.removeQueuedCounter();
+ },
+
+ updateQueuedCounter: function()
+ {
+ $('#realtime_playpause #queued_counter').html('('+RealtimeUpdate._queuedNotices.length+')');
+ },
+
+ removeQueuedCounter: function()
+ {
+ $('#realtime_playpause #queued_counter').empty();
+ },
+
+ initAddPopup: function(url, timeline, path)
+ {
+ var NP = $('#realtime_timeline');
+ NP.append('<button id="realtime_popup" title="Pop up in a window">Pop up</button>');
+
+ var PP = $('#realtime_popup');
+ PP.bind('click', function() {
window.open(url,
- timeline,
- 'toolbar=no,resizable=yes,scrollbars=yes,status=yes');
+ '',
+ 'toolbar=no,resizable=yes,scrollbars=yes,status=yes,width=500,height=550');
return false;
});
@@ -209,7 +276,6 @@ RealtimeUpdate = {
initPopupWindow: function()
{
- window.resizeTo(500, 550);
$('address').hide();
$('#content').css({'width':'93.5%'});
@@ -229,6 +295,12 @@ RealtimeUpdate = {
'left':'auto',
'right':'0'
});
+
+ $('.notices .entry-title a, .notices .entry-content a').bind('click', function() {
+ window.open(this.href, '');
+
+ return false;
+ });
}
}
diff --git a/plugins/recaptcha/LICENSE b/plugins/Recaptcha/LICENSE
index b612f71f0..b612f71f0 100644
--- a/plugins/recaptcha/LICENSE
+++ b/plugins/Recaptcha/LICENSE
diff --git a/plugins/Recaptcha/README b/plugins/Recaptcha/README
new file mode 100644
index 000000000..49c4b9c27
--- /dev/null
+++ b/plugins/Recaptcha/README
@@ -0,0 +1,28 @@
+StatusNet reCAPTCHA plugin 0.3 11/16/09
+=======================================
+Adds a captcha to your registration page to reduce automated spam bots registering.
+
+Use:
+1. Get an API key from http://recaptcha.net
+
+2. In config.php add:
+addPlugin('recaptcha', array('private_key' => 'YourKeyHere',
+ 'public_key' => 'ReplaceWithYourKey'));
+or
+addPlugin('recaptcha', array('private_key' => 'YourKeyHere',
+ 'public_key' => 'ReplaceWithYourKey',
+ 'display_errors' => true));
+Changelog
+=========
+0.1 initial release
+0.2 Work around for webkit browsers
+0.3 Moved to new plugin arch for SN
+ **YOU WILL NEED TO CHANGE YOUR CONFIG.PHP!**
+
+reCAPTCHA Lib README
+====================
+
+The reCAPTCHA PHP Lirary helps you use the reCAPTCHA API. Documentation
+for this library can be found at
+
+ http://recaptcha.net/plugins/php
diff --git a/plugins/recaptcha/recaptcha.php b/plugins/Recaptcha/RecaptchaPlugin.php
index 94cf0ccd1..1a51b16be 100644
--- a/plugins/recaptcha/recaptcha.php
+++ b/plugins/Recaptcha/RecaptchaPlugin.php
@@ -33,7 +33,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
define('RECAPTCHA', '0.2');
-class recaptcha extends Plugin
+require_once(INSTALLDIR.'/plugins/Recaptcha/recaptchalib.php');
+
+class RecaptchaPlugin extends Plugin
{
var $private_key;
var $public_key;
@@ -41,13 +43,13 @@ class recaptcha extends Plugin
var $failed;
var $ssl;
- function __construct($public_key, $private_key, $display_errors=false)
- {
- parent::__construct();
- require_once(INSTALLDIR.'/plugins/recaptcha/recaptchalib.php');
- $this->public_key = $public_key;
- $this->private_key = $private_key;
- $this->display_errors = $display_errors;
+ function onInitializePlugin(){
+ if(!isset($this->private_key)){
+ common_log(LOG_ERR, "Recaptcha: Must specify private_key in config.php");
+ }
+ if(!isset($this->public_key)){
+ common_log(LOG_ERR, "Recaptcha: Must specify public_key in config.php");
+ }
}
function checkssl(){
diff --git a/plugins/recaptcha/recaptchalib.php b/plugins/Recaptcha/recaptchalib.php
index 897c50981..897c50981 100644
--- a/plugins/recaptcha/recaptchalib.php
+++ b/plugins/Recaptcha/recaptchalib.php
diff --git a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php
new file mode 100644
index 000000000..4806538a0
--- /dev/null
+++ b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin that requires the user to have a validated email address before they can post notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+class RequireValidatedEmailPlugin extends Plugin
+{
+ function __construct()
+ {
+ parent::__construct();
+ }
+
+ function onStartNoticeSave($notice)
+ {
+ $user = User::staticGet('id', $notice->profile_id);
+ if (!empty($user)) { // it's a remote notice
+ if (empty($user->email)) {
+ throw new ClientException(_("You must validate your email address before posting."));
+ }
+ }
+ return true;
+ }
+}
+
diff --git a/plugins/ReverseUsernameAuthentication/README b/plugins/ReverseUsernameAuthentication/README
new file mode 100644
index 000000000..e9160ed9b
--- /dev/null
+++ b/plugins/ReverseUsernameAuthentication/README
@@ -0,0 +1,26 @@
+The Reverse Username Authentication plugin allows for StatusNet to handle authentication by checking if the provided password is the same as the reverse of the username.
+
+THIS PLUGIN IS FOR TESTING PURPOSES ONLY
+
+Installation
+============
+add "addPlugin('reverseUsernameAuthentication', array('setting'=>'value', 'setting2'=>'value2', ...);" to the bottom of your config.php
+
+Settings
+========
+provider_name*: a unique name for this authentication provider.
+password_changeable*: must be set to false. This plugin does not support changing passwords.
+authoritative (false): Set to true if this plugin's responses are authoritative (meaning if this fails, do check any other plugins or the internal password database).
+autoregistration (false): Set to true if users should be automatically created when they attempt to login.
+
+* required
+default values are in (parenthesis)
+
+Example
+=======
+addPlugin('reverseUsernameAuthentication', array(
+ 'provider_name'=>'Example',
+ 'password_changeable'=>false,
+ 'authoritative'=>true,
+ 'autoregistration'=>true
+));
diff --git a/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php
new file mode 100644
index 000000000..d48283b2e
--- /dev/null
+++ b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin that checks if the password is the reverse of username
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/Authentication/AuthenticationPlugin.php';
+
+class ReverseUsernameAuthenticationPlugin extends AuthenticationPlugin
+{
+ //---interface implementation---//
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->password_changeable) && $this->password_changeable){
+ throw new Exception("password_changeable cannot be set to true. This plugin does not support changing passwords.");
+ }
+ }
+
+ function checkPassword($username, $password)
+ {
+ return $username == strrev($password);
+ }
+
+ function autoRegister($username)
+ {
+ $registration_data = array();
+ $registration_data['nickname'] = $username ;
+ return User::register($registration_data);
+ }
+}
diff --git a/plugins/SimpleUrl/SimpleUrlPlugin.php b/plugins/SimpleUrl/SimpleUrlPlugin.php
new file mode 100644
index 000000000..45b745b07
--- /dev/null
+++ b/plugins/SimpleUrl/SimpleUrlPlugin.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
+
+class SimpleUrlPlugin extends UrlShortenerPlugin
+{
+ public $serviceUrl;
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->serviceUrl)){
+ throw new Exception("must specify a serviceUrl");
+ }
+ }
+
+ protected function shorten($url) {
+ return $this->http_get(sprintf($this->serviceUrl,urlencode($url)));
+ }
+}
+
diff --git a/plugins/SphinxSearch/README b/plugins/SphinxSearch/README
new file mode 100644
index 000000000..5a2c063bd
--- /dev/null
+++ b/plugins/SphinxSearch/README
@@ -0,0 +1,45 @@
+You can get a significant boost in performance using Sphinx Search
+instead of your database server to search for users and notices.
+<http://sphinxsearch.com/>.
+
+Configuration
+-------------
+
+In StatusNet's configuration, you can adjust the following settings
+under 'sphinx':
+
+enabled: Set to true to enable. Default false.
+server: a string with the hostname of the sphinx server.
+port: an integer with the port number of the sphinx server.
+
+
+Requirements
+------------
+
+To use a Sphinx server to search users and notices, you also need
+to install, compile and enable the sphinx pecl extension for php on the
+client side, which itself depends on the sphinx development files.
+"pecl install sphinx" should take care of that. Add "extension=sphinx.so"
+to your php.ini and reload apache to enable it.
+
+You can update your MySQL or Postgresql databases to drop their fulltext
+search indexes, since they're now provided by sphinx.
+
+
+You will also need a Sphinx server to serve the search queries.
+
+On the sphinx server side, a script reads the main database and build
+the keyword index. A cron job reads the database and keeps the sphinx
+indexes up to date. scripts/sphinx-cron.sh should be called by cron
+every 5 minutes, for example. scripts/sphinx.sh is an init.d script
+to start and stop the sphinx search daemon.
+
+
+Server configuration
+--------------------
+scripts/gen_config.php can generate a sphinx.conf file listing MySQL
+data sources for your databases. You may need to tweak paths afterwards.
+
+ $ plugins/SphinxSearch/scripts/gen_config.php > sphinx.conf
+
+If you wish, you can build a full config yourself based on sphinx.conf.sample
diff --git a/plugins/SphinxSearch/SphinxSearchPlugin.php b/plugins/SphinxSearch/SphinxSearchPlugin.php
new file mode 100644
index 000000000..7a27a4c04
--- /dev/null
+++ b/plugins/SphinxSearch/SphinxSearchPlugin.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+// Set defaults if not already set in the config array...
+global $config;
+$sphinxDefaults =
+ array('enabled' => true,
+ 'server' => 'localhost',
+ 'port' => 3312);
+foreach($sphinxDefaults as $key => $val) {
+ if (!isset($config['sphinx'][$key])) {
+ $config['sphinx'][$key] = $val;
+ }
+}
+
+
+
+/**
+ * Plugin for Sphinx search backend.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @link http://twitter.com/
+ */
+
+class SphinxSearchPlugin extends Plugin
+{
+ /**
+ * Automatically load any classes used
+ *
+ * @param string $cls the class
+ * @return boolean hook return
+ */
+ function onAutoload($cls)
+ {
+ switch ($cls) {
+ case 'SphinxSearch':
+ include_once INSTALLDIR . '/plugins/SphinxSearch/' .
+ strtolower($cls) . '.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Create sphinx search engine object for the given table type.
+ *
+ * @param Memcached_DataObject $target
+ * @param string $table
+ * @param out &$search_engine SearchEngine object on output if successful
+ * @ return boolean hook return
+ */
+ function onGetSearchEngine(Memcached_DataObject $target, $table, &$search_engine)
+ {
+ if (common_config('sphinx', 'enabled')) {
+ if (!class_exists('SphinxClient')) {
+ throw new ServerException('Sphinx PHP extension must be installed.');
+ }
+ $engine = new SphinxSearch($target, $table);
+ if ($engine->is_connected()) {
+ $search_engine = $engine;
+ return false;
+ }
+ }
+ // Sphinx disabled or disconnected
+ return true;
+ }
+}
diff --git a/plugins/SphinxSearch/scripts/gen_config.php b/plugins/SphinxSearch/scripts/gen_config.php
new file mode 100755
index 000000000..d5a00b6b6
--- /dev/null
+++ b/plugins/SphinxSearch/scripts/gen_config.php
@@ -0,0 +1,126 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$longoptions = array('base=', 'network');
+
+$helptext = <<<END_OF_TRIM_HELP
+Generates sphinx.conf file based on StatusNet configuration.
+ --base Base dir to Sphinx install
+ (default /usr/local)
+ --network Use status_network global config table
+ (non-functional at present)
+
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require dirname(__FILE__) . '/sphinx-utils.php';
+
+
+$timestamp = date('r');
+print <<<END
+#
+# Sphinx configuration for StatusNet
+# Generated {$timestamp}
+#
+
+END;
+
+sphinx_iterate_sites('sphinx_site_template');
+
+print <<<END
+
+indexer
+{
+ mem_limit = 300M
+}
+
+searchd
+{
+ port = 3312
+ log = {$base}/log/searchd.log
+ query_log = {$base}/log/query.log
+ read_timeout = 5
+ max_children = 30
+ pid_file = {$base}/log/searchd.pid
+ max_matches = 1000
+ seamless_rotate = 1
+ preopen_indexes = 0
+ unlink_old = 1
+}
+
+END;
+
+
+
+/**
+ * Build config entries for a single site
+ * @fixme we only seem to have master DB currently available...
+ */
+function sphinx_site_template($sn)
+{
+ return
+ sphinx_template($sn,
+ 'profile',
+ 'SELECT id, UNIX_TIMESTAMP(created) as created_ts, nickname, fullname, location, bio, homepage FROM profile',
+ 'SELECT * FROM profile where id = $id') .
+ sphinx_template($sn,
+ 'notice',
+ 'SELECT id, UNIX_TIMESTAMP(created) as created_ts, content FROM notice',
+ 'SELECT * FROM notice where notice.id = $id AND notice.is_local != -2');
+}
+
+function sphinx_template($sn, $table, $query, $query_info)
+{
+ $base = sphinx_base();
+ $dbtype = common_config('db', 'type');
+
+ print <<<END
+
+#
+# {$sn->sitename}
+#
+source {$sn->dbname}_src_{$table}
+{
+ type = {$dbtype}
+ sql_host = {$sn->dbhost}
+ sql_user = {$sn->dbuser}
+ sql_pass = {$sn->dbpass}
+ sql_db = {$sn->dbname}
+ sql_query_pre = SET NAMES utf8;
+ sql_query = {$query}
+ sql_query_info = {$query_info}
+ sql_attr_timestamp = created_ts
+}
+
+index {$sn->dbname}_{$table}
+{
+ source = {$sn->dbname}_src_{$table}
+ path = {$base}/data/{$sn->dbname}_{$table}
+ docinfo = extern
+ charset_type = utf-8
+ min_word_len = 3
+}
+
+
+END;
+}
diff --git a/plugins/SphinxSearch/scripts/index_update.php b/plugins/SphinxSearch/scripts/index_update.php
new file mode 100755
index 000000000..23c60ced7
--- /dev/null
+++ b/plugins/SphinxSearch/scripts/index_update.php
@@ -0,0 +1,61 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$longoptions = array('base=', 'network');
+
+$helptext = <<<END_OF_TRIM_HELP
+Runs Sphinx search indexer.
+ --rotate Have Sphinx run index update in background and
+ rotate updated indexes into place as they finish.
+ --base Base dir to Sphinx install
+ (default /usr/local)
+ --network Use status_network global config table for site list
+ (non-functional at present)
+
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require dirname(__FILE__) . '/sphinx-utils.php';
+
+sphinx_iterate_sites('sphinx_index_update');
+
+function sphinx_index_update($sn)
+{
+ $base = sphinx_base();
+
+ $baseIndexes = array('notice', 'profile');
+ $params = array();
+
+ if (have_option('rotate')) {
+ $params[] = '--rotate';
+ }
+ foreach ($baseIndexes as $index) {
+ $params[] = "{$sn->dbname}_{$index}";
+ }
+
+ $params = implode(' ', $params);
+ $cmd = "$base/bin/indexer --config $base/etc/sphinx.conf $params";
+
+ print "$cmd\n";
+ system($cmd);
+}
diff --git a/plugins/SphinxSearch/scripts/sphinx-utils.php b/plugins/SphinxSearch/scripts/sphinx-utils.php
new file mode 100644
index 000000000..7bbc25270
--- /dev/null
+++ b/plugins/SphinxSearch/scripts/sphinx-utils.php
@@ -0,0 +1,63 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function sphinx_use_network()
+{
+ return have_option('network');
+}
+
+function sphinx_base()
+{
+ if (have_option('base')) {
+ return get_option_value('base');
+ } else {
+ return "/usr/local/sphinx";
+ }
+}
+
+function sphinx_iterate_sites($callback)
+{
+ if (sphinx_use_network()) {
+ // @fixme this should use, like, some kind of config
+ Status_network::setupDB('localhost', 'statusnet', 'statuspass', 'statusnet');
+ $sn = new Status_network();
+ if (!$sn->find()) {
+ die("Confused... no sites in status_network table or lookup failed.\n");
+ }
+ while ($sn->fetch()) {
+ $callback($sn);
+ }
+ } else {
+ if (preg_match('!^(mysqli?|pgsql)://(.*?):(.*?)@(.*?)/(.*?)$!',
+ common_config('db', 'database'), $matches)) {
+ list(/*all*/, $dbtype, $dbuser, $dbpass, $dbhost, $dbname) = $matches;
+ $sn = (object)array(
+ 'sitename' => common_config('site', 'name'),
+ 'dbhost' => $dbhost,
+ 'dbuser' => $dbuser,
+ 'dbpass' => $dbpass,
+ 'dbname' => $dbname);
+ $callback($sn);
+ } else {
+ print "Unrecognized database configuration string in config.php\n";
+ exit(1);
+ }
+ }
+}
+
diff --git a/plugins/SphinxSearch/scripts/sphinx.sh b/plugins/SphinxSearch/scripts/sphinx.sh
new file mode 100755
index 000000000..b8edeb302
--- /dev/null
+++ b/plugins/SphinxSearch/scripts/sphinx.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+if [[ $1 = "start" ]]
+then
+ echo "Stopping any running daemons..."
+ /usr/local/bin/searchd --config /usr/local/etc/sphinx.conf --stop 2> /dev/null
+ echo "Starting sphinx search daemon..."
+ /usr/local/bin/searchd --config /usr/local/etc/sphinx.conf 2> /dev/null
+fi
+
+if [[ $1 = "stop" ]]
+then
+ echo "Stopping sphinx search daemon..."
+ /usr/local/bin/searchd --config /usr/local/etc/sphinx.conf --stop 2> /dev/null
+fi
diff --git a/plugins/SphinxSearch/sphinx.conf.sample b/plugins/SphinxSearch/sphinx.conf.sample
new file mode 100644
index 000000000..3de62f637
--- /dev/null
+++ b/plugins/SphinxSearch/sphinx.conf.sample
@@ -0,0 +1,71 @@
+#
+# Minimal Sphinx configuration sample for statusnet
+#
+
+source src1
+{
+ type = mysql
+ sql_host = localhost
+ sql_user = USERNAME
+ sql_pass = PASSWORD
+ sql_db = identi_ca
+ sql_port = 3306
+ sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, nickname, fullname, location, bio, homepage FROM profile
+ sql_query_info = SELECT * FROM profile where id = $id
+ sql_attr_timestamp = created_ts
+}
+
+
+source src2
+{
+ type = mysql
+ sql_host = localhost
+ sql_user = USERNAME
+ sql_pass = PASSWORD
+ sql_db = identi_ca
+ sql_port = 3306
+ sql_query = SELECT id, UNIX_TIMESTAMP(created) as created_ts, content FROM notice
+ sql_query_info = SELECT * FROM notice where notice.id = $id AND notice.is_local != -2
+ sql_attr_timestamp = created_ts
+}
+
+index identica_notices
+{
+ source = src2
+ path = DIRECTORY/data/identica_notices
+ docinfo = extern
+ charset_type = utf-8
+ min_word_len = 3
+ stopwords = DIRECTORY/data/stopwords-en.txt
+}
+
+
+index identica_people
+{
+ source = src1
+ path = DIRECTORY/data/identica_people
+ docinfo = extern
+ charset_type = utf-8
+ min_word_len = 3
+ stopwords = DIRECTORY/data/stopwords-en.txt
+}
+
+indexer
+{
+ mem_limit = 32M
+}
+
+searchd
+{
+ port = 3312
+ log = DIRECTORY/log/searchd.log
+ query_log = DIRECTORY/log/query.log
+ read_timeout = 5
+ max_children = 30
+ pid_file = DIRECTORY/log/searchd.pid
+ max_matches = 1000
+ seamless_rotate = 1
+ preopen_indexes = 0
+ unlink_old = 1
+}
+
diff --git a/plugins/SphinxSearch/sphinxsearch.php b/plugins/SphinxSearch/sphinxsearch.php
new file mode 100644
index 000000000..71f330828
--- /dev/null
+++ b/plugins/SphinxSearch/sphinxsearch.php
@@ -0,0 +1,96 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class SphinxSearch extends SearchEngine
+{
+ private $sphinx;
+ private $connected;
+
+ function __construct($target, $table)
+ {
+ $fp = @fsockopen(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
+ if (!$fp) {
+ $this->connected = false;
+ return;
+ }
+ fclose($fp);
+ parent::__construct($target, $table);
+ $this->sphinx = new SphinxClient;
+ $this->sphinx->setServer(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
+ $this->connected = true;
+ }
+
+ function is_connected()
+ {
+ return $this->connected;
+ }
+
+ function limit($offset, $count, $rss = false)
+ {
+ //FIXME without LARGEST_POSSIBLE, the most recent results aren't returned
+ // this probably has a large impact on performance
+ $LARGEST_POSSIBLE = 1e6;
+
+ if ($rss) {
+ $this->sphinx->setLimits($offset, $count, $count, $LARGEST_POSSIBLE);
+ }
+ else {
+ // return at most 50 pages of results
+ $this->sphinx->setLimits($offset, $count, 50 * ($count - 1), $LARGEST_POSSIBLE);
+ }
+
+ return $this->target->limit(0, $count);
+ }
+
+ function query($q)
+ {
+ $result = $this->sphinx->query($q, $this->remote_table());
+ if (!isset($result['matches'])) return false;
+ $id_set = join(', ', array_keys($result['matches']));
+ $this->target->whereAdd("id in ($id_set)");
+ return true;
+ }
+
+ function set_sort_mode($mode)
+ {
+ if ('chron' === $mode) {
+ $this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts');
+ return $this->target->orderBy('created desc');
+ }
+ }
+
+ function remote_table()
+ {
+ return $this->dbname() . '_' . $this->table;
+ }
+
+ function dbname()
+ {
+ // @fixme there should be a less dreadful way to do this.
+ // DB objects won't give database back until they connect, it's confusing
+ if (preg_match('!^.*?://.*?:.*?@.*?/(.*?)$!', common_config('db', 'database'), $matches)) {
+ return $matches[1];
+ }
+ throw new ServerException("Sphinx search could not identify database name");
+ }
+}
diff --git a/plugins/TemplatePlugin.php b/plugins/TemplatePlugin.php
index cfa051162..5f3ad81f5 100644
--- a/plugins/TemplatePlugin.php
+++ b/plugins/TemplatePlugin.php
@@ -32,7 +32,7 @@ class TemplatePlugin extends Plugin {
// capture the RouterInitialized event
// and connect a new API method
// for updating the template
- function onRouterInitialized( &$m ) {
+ function onRouterInitialized( $m ) {
$m->connect( 'template/update', array(
'action' => 'template',
));
diff --git a/plugins/TightUrl/TightUrlPlugin.php b/plugins/TightUrl/TightUrlPlugin.php
new file mode 100644
index 000000000..56414c8c8
--- /dev/null
+++ b/plugins/TightUrl/TightUrlPlugin.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to push RSS/Atom updates to a PubSubHubBub hub
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @copyright 2009 Craig Andrews http://candrews.integralblue.com
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php';
+
+class TightUrlPlugin extends UrlShortenerPlugin
+{
+ public $serviceUrl;
+
+ function onInitializePlugin(){
+ parent::onInitializePlugin();
+ if(!isset($this->serviceUrl)){
+ throw new Exception("must specify a serviceUrl");
+ }
+ }
+
+ protected function shorten($url)
+ {
+ $response = $this->http_get(sprintf($this->serviceUrl,urlencode($url)));
+ if (!$response) return;
+ $response = $this->tidy($response);
+ $y = @simplexml_load_string($response);
+ if (!isset($y->body)) return;
+ $xml = $y->body->p[0]->code[0]->a->attributes();
+ if (isset($xml['href'])) return $xml['href'];
+ }
+}
diff --git a/plugins/TwitterBridge/README b/plugins/TwitterBridge/README
new file mode 100644
index 000000000..d3bcda598
--- /dev/null
+++ b/plugins/TwitterBridge/README
@@ -0,0 +1,85 @@
+This Twitter "bridge" plugin allows you to integrate your StatusNet
+instance with Twitter. Installing it will allow your users to:
+
+ - automatically post notices to thier Twitter accounts
+ - automatically subscribe to other Twitter users who are also using
+ your StatusNet install, if possible (requires running a daemon)
+ - import their Twitter friends' tweets (requires running a daemon)
+
+Installation
+------------
+
+To enable the plugin, add the following to your config.php:
+
+ addPlugin("TwitterBridge");
+
+OAuth is used to to access protected resources on Twitter (as opposed to
+HTTP Basic Auth)*. To use Twitter bridging you will need to register
+your instance of StatusNet as an application on Twitter
+(http://twitter.com/apps), and update the following variables in your
+config.php with the consumer key and secret Twitter generates for you:
+
+ $config['twitter']['consumer_key'] = 'YOURKEY';
+ $config['twitter']['consumer_secret'] = 'YOURSECRET';
+
+When registering your application with Twitter set the type to "Browser"
+and your Callback URL to:
+
+ http://example.org/mublog/twitter/authorization
+
+The default access type should be, "Read & Write".
+
+* Note: The plugin will still push notices to Twitter for users who
+ have previously setup the Twitter bridge using their Twitter name and
+ password under an older versions of StatusNet, but all new Twitter
+ bridge connections will use OAuth.
+
+Deamons
+-------
+
+For friend syncing and importing notices running two additional daemon
+scripts is necessary (synctwitterfriends.php and
+twitterstatusfetcher.php).
+
+In the daemons subidrectory of the plugin are three scripts:
+
+* Twitter Friends Syncing (daemons/synctwitterfriends.php)
+
+Users may set a flag in their settings ("Subscribe to my Twitter friends
+here" under the Twitter tab) to have StatusNet attempt to locate and
+subscribe to "friends" (people they "follow") on Twitter who also have
+accounts on your StatusNet system, and who have previously set up a link
+for automatically posting notices to Twitter.
+
+The plugin will try to start this daemon when you run
+scripts/startdaemons.sh.
+
+* Importing statuses from Twitter (daemons/twitterstatusfetcher.php)
+
+To allow your users to import their friends' Twitter statuses, you will
+need to enable the bidirectional Twitter bridge in your config.php:
+
+ $config['twitterimport']['enabled'] = true;
+
+The plugin will then start the TwitterStatusFetcher daemon along with the
+other daemons when you run scripts/startdaemons.sh.
+
+Additionally, you will want to set the integration source variable,
+which will keep notices posted to Twitter via StatusNet from looping
+back. The integration source should be set to the name of your
+application, exactly as you specified it on the settings page for your
+StatusNet application on Twitter, e.g.:
+
+ $config['integration']['source'] = 'YourApp';
+
+* TwitterQueueHandler (daemons/twitterqueuehandler.php)
+
+This script sends queued notices to Twitter for user who have opted to
+set up Twitter bridging.
+
+It's not strictly necessary to run this queue handler, and sites that
+haven't enabled queuing are still able to push notices to Twitter, but
+for larger sites and sites that wish to improve performance, this
+script allows notices to be sent "offline" via a separate process.
+
+The plugin will start this script when you run scripts/startdaemons.sh.
diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php
new file mode 100644
index 000000000..ad3c2e551
--- /dev/null
+++ b/plugins/TwitterBridge/TwitterBridgePlugin.php
@@ -0,0 +1,187 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+
+/**
+ * Plugin for sending and importing Twitter statuses
+ *
+ * This class allows users to link their Twitter accounts
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @link http://twitter.com/
+ */
+
+class TwitterBridgePlugin extends Plugin
+{
+ /**
+ * Initializer for the plugin.
+ */
+
+ function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Add Twitter-related paths to the router table
+ *
+ * Hook for RouterInitialized event.
+ *
+ * @param Net_URL_Mapper $m path-to-action mapper
+ *
+ * @return boolean hook return
+ */
+
+ function onRouterInitialized($m)
+ {
+ $m->connect('twitter/authorization',
+ array('action' => 'twitterauthorization'));
+ $m->connect('settings/twitter', array('action' => 'twittersettings'));
+
+ return true;
+ }
+
+ /**
+ * Add the Twitter Settings page to the Connect Settings menu
+ *
+ * @param Action &$action The calling page
+ *
+ * @return boolean hook return
+ */
+ function onEndConnectSettingsNav(&$action)
+ {
+ $action_name = $action->trimmed('action');
+
+ $action->menuItem(common_local_url('twittersettings'),
+ _('Twitter'),
+ _('Twitter integration options'),
+ $action_name === 'twittersettings');
+
+ return true;
+ }
+
+ /**
+ * Automatically load the actions and libraries used by the Twitter bridge
+ *
+ * @param Class $cls the class
+ *
+ * @return boolean hook return
+ *
+ */
+ function onAutoload($cls)
+ {
+ switch ($cls) {
+ case 'TwittersettingsAction':
+ case 'TwitterauthorizationAction':
+ include_once INSTALLDIR . '/plugins/TwitterBridge/' .
+ strtolower(mb_substr($cls, 0, -6)) . '.php';
+ return false;
+ case 'TwitterOAuthClient':
+ include_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Add a Twitter queue item for each notice
+ *
+ * @param Notice $notice the notice
+ * @param array &$transports the list of transports (queues)
+ *
+ * @return boolean hook return
+ */
+ function onStartEnqueueNotice($notice, &$transports)
+ {
+ array_push($transports, 'twitter');
+ return true;
+ }
+
+ /**
+ * broadcast the message when not using queuehandler
+ *
+ * @param Notice &$notice the notice
+ * @param array $queue destination queue
+ *
+ * @return boolean hook return
+ */
+ function onUnqueueHandleNotice(&$notice, $queue)
+ {
+ if (($queue == 'twitter') && ($this->_isLocal($notice))) {
+ broadcast_twitter($notice);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determine whether the notice was locally created
+ *
+ * @param Notice $notice
+ *
+ * @return boolean locality
+ */
+ function _isLocal($notice)
+ {
+ return ($notice->is_local == Notice::LOCAL_PUBLIC ||
+ $notice->is_local == Notice::LOCAL_NONPUBLIC);
+ }
+
+ /**
+ * Add Twitter bridge daemons to the list of daemons to start
+ *
+ * @param array $daemons the list fo daemons to run
+ *
+ * @return boolean hook return
+ *
+ */
+ function onGetValidDaemons($daemons)
+ {
+ array_push($daemons, INSTALLDIR .
+ '/plugins/TwitterBridge/daemons/twitterqueuehandler.php');
+ array_push($daemons, INSTALLDIR .
+ '/plugins/TwitterBridge/daemons/synctwitterfriends.php');
+
+ if (common_config('twitterimport', 'enabled')) {
+ array_push($daemons, INSTALLDIR
+ . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php');
+ }
+
+ return true;
+ }
+
+}
diff --git a/plugins/TwitterBridge/daemons/synctwitterfriends.php b/plugins/TwitterBridge/daemons/synctwitterfriends.php
new file mode 100755
index 000000000..671e3c7af
--- /dev/null
+++ b/plugins/TwitterBridge/daemons/synctwitterfriends.php
@@ -0,0 +1,281 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
+
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for synching local friends with Twitter friends.
+ -i --id Identity (default 'generic')
+ -d --debug Debug (lots of log output)
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
+
+/**
+ * Daemon to sync local friends with Twitter friends
+ *
+ * @category Twitter
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class SyncTwitterFriendsDaemon extends ParallelizingDaemon
+{
+ /**
+ * Constructor
+ *
+ * @param string $id the name/id of this daemon
+ * @param int $interval sleep this long before doing everything again
+ * @param int $max_children maximum number of child processes at a time
+ * @param boolean $debug debug output flag
+ *
+ * @return void
+ *
+ **/
+
+ function __construct($id = null, $interval = 60,
+ $max_children = 2, $debug = null)
+ {
+ parent::__construct($id, $interval, $max_children, $debug);
+ }
+
+ /**
+ * Name of this daemon
+ *
+ * @return string Name of the daemon.
+ */
+
+ function name()
+ {
+ return ('synctwitterfriends.' . $this->_id);
+ }
+
+ /**
+ * Find all the Twitter foreign links for users who have requested
+ * automatically subscribing to their Twitter friends locally.
+ *
+ * @return array flinks an array of Foreign_link objects
+ */
+ function getObjects()
+ {
+ $flinks = array();
+ $flink = new Foreign_link();
+
+ $conn = &$flink->getDatabaseConnection();
+
+ $flink->service = TWITTER_SERVICE;
+ $flink->orderBy('last_friendsync');
+ $flink->limit(25); // sync this many users during this run
+ $flink->find();
+
+ while ($flink->fetch()) {
+ if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) {
+ $flinks[] = clone($flink);
+ }
+ }
+
+ $conn->disconnect();
+
+ global $_DB_DATAOBJECT;
+ unset($_DB_DATAOBJECT['CONNECTIONS']);
+
+ return $flinks;
+ }
+
+ function childTask($flink) {
+
+ // Each child ps needs its own DB connection
+
+ // Note: DataObject::getDatabaseConnection() creates
+ // a new connection if there isn't one already
+
+ $conn = &$flink->getDatabaseConnection();
+
+ $this->subscribeTwitterFriends($flink);
+
+ $flink->last_friendsync = common_sql_now();
+ $flink->update();
+
+ $conn->disconnect();
+
+ // XXX: Couldn't find a less brutal way to blow
+ // away a cached connection
+
+ global $_DB_DATAOBJECT;
+ unset($_DB_DATAOBJECT['CONNECTIONS']);
+ }
+
+ function fetchTwitterFriends($flink)
+ {
+ $friends = array();
+
+ $client = null;
+
+ if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
+ $token = TwitterOAuthClient::unpackToken($flink->credentials);
+ $client = new TwitterOAuthClient($token->key, $token->secret);
+ common_debug($this->name() . '- Grabbing friends IDs with OAuth.');
+ } else {
+ $client = new TwitterBasicAuthClient($flink);
+ common_debug($this->name() . '- Grabbing friends IDs with basic auth.');
+ }
+
+ try {
+ $friends_ids = $client->friendsIds();
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, $this->name() .
+ ' - error getting friend ids: ' .
+ $e->getMessage());
+ return $friends;
+ }
+
+ if (empty($friends_ids)) {
+ common_debug($this->name() .
+ " - Twitter user $flink->foreign_id " .
+ 'doesn\'t have any friends!');
+ return $friends;
+ }
+
+ common_debug($this->name() . ' - Twitter\'s API says Twitter user id ' .
+ "$flink->foreign_id has " .
+ count($friends_ids) . ' friends.');
+
+ // Calculate how many pages to get...
+ $pages = ceil(count($friends_ids) / 100);
+
+ if ($pages == 0) {
+ common_debug($this->name() . " - $user seems to have no friends.");
+ }
+
+ for ($i = 1; $i <= $pages; $i++) {
+
+ try {
+ $more_friends = $client->statusesFriends(null, null, null, $i);
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, $this->name() .
+ ' - cURL error getting Twitter statuses/friends ' .
+ "page $i - " . $e->getCode() . ' - ' .
+ $e->getMessage());
+ }
+
+ if (empty($more_friends)) {
+ common_log(LOG_WARNING, $this->name() .
+ " - Couldn't retrieve page $i " .
+ "of Twitter user $flink->foreign_id friends.");
+ continue;
+ } else {
+ $friends = array_merge($friends, $more_friends);
+ }
+ }
+
+ return $friends;
+ }
+
+ function subscribeTwitterFriends($flink)
+ {
+ $friends = $this->fetchTwitterFriends($flink);
+
+ if (empty($friends)) {
+ common_debug($this->name() .
+ ' - Couldn\'t get friends from Twitter for ' .
+ "Twitter user $flink->foreign_id.");
+ return false;
+ }
+
+ $user = $flink->getUser();
+
+ foreach ($friends as $friend) {
+
+ $friend_name = $friend->screen_name;
+ $friend_id = (int) $friend->id;
+
+ // Update or create the Foreign_user record for each
+ // Twitter friend
+
+ if (!save_twitter_user($friend_id, $friend_name)) {
+ common_log(LOG_WARNING, $this-name() .
+ " - Couldn't save $screen_name's friend, $friend_name.");
+ continue;
+ }
+
+ // Check to see if there's a related local user
+
+ $friend_flink = Foreign_link::getByForeignID($friend_id,
+ TWITTER_SERVICE);
+
+ if (!empty($friend_flink)) {
+
+ // Get associated user and subscribe her
+
+ $friend_user = User::staticGet('id', $friend_flink->user_id);
+
+ if (!empty($friend_user)) {
+ $result = subs_subscribe_to($user, $friend_user);
+
+ if ($result === true) {
+ common_log(LOG_INFO,
+ $this->name() . ' - Subscribed ' .
+ "$friend_user->nickname to $user->nickname.");
+ } else {
+ common_debug($this->name() .
+ ' - Tried subscribing ' .
+ "$friend_user->nickname to $user->nickname - " .
+ $result);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+}
+
+$id = null;
+$debug = null;
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+if (have_option('d') || have_option('debug')) {
+ $debug = true;
+}
+
+$syncer = new SyncTwitterFriendsDaemon($id, 60, 2, $debug);
+$syncer->runOnce();
+
diff --git a/plugins/TwitterBridge/daemons/twitterqueuehandler.php b/plugins/TwitterBridge/daemons/twitterqueuehandler.php
new file mode 100755
index 000000000..f0e76bb74
--- /dev/null
+++ b/plugins/TwitterBridge/daemons/twitterqueuehandler.php
@@ -0,0 +1,73 @@
+#!/usr/bin/env php
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+$shortoptions = 'i::';
+$longoptions = array('id::');
+
+$helptext = <<<END_OF_ENJIT_HELP
+Daemon script for pushing new notices to Twitter.
+
+ -i --id Identity (default none)
+
+END_OF_ENJIT_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/queuehandler.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+
+class TwitterQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'twitter';
+ }
+
+ function start()
+ {
+ $this->log(LOG_INFO, "INITIALIZE");
+ return true;
+ }
+
+ function handle_notice($notice)
+ {
+ return broadcast_twitter($notice);
+ }
+
+ function finish()
+ {
+ }
+
+}
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+$handler = new TwitterQueueHandler($id);
+
+$handler->runOnce();
diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php
new file mode 100755
index 000000000..b5428316b
--- /dev/null
+++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php
@@ -0,0 +1,570 @@
+#!/usr/bin/env php
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+
+// Tune number of processes and how often to poll Twitter
+// XXX: Should these things be in config.php?
+define('MAXCHILDREN', 2);
+define('POLL_INTERVAL', 60); // in seconds
+
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
+
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for retrieving Twitter messages from foreign service.
+
+ -i --id Identity (default 'generic')
+ -d --debug Debug (lots of log output)
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/lib/daemon.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
+
+/**
+ * Fetcher for statuses from Twitter
+ *
+ * Fetches statuses from Twitter and inserts them as notices in local
+ * system.
+ *
+ * @category Twitter
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+// NOTE: an Avatar path MUST be set in config.php for this
+// script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar';
+
+class TwitterStatusFetcher extends ParallelizingDaemon
+{
+ /**
+ * Constructor
+ *
+ * @param string $id the name/id of this daemon
+ * @param int $interval sleep this long before doing everything again
+ * @param int $max_children maximum number of child processes at a time
+ * @param boolean $debug debug output flag
+ *
+ * @return void
+ *
+ **/
+ function __construct($id = null, $interval = 60,
+ $max_children = 2, $debug = null)
+ {
+ parent::__construct($id, $interval, $max_children, $debug);
+ }
+
+ /**
+ * Name of this daemon
+ *
+ * @return string Name of the daemon.
+ */
+
+ function name()
+ {
+ return ('twitterstatusfetcher.'.$this->_id);
+ }
+
+ /**
+ * Find all the Twitter foreign links for users who have requested
+ * importing of their friends' timelines
+ *
+ * @return array flinks an array of Foreign_link objects
+ */
+
+ function getObjects()
+ {
+ global $_DB_DATAOBJECT;
+
+ $flink = new Foreign_link();
+ $conn = &$flink->getDatabaseConnection();
+
+ $flink->service = TWITTER_SERVICE;
+ $flink->orderBy('last_noticesync');
+ $flink->find();
+
+ $flinks = array();
+ common_log(LOG_INFO, "hello");
+
+ while ($flink->fetch()) {
+
+ if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
+ FOREIGN_NOTICE_RECV) {
+ $flinks[] = clone($flink);
+ common_log(LOG_INFO, "sync: foreign id $flink->foreign_id");
+ } else {
+ common_log(LOG_INFO, "nothing to sync");
+ }
+ }
+
+ $flink->free();
+ unset($flink);
+
+ $conn->disconnect();
+ unset($_DB_DATAOBJECT['CONNECTIONS']);
+
+ return $flinks;
+ }
+
+ function childTask($flink) {
+
+ // Each child ps needs its own DB connection
+
+ // Note: DataObject::getDatabaseConnection() creates
+ // a new connection if there isn't one already
+
+ $conn = &$flink->getDatabaseConnection();
+
+ $this->getTimeline($flink);
+
+ $flink->last_friendsync = common_sql_now();
+ $flink->update();
+
+ $conn->disconnect();
+
+ // XXX: Couldn't find a less brutal way to blow
+ // away a cached connection
+
+ global $_DB_DATAOBJECT;
+ unset($_DB_DATAOBJECT['CONNECTIONS']);
+ }
+
+ function getTimeline($flink)
+ {
+ if (empty($flink)) {
+ common_log(LOG_WARNING, $this->name() .
+ " - Can't retrieve Foreign_link for foreign ID $fid");
+ return;
+ }
+
+ common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
+ $flink->foreign_id);
+
+ // XXX: Biggest remaining issue - How do we know at which status
+ // to start importing? How many statuses? Right now I'm going
+ // with the default last 20.
+
+ $client = null;
+
+ if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
+ $token = TwitterOAuthClient::unpackToken($flink->credentials);
+ $client = new TwitterOAuthClient($token->key, $token->secret);
+ common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
+ } else {
+ $client = new TwitterBasicAuthClient($flink);
+ common_debug($this->name() . ' - Grabbing friends timeline with basic auth.');
+ }
+
+ $timeline = null;
+
+ try {
+ $timeline = $client->statusesFriendsTimeline();
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, $this->name() .
+ ' - Twitter client unable to get friends timeline for user ' .
+ $flink->user_id . ' - code: ' .
+ $e->getCode() . 'msg: ' . $e->getMessage());
+ }
+
+ if (empty($timeline)) {
+ common_log(LOG_WARNING, $this->name() . " - Empty timeline.");
+ return;
+ }
+
+ // Reverse to preserve order
+
+ foreach (array_reverse($timeline) as $status) {
+
+ // Hacktastic: filter out stuff coming from this StatusNet
+
+ $source = mb_strtolower(common_config('integration', 'source'));
+
+ if (preg_match("/$source/", mb_strtolower($status->source))) {
+ common_debug($this->name() . ' - Skipping import of status ' .
+ $status->id . ' with source ' . $source);
+ continue;
+ }
+
+ $this->saveStatus($status, $flink);
+ }
+
+ // Okay, record the time we synced with Twitter for posterity
+
+ $flink->last_noticesync = common_sql_now();
+ $flink->update();
+ }
+
+ function saveStatus($status, $flink)
+ {
+ $id = $this->ensureProfile($status->user);
+
+ $profile = Profile::staticGet($id);
+
+ if (empty($profile)) {
+ common_log(LOG_ERR, $this->name() .
+ ' - Problem saving notice. No associated Profile.');
+ return null;
+ }
+
+ // XXX: change of screen name?
+
+ $uri = 'http://twitter.com/' . $status->user->screen_name .
+ '/status/' . $status->id;
+
+ $notice = Notice::staticGet('uri', $uri);
+
+ // check to see if we've already imported the status
+
+ if (empty($notice)) {
+
+ $notice = new Notice();
+
+ $notice->profile_id = $id;
+ $notice->uri = $uri;
+ $notice->created = strftime('%Y-%m-%d %H:%M:%S',
+ strtotime($status->created_at));
+ $notice->content = common_shorten_links($status->text); // XXX
+ $notice->rendered = common_render_content($notice->content, $notice);
+ $notice->source = 'twitter';
+ $notice->reply_to = null; // XXX: lookup reply
+ $notice->is_local = Notice::GATEWAY;
+
+ if (Event::handle('StartNoticeSave', array(&$notice))) {
+ $id = $notice->insert();
+ Event::handle('EndNoticeSave', array($notice));
+ }
+ }
+
+ if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
+ 'user_id' => $flink->user_id))) {
+ // Add to inbox
+ $inbox = new Notice_inbox();
+
+ $inbox->user_id = $flink->user_id;
+ $inbox->notice_id = $notice->id;
+ $inbox->created = $notice->created;
+ $inbox->source = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
+
+ $inbox->insert();
+ }
+ }
+
+ function ensureProfile($user)
+ {
+ // check to see if there's already a profile for this user
+
+ $profileurl = 'http://twitter.com/' . $user->screen_name;
+ $profile = Profile::staticGet('profileurl', $profileurl);
+
+ if (!empty($profile)) {
+ common_debug($this->name() .
+ " - Profile for $profile->nickname found.");
+
+ // Check to see if the user's Avatar has changed
+
+ $this->checkAvatar($user, $profile);
+ return $profile->id;
+
+ } else {
+ common_debug($this->name() . ' - Adding profile and remote profile ' .
+ "for Twitter user: $profileurl.");
+
+ $profile = new Profile();
+ $profile->query("BEGIN");
+
+ $profile->nickname = $user->screen_name;
+ $profile->fullname = $user->name;
+ $profile->homepage = $user->url;
+ $profile->bio = $user->description;
+ $profile->location = $user->location;
+ $profile->profileurl = $profileurl;
+ $profile->created = common_sql_now();
+
+ $id = $profile->insert();
+
+ if (empty($id)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+
+ // check for remote profile
+
+ $remote_pro = Remote_profile::staticGet('uri', $profileurl);
+
+ if (empty($remote_pro)) {
+
+ $remote_pro = new Remote_profile();
+
+ $remote_pro->id = $id;
+ $remote_pro->uri = $profileurl;
+ $remote_pro->created = common_sql_now();
+
+ $rid = $remote_pro->insert();
+
+ if (empty($rid)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+ }
+
+ $profile->query("COMMIT");
+
+ $this->saveAvatars($user, $id);
+
+ return $id;
+ }
+ }
+
+ function checkAvatar($twitter_user, $profile)
+ {
+ global $config;
+
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $newname = 'Twitter_' . $twitter_user->id . '_' .
+ $path_parts['basename'];
+
+ $oldname = $profile->getAvatar(48)->filename;
+
+ if ($newname != $oldname) {
+ common_debug($this->name() . ' - Avatar for Twitter user ' .
+ "$profile->nickname has changed.");
+ common_debug($this->name() . " - old: $oldname new: $newname");
+
+ $this->updateAvatars($twitter_user, $profile);
+ }
+
+ if ($this->missingAvatarFile($profile)) {
+ common_debug($this->name() . ' - Twitter user ' .
+ $profile->nickname .
+ ' is missing one or more local avatars.');
+ common_debug($this->name() ." - old: $oldname new: $newname");
+
+ $this->updateAvatars($twitter_user, $profile);
+ }
+
+ }
+
+ function updateAvatars($twitter_user, $profile) {
+
+ global $config;
+
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $img_root = substr($path_parts['basename'], 0, -11);
+ $ext = $path_parts['extension'];
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $twitter_user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ $this->updateAvatar($profile->id, $size, $mediatype, $filename);
+ $this->fetchAvatar($url, $filename);
+ }
+ }
+
+ function missingAvatarFile($profile) {
+
+ foreach (array(24, 48, 73) as $size) {
+
+ $filename = $profile->getAvatar($size)->filename;
+ $avatarpath = Avatar::path($filename);
+
+ if (file_exists($avatarpath) == FALSE) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function getMediatype($ext)
+ {
+ $mediatype = null;
+
+ switch (strtolower($ext)) {
+ case 'jpg':
+ $mediatype = 'image/jpg';
+ break;
+ case 'gif':
+ $mediatype = 'image/gif';
+ break;
+ default:
+ $mediatype = 'image/png';
+ }
+
+ return $mediatype;
+ }
+
+ function saveAvatars($user, $id)
+ {
+ global $config;
+
+ $path_parts = pathinfo($user->profile_image_url);
+ $ext = $path_parts['extension'];
+ $end = strlen('_normal' . $ext);
+ $img_root = substr($path_parts['basename'], 0, -($end+1));
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ if ($this->fetchAvatar($url, $filename)) {
+ $this->newAvatar($id, $size, $mediatype, $filename);
+ } else {
+ common_log(LOG_WARNING, $this->id() .
+ " - Problem fetching Avatar: $url");
+ }
+ }
+ }
+
+ function updateAvatar($profile_id, $size, $mediatype, $filename) {
+
+ common_debug($this->name() . " - Updating avatar: $size");
+
+ $profile = Profile::staticGet($profile_id);
+
+ if (empty($profile)) {
+ common_debug($this->name() . " - Couldn't get profile: $profile_id!");
+ return;
+ }
+
+ $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
+ $avatar = $profile->getAvatar($sizes[$size]);
+
+ // Delete the avatar, if present
+
+ if ($avatar) {
+ $avatar->delete();
+ }
+
+ $this->newAvatar($profile->id, $size, $mediatype, $filename);
+ }
+
+ function newAvatar($profile_id, $size, $mediatype, $filename)
+ {
+ global $config;
+
+ $avatar = new Avatar();
+ $avatar->profile_id = $profile_id;
+
+ switch($size) {
+ case 'mini':
+ $avatar->width = 24;
+ $avatar->height = 24;
+ break;
+ case 'normal':
+ $avatar->width = 48;
+ $avatar->height = 48;
+ break;
+ default:
+
+ // Note: Twitter's big avatars are a different size than
+ // StatusNet's (StatusNet's = 96)
+
+ $avatar->width = 73;
+ $avatar->height = 73;
+ }
+
+ $avatar->original = 0; // we don't have the original
+ $avatar->mediatype = $mediatype;
+ $avatar->filename = $filename;
+ $avatar->url = Avatar::url($filename);
+
+ $avatar->created = common_sql_now();
+
+ $id = $avatar->insert();
+
+ if (empty($id)) {
+ common_log_db_error($avatar, 'INSERT', __FILE__);
+ return null;
+ }
+
+ common_debug($this->name() .
+ " - Saved new $size avatar for $profile_id.");
+
+ return $id;
+ }
+
+ /**
+ * Fetch a remote avatar image and save to local storage.
+ *
+ * @param string $url avatar source URL
+ * @param string $filename bare local filename for download
+ * @return bool true on success, false on failure
+ */
+ function fetchAvatar($url, $filename)
+ {
+ common_debug($this->name() . " - Fetching Twitter avatar: $url");
+
+ $request = HTTPClient::start();
+ $response = $request->get($url);
+ if ($response->isOk()) {
+ $avatarfile = Avatar::path($filename);
+ $ok = file_put_contents($avatarfile, $response->getBody());
+ if (!$ok) {
+ common_log(LOG_WARNING, $this->name() .
+ " - Couldn't open file $filename");
+ return false;
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+}
+
+$id = null;
+$debug = null;
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+if (have_option('d') || have_option('debug')) {
+ $debug = true;
+}
+
+$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
+$fetcher->runOnce();
+
diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php
new file mode 100644
index 000000000..3c6803e49
--- /dev/null
+++ b/plugins/TwitterBridge/twitter.php
@@ -0,0 +1,351 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1
+
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
+
+function updateTwitter_user($twitter_id, $screen_name)
+{
+ $uri = 'http://twitter.com/' . $screen_name;
+ $fuser = new Foreign_user();
+
+ $fuser->query('BEGIN');
+
+ // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem
+ // to work so good with tables that have multiple column primary keys
+
+ // Any time we update the uri for a forein user we have to make sure there
+ // are no dupe entries first -- unique constraint on the uri column
+
+ $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = ';
+ $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE;
+
+ $fuser->query($qry);
+
+ // Update the user
+
+ $qry = 'UPDATE foreign_user SET nickname = ';
+ $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' ';
+ $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE;
+
+ $fuser->query('COMMIT');
+
+ $fuser->free();
+ unset($fuser);
+
+ return true;
+}
+
+function add_twitter_user($twitter_id, $screen_name)
+{
+
+ $new_uri = 'http://twitter.com/' . $screen_name;
+
+ // Clear out any bad old foreign_users with the new user's legit URL
+ // This can happen when users move around or fakester accounts get
+ // repoed, and things like that.
+
+ $luser = new Foreign_user();
+ $luser->uri = $new_uri;
+ $luser->service = TWITTER_SERVICE;
+ $result = $luser->delete();
+
+ if (empty($result)) {
+ common_log(LOG_WARNING,
+ "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri");
+ }
+
+ $luser->free();
+ unset($luser);
+
+ // Otherwise, create a new Twitter user
+
+ $fuser = new Foreign_user();
+
+ $fuser->nickname = $screen_name;
+ $fuser->uri = 'http://twitter.com/' . $screen_name;
+ $fuser->id = $twitter_id;
+ $fuser->service = TWITTER_SERVICE;
+ $fuser->created = common_sql_now();
+ $result = $fuser->insert();
+
+ if (empty($result)) {
+ common_log(LOG_WARNING,
+ "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name.");
+ common_log_db_error($fuser, 'INSERT', __FILE__);
+ } else {
+ common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id).");
+ }
+
+ return $result;
+}
+
+// Creates or Updates a Twitter user
+function save_twitter_user($twitter_id, $screen_name)
+{
+
+ // Check to see whether the Twitter user is already in the system,
+ // and update its screen name and uri if so.
+
+ $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
+
+ if (!empty($fuser)) {
+
+ $result = true;
+
+ // Only update if Twitter screen name has changed
+
+ if ($fuser->nickname != $screen_name) {
+ $result = updateTwitter_user($twitter_id, $screen_name);
+
+ common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' .
+ "$fuser->id to $screen_name, was $fuser->nickname");
+ }
+
+ return $result;
+
+ } else {
+ return add_twitter_user($twitter_id, $screen_name);
+ }
+
+ $fuser->free();
+ unset($fuser);
+
+ return true;
+}
+
+function is_twitter_bound($notice, $flink) {
+
+ // Check to see if notice should go to Twitter
+ if (!empty($flink) && ($flink->noticesync & FOREIGN_NOTICE_SEND)) {
+
+ // If it's not a Twitter-style reply, or if the user WANTS to send replies.
+ if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function broadcast_twitter($notice)
+{
+ $flink = Foreign_link::getByUserID($notice->profile_id,
+ TWITTER_SERVICE);
+
+ if (is_twitter_bound($notice, $flink)) {
+ if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
+ return broadcast_oauth($notice, $flink);
+ } else {
+ return broadcast_basicauth($notice, $flink);
+ }
+ }
+
+ return true;
+}
+
+function broadcast_oauth($notice, $flink) {
+ $user = $flink->getUser();
+ $statustxt = format_status($notice);
+ // Convert !groups to #hashes
+ $statustxt = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/', "\\1#\\2", $statustxt);
+ $token = TwitterOAuthClient::unpackToken($flink->credentials);
+ $client = new TwitterOAuthClient($token->key, $token->secret);
+ $status = null;
+
+ try {
+ $status = $client->statusesUpdate($statustxt);
+ } catch (OAuthClientCurlException $e) {
+ return process_error($e, $flink);
+ }
+
+ if (empty($status)) {
+
+ // This could represent a failure posting,
+ // or the Twitter API might just be behaving flakey.
+
+ $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' .
+ 'trying to send update for %1$s (user id %2$s).',
+ $user->nickname, $user->id);
+ common_log(LOG_WARNING, $errmsg);
+
+ return false;
+ }
+
+ // Notice crossed the great divide
+
+ $msg = sprintf('Twitter bridge - posted notice %s to Twitter using OAuth.',
+ $notice->id);
+ common_log(LOG_INFO, $msg);
+
+ return true;
+}
+
+function broadcast_basicauth($notice, $flink)
+{
+ $user = $flink->getUser();
+
+ $statustxt = format_status($notice);
+
+ $client = new TwitterBasicAuthClient($flink);
+ $status = null;
+
+ try {
+ $status = $client->statusesUpdate($statustxt);
+ } catch (HTTP_Request2_Exception $e) {
+ return process_error($e, $flink);
+ }
+
+ if (empty($status)) {
+
+ $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' .
+ 'trying to send update for %1$s (user id %2$s).',
+ $user->nickname, $user->id);
+ common_log(LOG_WARNING, $errmsg);
+
+ $errmsg = sprintf('No data returned by Twitter API when ' .
+ 'trying to send update for %1$s (user id %2$s).',
+ $user->nickname, $user->id);
+ common_log(LOG_WARNING, $errmsg);
+ return false;
+ }
+
+ $msg = sprintf('Twitter bridge - posted notice %s to Twitter using basic auth.',
+ $notice->id);
+ common_log(LOG_INFO, $msg);
+
+ return true;
+}
+
+function process_error($e, $flink)
+{
+ $user = $flink->getUser();
+ $errmsg = $e->getMessage();
+ $delivered = false;
+
+ switch($errmsg) {
+ case 'The requested URL returned error: 401':
+ $logmsg = sprintf('Twiter bridge - User %1$s (user id: %2$s) has an invalid ' .
+ 'Twitter screen_name/password combo or an invalid acesss token.',
+ $user->nickname, $user->id);
+ $delivered = true;
+ remove_twitter_link($flink);
+ break;
+ case 'The requested URL returned error: 403':
+ $logmsg = sprintf('Twitter bridge - User %1$s (user id: %2$s) has exceeded ' .
+ 'his/her Twitter request limit.',
+ $user->nickname, $user->id);
+ break;
+ default:
+ $logmsg = sprintf('Twitter bridge - cURL error trying to send notice to Twitter ' .
+ 'for user %1$s (user id: %2$s) - ' .
+ 'code: %3$s message: %4$s.',
+ $user->nickname, $user->id,
+ $e->getCode(), $e->getMessage());
+ break;
+ }
+
+ common_log(LOG_WARNING, $logmsg);
+
+ return $delivered;
+}
+
+function format_status($notice)
+{
+ // XXX: Hack to get around PHP cURL's use of @ being a a meta character
+ return preg_replace('/^@/', ' @', $notice->content);
+}
+
+function remove_twitter_link($flink)
+{
+ $user = $flink->getUser();
+
+ common_log(LOG_INFO, 'Removing Twitter bridge Foreign link for ' .
+ "user $user->nickname (user id: $user->id).");
+
+ $result = $flink->delete();
+
+ if (empty($result)) {
+ common_log(LOG_ERR, 'Could not remove Twitter bridge ' .
+ "Foreign_link for $user->nickname (user id: $user->id)!");
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ }
+
+ // Notify the user that her Twitter bridge is down
+
+ if (isset($user->email)) {
+
+ $result = mail_twitter_bridge_removed($user);
+
+ if (!$result) {
+
+ $msg = 'Unable to send email to notify ' .
+ "$user->nickname (user id: $user->id) " .
+ 'that their Twitter bridge link was ' .
+ 'removed!';
+
+ common_log(LOG_WARNING, $msg);
+ }
+ }
+
+}
+
+/**
+ * Send a mail message to notify a user that her Twitter bridge link
+ * has stopped working, and therefore has been removed. This can
+ * happen when the user changes her Twitter password, or otherwise
+ * revokes access.
+ *
+ * @param User $user user whose Twitter bridge link has been removed
+ *
+ * @return boolean success flag
+ */
+
+function mail_twitter_bridge_removed($user)
+{
+ common_init_locale($user->language);
+
+ $profile = $user->getProfile();
+
+ $subject = sprintf(_('Your Twitter bridge has been disabled.'));
+
+ $site_name = common_config('site', 'name');
+
+ $body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' .
+ 'link to Twitter has been disabled. We no longer seem to have ' .
+ 'permission to update your Twitter status. (Did you revoke ' .
+ '%3$s\'s access?)' . "\n\n" .
+ 'You can re-enable your Twitter bridge by visiting your ' .
+ "Twitter settings page:\n\n\t%2\$s\n\n" .
+ "Regards,\n%3\$s\n"),
+ $profile->getBestName(),
+ common_local_url('twittersettings'),
+ common_config('site', 'name'));
+
+ common_init_locale();
+ return mail_to_user($user, $subject, $body);
+}
+
diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php
new file mode 100644
index 000000000..f1daefab1
--- /dev/null
+++ b/plugins/TwitterBridge/twitterauthorization.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth authentication against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category TwitterauthorizationAction
+ * @package StatusNet
+ * @author Zach Copely <zach@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+
+/**
+ * Class for doing OAuth authentication against Twitter
+ *
+ * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
+ * authorizes it, and exchanges it for an access token. It also creates a link
+ * (Foreign_link) between the StatusNet user and Twitter user and stores the
+ * access token and secret in the link.
+ *
+ * @category Twitter
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ */
+class TwitterauthorizationAction extends Action
+{
+ /**
+ * Initialize class members. Looks for 'oauth_token' parameter.
+ *
+ * @param array $args misc. arguments
+ *
+ * @return boolean true
+ */
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->oauth_token = $this->arg('oauth_token');
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $args is ignored since it's now passed in in prepare()
+ *
+ * @return nothing
+ */
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'), 403);
+ }
+
+ $user = common_current_user();
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+ // If there's already a foreign link record, it means we already
+ // have an access token, and this is unecessary. So go back.
+
+ if (isset($flink)) {
+ common_redirect(common_local_url('twittersettings'));
+ }
+
+ // $this->oauth_token is only populated once Twitter authorizes our
+ // request token. If it's empty we're at the beginning of the auth
+ // process
+
+ if (empty($this->oauth_token)) {
+ $this->authorizeRequestToken();
+ } else {
+ $this->saveAccessToken();
+ }
+ }
+
+ /**
+ * Asks Twitter for a request token, and then redirects to Twitter
+ * to authorize it.
+ *
+ * @return nothing
+ */
+ function authorizeRequestToken()
+ {
+ try {
+
+ // Get a new request token and authorize it
+
+ $client = new TwitterOAuthClient();
+ $req_tok =
+ $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
+
+ // Sock the request token away in the session temporarily
+
+ $_SESSION['twitter_request_token'] = $req_tok->key;
+ $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
+
+ $auth_link = $client->getAuthorizeLink($req_tok);
+
+ } catch (OAuthClientException $e) {
+ $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
+ $e->getCode(), $e->getMessage());
+ $this->serverError(_('Couldn\'t link your Twitter account.'));
+ }
+
+ common_redirect($auth_link);
+ }
+
+ /**
+ * Called when Twitter returns an authorized request token. Exchanges
+ * it for an access token and stores it.
+ *
+ * @return nothing
+ */
+ function saveAccessToken()
+ {
+
+ // Check to make sure Twitter returned the same request
+ // token we sent them
+
+ if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
+ $this->serverError(_('Couldn\'t link your Twitter account.'));
+ }
+
+ try {
+
+ $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
+ $_SESSION['twitter_request_token_secret']);
+
+ // Exchange the request token for an access token
+
+ $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
+
+ // Test the access token and get the user's Twitter info
+
+ $client = new TwitterOAuthClient($atok->key, $atok->secret);
+ $twitter_user = $client->verifyCredentials();
+
+ } catch (OAuthClientException $e) {
+ $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
+ $e->getCode(), $e->getMessage());
+ $this->serverError(_('Couldn\'t link your Twitter account.'));
+ }
+
+ // Save the access token and Twitter user info
+
+ $this->saveForeignLink($atok, $twitter_user);
+
+ // Clean up the the mess we made in the session
+
+ unset($_SESSION['twitter_request_token']);
+ unset($_SESSION['twitter_request_token_secret']);
+
+ common_redirect(common_local_url('twittersettings'));
+ }
+
+ /**
+ * Saves a Foreign_link between Twitter user and local user,
+ * which includes the access token and secret.
+ *
+ * @param OAuthToken $access_token the access token to save
+ * @param mixed $twitter_user twitter API user object
+ *
+ * @return nothing
+ */
+ function saveForeignLink($access_token, $twitter_user)
+ {
+ $user = common_current_user();
+
+ $flink = new Foreign_link();
+
+ $flink->user_id = $user->id;
+ $flink->foreign_id = $twitter_user->id;
+ $flink->service = TWITTER_SERVICE;
+
+ $creds = TwitterOAuthClient::packToken($access_token);
+
+ $flink->credentials = $creds;
+ $flink->created = common_sql_now();
+
+ // Defaults: noticesync on, everything else off
+
+ $flink->set_flags(true, false, false, false);
+
+ $flink_id = $flink->insert();
+
+ if (empty($flink_id)) {
+ common_log_db_error($flink, 'INSERT', __FILE__);
+ $this->serverError(_('Couldn\'t link your Twitter account.'));
+ }
+
+ save_twitter_user($twitter_user->id, $twitter_user->screen_name);
+ }
+
+}
+
diff --git a/plugins/TwitterBridge/twitterbasicauthclient.php b/plugins/TwitterBridge/twitterbasicauthclient.php
new file mode 100644
index 000000000..d1cf45aec
--- /dev/null
+++ b/plugins/TwitterBridge/twitterbasicauthclient.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth calls against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Integration
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Class for talking to the Twitter API with HTTP Basic Auth.
+ *
+ * @category Integration
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ */
+class TwitterBasicAuthClient
+{
+ var $screen_name = null;
+ var $password = null;
+
+ /**
+ * constructor
+ *
+ * @param Foreign_link $flink a Foreign_link storing the
+ * Twitter user's password, etc.
+ */
+ function __construct($flink)
+ {
+ $fuser = $flink->getForeignUser();
+ $this->screen_name = $fuser->nickname;
+ $this->password = $flink->credentials;
+ }
+
+ /**
+ * Calls Twitter's /statuses/update API method
+ *
+ * @param string $status text of the status
+ * @param int $in_reply_to_status_id optional id of the status it's
+ * a reply to
+ *
+ * @return mixed the status
+ */
+ function statusesUpdate($status, $in_reply_to_status_id = null)
+ {
+ $url = 'https://twitter.com/statuses/update.json';
+ $params = array('status' => $status,
+ 'source' => common_config('integration', 'source'),
+ 'in_reply_to_status_id' => $in_reply_to_status_id);
+ $response = $this->httpRequest($url, $params);
+ $status = json_decode($response);
+ return $status;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends_timeline API method
+ *
+ * @param int $since_id show statuses after this id
+ * @param int $max_id show statuses before this id
+ * @param int $cnt number of statuses to show
+ * @param int $page page number
+ *
+ * @return mixed an array of statuses
+ */
+ function statusesFriendsTimeline($since_id = null, $max_id = null,
+ $cnt = null, $page = null)
+ {
+ $url = 'https://twitter.com/statuses/friends_timeline.json';
+ $params = array('since_id' => $since_id,
+ 'max_id' => $max_id,
+ 'count' => $cnt,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->httpRequest($url);
+ $statuses = json_decode($response);
+ return $statuses;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends API method
+ *
+ * @param int $id id of the user whom you wish to see friends of
+ * @param int $user_id numerical user id
+ * @param int $screen_name screen name
+ * @param int $page page number
+ *
+ * @return mixed an array of twitter users and their latest status
+ */
+ function statusesFriends($id = null, $user_id = null, $screen_name = null,
+ $page = null)
+ {
+ $url = "https://twitter.com/statuses/friends.json";
+
+ $params = array('id' => $id,
+ 'user_id' => $user_id,
+ 'screen_name' => $screen_name,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->httpRequest($url);
+ $friends = json_decode($response);
+ return $friends;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends/ids API method
+ *
+ * @param int $id id of the user whom you wish to see friends of
+ * @param int $user_id numerical user id
+ * @param int $screen_name screen name
+ * @param int $page page number
+ *
+ * @return mixed a list of ids, 100 per page
+ */
+ function friendsIds($id = null, $user_id = null, $screen_name = null,
+ $page = null)
+ {
+ $url = "https://twitter.com/friends/ids.json";
+
+ $params = array('id' => $id,
+ 'user_id' => $user_id,
+ 'screen_name' => $screen_name,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->httpRequest($url);
+ $ids = json_decode($response);
+ return $ids;
+ }
+
+ /**
+ * Make a HTTP request using cURL.
+ *
+ * @param string $url Where to make the request
+ * @param array $params post parameters
+ *
+ * @return mixed the request
+ */
+ function httpRequest($url, $params = null, $auth = true)
+ {
+ $request = HTTPClient::start();
+ $request->setConfig(array(
+ 'follow_redirects' => true,
+ 'connect_timeout' => 120,
+ 'timeout' => 120,
+ 'ssl_verifypeer' => false,
+ ));
+
+ if ($auth) {
+ $request->setAuth($this->screen_name, $this->password);
+ }
+
+ if (isset($params)) {
+ // Twitter is strict about accepting invalid "Expect" headers
+ $headers = array('Expect:');
+ $response = $request->post($url, $headers, $params);
+ } else {
+ $response = $request->get($url);
+ }
+
+ return $response->getBody();
+ }
+
+}
diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php
new file mode 100644
index 000000000..bad2b74ca
--- /dev/null
+++ b/plugins/TwitterBridge/twitteroauthclient.php
@@ -0,0 +1,229 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth calls against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Integration
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Class for talking to the Twitter API with OAuth.
+ *
+ * @category Integration
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ */
+class TwitterOAuthClient extends OAuthClient
+{
+ public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
+ public static $authorizeURL = 'https://twitter.com/oauth/authorize';
+ public static $accessTokenURL = 'https://twitter.com/oauth/access_token';
+
+ /**
+ * Constructor
+ *
+ * @param string $oauth_token the user's token
+ * @param string $oauth_token_secret the user's token secret
+ *
+ * @return nothing
+ */
+ function __construct($oauth_token = null, $oauth_token_secret = null)
+ {
+ $consumer_key = common_config('twitter', 'consumer_key');
+ $consumer_secret = common_config('twitter', 'consumer_secret');
+
+ parent::__construct($consumer_key, $consumer_secret,
+ $oauth_token, $oauth_token_secret);
+ }
+
+ // XXX: the following two functions are to support the horrible hack
+ // of using the credentils field in Foreign_link to store both
+ // the access token and token secret. This hack should go away with
+ // 0.9, in which we can make DB changes and add a new column for the
+ // token itself.
+
+ static function packToken($token)
+ {
+ return implode(chr(0), array($token->key, $token->secret));
+ }
+
+ static function unpackToken($str)
+ {
+ $vals = explode(chr(0), $str);
+ return new OAuthToken($vals[0], $vals[1]);
+ }
+
+ static function isPackedToken($str)
+ {
+ if (strpos($str, chr(0)) === false) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Builds a link to Twitter's endpoint for authorizing a request token
+ *
+ * @param OAuthToken $request_token token to authorize
+ *
+ * @return the link
+ */
+ function getAuthorizeLink($request_token)
+ {
+ return parent::getAuthorizeLink(self::$authorizeURL,
+ $request_token,
+ common_local_url('twitterauthorization'));
+ }
+
+ /**
+ * Calls Twitter's /account/verify_credentials API method
+ *
+ * @return mixed the Twitter user
+ */
+ function verifyCredentials()
+ {
+ $url = 'https://twitter.com/account/verify_credentials.json';
+ $response = $this->oAuthGet($url);
+ $twitter_user = json_decode($response);
+ return $twitter_user;
+ }
+
+ /**
+ * Calls Twitter's /statuses/update API method
+ *
+ * @param string $status text of the status
+ * @param int $in_reply_to_status_id optional id of the status it's
+ * a reply to
+ *
+ * @return mixed the status
+ */
+ function statusesUpdate($status, $in_reply_to_status_id = null)
+ {
+ $url = 'https://twitter.com/statuses/update.json';
+ $params = array('status' => $status,
+ 'in_reply_to_status_id' => $in_reply_to_status_id);
+ $response = $this->oAuthPost($url, $params);
+ $status = json_decode($response);
+ return $status;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends_timeline API method
+ *
+ * @param int $since_id show statuses after this id
+ * @param int $max_id show statuses before this id
+ * @param int $cnt number of statuses to show
+ * @param int $page page number
+ *
+ * @return mixed an array of statuses
+ */
+ function statusesFriendsTimeline($since_id = null, $max_id = null,
+ $cnt = null, $page = null)
+ {
+
+ $url = 'https://twitter.com/statuses/friends_timeline.json';
+ $params = array('since_id' => $since_id,
+ 'max_id' => $max_id,
+ 'count' => $cnt,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->oAuthGet($url);
+ $statuses = json_decode($response);
+ return $statuses;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends API method
+ *
+ * @param int $id id of the user whom you wish to see friends of
+ * @param int $user_id numerical user id
+ * @param int $screen_name screen name
+ * @param int $page page number
+ *
+ * @return mixed an array of twitter users and their latest status
+ */
+ function statusesFriends($id = null, $user_id = null, $screen_name = null,
+ $page = null)
+ {
+ $url = "https://twitter.com/statuses/friends.json";
+
+ $params = array('id' => $id,
+ 'user_id' => $user_id,
+ 'screen_name' => $screen_name,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->oAuthGet($url);
+ $friends = json_decode($response);
+ return $friends;
+ }
+
+ /**
+ * Calls Twitter's /statuses/friends/ids API method
+ *
+ * @param int $id id of the user whom you wish to see friends of
+ * @param int $user_id numerical user id
+ * @param int $screen_name screen name
+ * @param int $page page number
+ *
+ * @return mixed a list of ids, 100 per page
+ */
+ function friendsIds($id = null, $user_id = null, $screen_name = null,
+ $page = null)
+ {
+ $url = "https://twitter.com/friends/ids.json";
+
+ $params = array('id' => $id,
+ 'user_id' => $user_id,
+ 'screen_name' => $screen_name,
+ 'page' => $page);
+ $qry = http_build_query($params);
+
+ if (!empty($qry)) {
+ $url .= "?$qry";
+ }
+
+ $response = $this->oAuthGet($url);
+ $ids = json_decode($response);
+ return $ids;
+ }
+
+}
diff --git a/plugins/TwitterBridge/twittersettings.php b/plugins/TwitterBridge/twittersettings.php
new file mode 100644
index 000000000..ca22c9553
--- /dev/null
+++ b/plugins/TwitterBridge/twittersettings.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Settings for Twitter integration
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/connectsettingsaction.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+
+/**
+ * Settings for Twitter integration
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see SettingsAction
+ */
+
+class TwittersettingsAction extends ConnectSettingsAction
+{
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Twitter settings');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Connect your Twitter account to share your updates ' .
+ 'with your Twitter friends and vice-versa.');
+ }
+
+ /**
+ * Content area of the page
+ *
+ * Shows a form for associating a Twitter account with this
+ * StatusNet account. Also lets the user set preferences.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+
+ $user = common_current_user();
+
+ $profile = $user->getProfile();
+
+ $fuser = null;
+
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+ if (!empty($flink)) {
+ $fuser = $flink->getForeignUser();
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_twitter',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('twittersettings')));
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', array('id' => 'settings_twitter_account'));
+
+ if (empty($fuser)) {
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+ $this->element('a', array('href' => common_local_url('twitterauthorization')),
+ 'Connect my Twitter account');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->elementEnd('fieldset');
+ } else {
+ $this->element('legend', null, _('Twitter account'));
+ $this->elementStart('p', array('id' => 'form_confirmed'));
+ $this->element('a', array('href' => $fuser->uri), $fuser->nickname);
+ $this->elementEnd('p');
+ $this->element('p', 'form_note',
+ _('Connected Twitter account'));
+
+ $this->submit('remove', _('Remove'));
+
+ $this->elementEnd('fieldset');
+
+ $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences'));
+
+ $this->element('legend', null, _('Preferences'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->checkbox('noticesend',
+ _('Automatically send my notices to Twitter.'),
+ ($flink) ?
+ ($flink->noticesync & FOREIGN_NOTICE_SEND) :
+ true);
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->checkbox('replysync',
+ _('Send local "@" replies to Twitter.'),
+ ($flink) ?
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) :
+ true);
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->checkbox('friendsync',
+ _('Subscribe to my Twitter friends here.'),
+ ($flink) ?
+ ($flink->friendsync & FOREIGN_FRIEND_RECV) :
+ false);
+ $this->elementEnd('li');
+
+ if (common_config('twitterimport','enabled')) {
+ $this->elementStart('li');
+ $this->checkbox('noticerecv',
+ _('Import my Friends Timeline.'),
+ ($flink) ?
+ ($flink->noticesync & FOREIGN_NOTICE_RECV) :
+ false);
+ $this->elementEnd('li');
+ } else {
+ // preserve setting even if bidrection bridge toggled off
+
+ if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
+ $this->hidden('noticerecv', true, 'noticerecv');
+ }
+ }
+
+ $this->elementEnd('ul');
+
+ if ($flink) {
+ $this->submit('save', _('Save'));
+ } else {
+ $this->submit('add', _('Add'));
+ }
+
+ $this->elementEnd('fieldset');
+ }
+
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('save')) {
+ $this->savePreferences();
+ } else if ($this->arg('remove')) {
+ $this->removeTwitterAccount();
+ } else {
+ $this->showForm(_('Unexpected form submission.'));
+ }
+ }
+
+ /**
+ * Disassociate an existing Twitter account from this account
+ *
+ * @return void
+ */
+
+ function removeTwitterAccount()
+ {
+ $user = common_current_user();
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+ $result = $flink->delete();
+
+ if (empty($result)) {
+ common_log_db_error($flink, 'DELETE', __FILE__);
+ $this->serverError(_('Couldn\'t remove Twitter user.'));
+ return;
+ }
+
+ $this->showForm(_('Twitter account removed.'), true);
+ }
+
+ /**
+ * Save user's Twitter-bridging preferences
+ *
+ * @return void
+ */
+
+ function savePreferences()
+ {
+ $noticesend = $this->boolean('noticesend');
+ $noticerecv = $this->boolean('noticerecv');
+ $friendsync = $this->boolean('friendsync');
+ $replysync = $this->boolean('replysync');
+
+ $user = common_current_user();
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+ if (empty($flink)) {
+ common_log_db_error($flink, 'SELECT', __FILE__);
+ $this->showForm(_('Couldn\'t save Twitter preferences.'));
+ return;
+ }
+
+ $original = clone($flink);
+ $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
+ $result = $flink->update($original);
+
+ if ($result === false) {
+ common_log_db_error($flink, 'UPDATE', __FILE__);
+ $this->showForm(_('Couldn\'t save Twitter preferences.'));
+ return;
+ }
+
+ $this->showForm(_('Twitter preferences saved.'), true);
+ }
+
+}
diff --git a/plugins/UrlShortener/UrlShortenerPlugin.php b/plugins/UrlShortener/UrlShortenerPlugin.php
new file mode 100644
index 000000000..37206aa89
--- /dev/null
+++ b/plugins/UrlShortener/UrlShortenerPlugin.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Superclass for plugins that do URL shortening
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Superclass for plugins that do URL shortening
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+abstract class UrlShortenerPlugin extends Plugin
+{
+ public $shortenerName;
+ public $freeService=false;
+ //------------Url Shortener plugin should implement some (or all) of these methods------------\\
+
+ /**
+ * Short a URL
+ * @param url
+ * @return string shortened version of the url, or null if URL shortening failed
+ */
+ protected abstract function shorten($url);
+
+ //------------These methods may help you implement your plugin------------\\
+ protected function http_get($url)
+ {
+ $request = HTTPClient::start();
+ $response = $request->get($url);
+ return $response->getBody();
+ }
+
+ protected function http_post($url,$data)
+ {
+ $request = HTTPClient::start();
+ $response = $request->post($url, null, $data);
+ return $response->getBody();
+ }
+
+ protected function tidy($response) {
+ $response = str_replace('&nbsp;', ' ', $response);
+ $config = array('output-xhtml' => true);
+ $tidy = new tidy;
+ $tidy->parseString($response, $config, 'utf8');
+ $tidy->cleanRepair();
+ return (string)$tidy;
+ }
+ //------------Below are the methods that connect StatusNet to the implementing Url Shortener plugin------------\\
+
+ function onInitializePlugin(){
+ if(!isset($this->shortenerName)){
+ throw new Exception("must specify a shortenerName");
+ }
+ }
+
+ function onGetUrlShorteners(&$shorteners)
+ {
+ $shorteners[$this->shortenerName]=array('freeService'=>$this->freeService);
+ }
+
+ function onStartShortenUrl($url,$shortenerName,&$shortenedUrl)
+ {
+ if($shortenerName == $this->shortenerName && strlen($url) >= common_config('site', 'shorturllength')){
+ $result = $this->shorten($url);
+ if(isset($result) && $result != null && $result !== false){
+ $shortenedUrl=$result;
+ common_log(LOG_INFO, __CLASS__ . ": $this->shortenerName shortened $url to $shortenedUrl");
+ return false;
+ }
+ }
+ }
+}
diff --git a/plugins/UserFlag/UserFlagPlugin.php b/plugins/UserFlag/UserFlagPlugin.php
new file mode 100644
index 000000000..97b999a2f
--- /dev/null
+++ b/plugins/UserFlag/UserFlagPlugin.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Allows users to flag content and accounts as offensive/spam/whatever
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Allows users to flag content and accounts as offensive/spam/whatever
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class UserFlagPlugin extends Plugin
+{
+ function onCheckSchema()
+ {
+ $schema = Schema::get();
+
+ // For storing user-submitted flags on profiles
+ $schema->ensureDataObject('User_flag_profile');
+
+ return true;
+ }
+
+ function onInitializePlugin()
+ {
+ // XXX: do something here?
+ return true;
+ }
+
+ function onRouterInitialized(&$m) {
+ $m->connect('main/flag/profile', array('action' => 'flagprofile'));
+ $m->connect('admin/profile/flag', array('action' => 'adminprofileflag'));
+ return true;
+ }
+
+ function onAutoload($cls)
+ {
+ switch ($cls)
+ {
+ case 'FlagprofileAction':
+ case 'AdminprofileflagAction':
+ require_once(INSTALLDIR.'/plugins/UserFlag/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
+ return false;
+ case 'FlagProfileForm':
+ require_once(INSTALLDIR.'/plugins/UserFlag/' . strtolower($cls . '.php'));
+ return false;
+ case 'User_flag_profile':
+ require_once(INSTALLDIR.'/plugins/UserFlag/'.$cls.'.php');
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ function onEndProfilePageActionsElements(&$action, $profile)
+ {
+ $user = common_current_user();
+
+ if (!empty($user) && ($user->id != $profile->id)) {
+
+ $action->elementStart('li', 'entity_flag');
+
+ if (User_flag_profile::exists($profile->id, $user->id)) {
+ $action->element('p', 'flagged', _('Flagged'));
+ } else {
+ $form = new FlagProfileForm($action, $profile,
+ array('action' => 'showstream',
+ 'nickname' => $profile->nickname));
+ $form->show();
+ }
+
+ $action->elementEnd('li');
+ }
+
+ return true;
+ }
+
+ function onEndProfileListItemActionElements($item)
+ {
+ $user = common_current_user();
+
+ if (!empty($user)) {
+
+ list($action, $args) = $item->action->returnToArgs();
+
+ $args['action'] = $action;
+
+ $form = new FlagProfileForm($item->action, $item->profile, $args);
+
+ $item->action->elementStart('li', 'entity_flag');
+ $form->show();
+ $item->action->elementEnd('li');
+ }
+
+ return true;
+ }
+
+ function onEndShowStatusNetStyles($action)
+ {
+ $action->elementStart('style', array('type' => 'text/css'));
+ $action->raw('.entity_flag input, .entity_flag p {'.
+ ' background:url('.common_path('plugins/UserFlag/flag.gif').') 5px 5px no-repeat;'.
+ ' }');
+ $action->elementEnd('style');
+
+ return true;
+ }
+
+ function onEndShowScripts($action)
+ {
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw('/*<![CDATA[*/ SN.U.FormXHR($(".form_entity_flag")); /*]]>*/');
+ $action->elementEnd('script');
+ return true;
+ }
+}
diff --git a/plugins/UserFlag/User_flag_profile.php b/plugins/UserFlag/User_flag_profile.php
new file mode 100644
index 000000000..2fb27912d
--- /dev/null
+++ b/plugins/UserFlag/User_flag_profile.php
@@ -0,0 +1,85 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/classes/Plugin_DataObject.php';
+
+class User_flag_profile extends Plugin_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'user_flag_profile'; // table name
+ public $profile_id; // int(4) primary_key not_null
+ public $user_id; // int(4) primary_key not_null
+ public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00
+ public $cleared; // datetime not_null default_0000-00-00%2000%3A00%3A00
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_flag_profile',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ function table() {
+ return array(
+ 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'cleared' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME
+ );
+ }
+
+ function keys() {
+ return array('profile_id' => 'N', 'user_id' => 'N');
+ }
+
+ function &pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('User_flag_profile', $kv);
+ }
+
+ static function exists($profile_id, $user_id)
+ {
+ $ufp = User_flag_profile::pkeyGet(array('profile_id' => $profile_id,
+ 'user_id' => $user_id));
+
+ return !empty($ufp);
+ }
+
+ /**
+ * Get the TableDef object that represents the table backing this class
+ * @return TableDef TableDef instance
+ */
+ function tableDef()
+ {
+ return new TableDef($this->__table,
+ array(new ColumnDef('profile_id', 'integer', null,
+ false, 'PRI'),
+ new ColumnDef('user_id', 'integer', null,
+ false, 'PRI'),
+ new ColumnDef('created', 'datetime', null,
+ false, 'MUL'),
+ new ColumnDef('cleared', 'datetime', null,
+ true, 'MUL')));
+ }
+}
diff --git a/plugins/UserFlag/adminprofileflag.php b/plugins/UserFlag/adminprofileflag.php
new file mode 100644
index 000000000..1ac76b506
--- /dev/null
+++ b/plugins/UserFlag/adminprofileflag.php
@@ -0,0 +1,201 @@
+<?php
+/**
+ * Show latest and greatest profile flags
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Show the latest and greatest profile flags
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+class AdminprofileflagAction extends Action
+{
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ return true;
+ }
+
+ /**
+ * Handle request
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ $this->showPage();
+ }
+
+ function title() {
+ return _('Flagged profiles');
+ }
+
+ /**
+ * save the profile flag
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $profile = $this->getProfiles();
+
+ $pl = new FlaggedProfileList($profile, $this);
+
+ $pl->show();
+ }
+
+ function getProfiles()
+ {
+ $ufp = new User_flag_profile();
+
+ $ufp->selectAdd();
+ $ufp->selectAdd('profile_id');
+ $ufp->selectAdd('count(*) as flag_count');
+
+ $ufp->whereAdd('cleared is NULL');
+
+ $ufp->groupBy('profile_id');
+ $ufp->orderBy('flag_count DESC');
+
+ $profiles = array();
+
+ if ($ufp->find()) {
+ while ($ufp->fetch()) {
+ $profile = Profile::staticGet('id', $ufp->profile_id);
+ if (!empty($profile)) {
+ $profiles[] = $profile;
+ }
+ }
+ }
+
+ $ufp->free();
+
+ return new ArrayWrapper($profiles);
+ }
+}
+
+class FlaggedProfileList extends ProfileList {
+
+ function newListItem($profile)
+ {
+ return new FlaggedProfileListItem($this->profile, $this->action);
+ }
+}
+
+class FlaggedProfileListItem extends ProfileListItem
+{
+ var $user = null;
+ var $r2args = null;
+
+ function showActions()
+ {
+ $this->user = common_current_user();
+
+ list($action, $this->r2args) = $this->out->returnToArgs();
+
+ $this->r2args['action'] = $action;
+
+ $this->startActions();
+ if (Event::handle('StartProfileListItemActionElements', array($this))) {
+ $this->showSandboxButton();
+ $this->showSilenceButton();
+ $this->showDeleteButton();
+ $this->showClearButton();
+ Event::handle('EndProfileListItemActionElements', array($this));
+ }
+ $this->endActions();
+ }
+
+ function showSandboxButton()
+ {
+ if ($this->user->hasRight(Right::SANDBOXUSER)) {
+ $this->out->elementStart('li', 'entity_sandbox');
+ if ($this->profile->isSandboxed()) {
+ $usf = new UnSandboxForm($this->out, $this->profile, $this->r2args);
+ $usf->show();
+ } else {
+ $sf = new SandboxForm($this->out, $this->profile, $this->r2args);
+ $sf->show();
+ }
+ $this->out->elementEnd('li');
+ }
+ }
+
+ function showSilenceButton()
+ {
+ if ($this->user->hasRight(Right::SILENCEUSER)) {
+ $this->out->elementStart('li', 'entity_silence');
+ if ($this->profile->isSilenced()) {
+ $usf = new UnSilenceForm($this->out, $this->profile, $this->r2args);
+ $usf->show();
+ } else {
+ $sf = new SilenceForm($this->out, $this->profile, $this->r2args);
+ $sf->show();
+ }
+ $this->out->elementEnd('li');
+ }
+ }
+
+ function showDeleteButton()
+ {
+
+ if ($this->user->hasRight(Right::DELETEUSER)) {
+ $this->out->elementStart('li', 'entity_delete');
+ $df = new DeleteUserForm($this->out, $this->profile, $this->r2args);
+ $df->show();
+ $this->out->elementEnd('li');
+ }
+ }
+
+ function showClearButton()
+ {
+ }
+}
diff --git a/plugins/UserFlag/flag.gif b/plugins/UserFlag/flag.gif
new file mode 100644
index 000000000..68c8aee25
--- /dev/null
+++ b/plugins/UserFlag/flag.gif
Binary files differ
diff --git a/plugins/UserFlag/flagprofile.php b/plugins/UserFlag/flagprofile.php
new file mode 100644
index 000000000..9bce7865b
--- /dev/null
+++ b/plugins/UserFlag/flagprofile.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Add a flag to a profile
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Action to flag a profile.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+class FlagprofileAction extends ProfileFormAction
+{
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ if (!parent::prepare($args)) {
+ return false;
+ }
+
+ $user = common_current_user();
+
+ assert(!empty($user)); // checked above
+ assert(!empty($this->profile)); // checked above
+
+ if (User_flag_profile::exists($this->profile->id,
+ $user->id))
+ {
+ $this->clientError(_('Flag already exists.'));
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Handle request
+ *
+ * Overriding the base Action's handle() here to deal check
+ * for Ajax and return an HXR response if necessary
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost();
+ if (!$this->boolean('ajax')) {
+ $this->returnToArgs();
+ }
+ }
+ }
+
+ /**
+ * Handle POST
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ $user = common_current_user();
+
+ assert(!empty($user));
+ assert(!empty($this->profile));
+
+ $ufp = new User_flag_profile();
+
+ $ufp->profile_id = $this->profile->id;
+ $ufp->user_id = $user->id;
+ $ufp->created = common_sql_now();
+
+ if (!$ufp->insert()) {
+ throw new ServerException(sprintf(_("Couldn't flag profile '%s' for review."),
+ $this->profile->nickname));
+ }
+
+ $ufp->free();
+
+ if ($this->boolean('ajax')) {
+ $this->ajaxResults();
+ }
+ }
+
+ function ajaxResults() {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ $this->element('title', null, _('Flagged for review'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->element('p', 'flagged', _('Flagged'));
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ }
+}
+
diff --git a/plugins/UserFlag/flagprofileform.php b/plugins/UserFlag/flagprofileform.php
new file mode 100644
index 000000000..c20929a20
--- /dev/null
+++ b/plugins/UserFlag/flagprofileform.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Form for flagging a profile
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/form.php';
+
+/**
+ * Form for flagging a profile
+ *
+ * A form for flagging a profile
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class FlagProfileForm extends ProfileActionForm
+{
+ /**
+ * class of the form
+ * Action this form provides
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_entity_flag';
+ }
+
+ /**
+ * Action this form provides
+ *
+ * @return string Name of the action, lowercased.
+ */
+
+ function target()
+ {
+ return 'flagprofile';
+ }
+
+ /**
+ * Title of the form
+ *
+ * @return string Title of the form, internationalized
+ */
+
+ function title()
+ {
+ return _('Flag');
+ }
+
+ /**
+ * Description of the form
+ *
+ * @return string description of the form, internationalized
+ */
+
+ function description()
+ {
+ return _('Flag profile for review');
+ }
+}
diff --git a/plugins/WikiHashtagsPlugin.php b/plugins/WikiHashtagsPlugin.php
index 0c5649aa4..334fc13ba 100644
--- a/plugins/WikiHashtagsPlugin.php
+++ b/plugins/WikiHashtagsPlugin.php
@@ -68,14 +68,13 @@ class WikiHashtagsPlugin extends Plugin
$editurl = sprintf('http://hashtags.wikia.com/index.php?title=%s&action=edit',
urlencode($tag));
- $context = stream_context_create(array('http' => array('method' => "GET",
- 'header' =>
- "User-Agent: " . $this->userAgent())));
- $html = @file_get_contents($url, false, $context);
+ $request = HTTPClient::start();
+ $response = $request->get($url);
+ $html = $response->getBody();
$action->elementStart('div', array('id' => 'wikihashtags', 'class' => 'section'));
- if (!empty($html)) {
+ if ($response->isOk() && !empty($html)) {
$action->element('style', null,
"span.editsection { display: none }\n".
"table.toc { display: none }");
@@ -100,10 +99,4 @@ class WikiHashtagsPlugin extends Plugin
return true;
}
-
- function userAgent()
- {
- return 'WikiHashtagsPlugin/'.WIKIHASHTAGSPLUGIN_VERSION .
- ' StatusNet/' . STATUSNET_VERSION;
- }
}
diff --git a/plugins/recaptcha/README b/plugins/recaptcha/README
deleted file mode 100644
index b996f96cc..000000000
--- a/plugins/recaptcha/README
+++ /dev/null
@@ -1,23 +0,0 @@
-StatusNet reCAPTCHA plugin 0.2 8/3/09
-====================================
-Adds a captcha to your registration page to reduce automated spam bots registering.
-
-Use:
-1. Get an API key from http://recaptcha.net
-
-2. In config.php add:
-include_once('plugins/recaptcha/recaptcha.php');
-$captcha = new recaptcha(publickey, privatekey, showErrors);
-
-Changelog
-=========
-0.1 initial release
-0.2 Work around for webkit browsers
-
-reCAPTCHA README
-================
-
-The reCAPTCHA PHP Lirary helps you use the reCAPTCHA API. Documentation
-for this library can be found at
-
- http://recaptcha.net/plugins/php