diff options
Diffstat (limited to 'plugins')
63 files changed, 4753 insertions, 1267 deletions
diff --git a/plugins/APCPlugin.php b/plugins/APCPlugin.php new file mode 100644 index 000000000..18409e29e --- /dev/null +++ b/plugins/APCPlugin.php @@ -0,0 +1,108 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Plugin to implement cache interface for APC variable cache + * + * PHP version 5 + * + * 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 Cache + * @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')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A plugin to use APC's variable cache for the cache interface + * + * New plugin interface lets us use alternative cache systems + * for caching. This one uses APC's variable cache. + * + * @category Cache + * @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/ + */ + +class APCPlugin extends Plugin +{ + /** + * Get a value associated with a key + * + * The value should have been set previously. + * + * @param string &$key in; Lookup key + * @param mixed &$value out; value associated with key + * + * @return boolean hook success + */ + + function onStartCacheGet(&$key, &$value) + { + $value = apc_fetch($key); + Event::handle('EndCacheGet', array($key, &$value)); + return false; + } + + /** + * Associate a value with a key + * + * @param string &$key in; Key to use for lookups + * @param mixed &$value in; Value to associate + * @param integer &$flag in; Flag (passed through to Memcache) + * @param integer &$expiry in; Expiry (passed through to Memcache) + * @param boolean &$success out; Whether the set was successful + * + * @return boolean hook success + */ + + function onStartCacheSet(&$key, &$value, &$flag, &$expiry, &$success) + { + $success = apc_store($key, $value, ((is_null($expiry)) ? 0 : $expiry)); + + Event::handle('EndCacheSet', array($key, $value, $flag, + $expiry)); + return false; + } + + /** + * Delete a value associated with a key + * + * @param string &$key in; Key to lookup + * @param boolean &$success out; whether it worked + * + * @return boolean hook success + */ + + function onStartCacheDelete(&$key, &$success) + { + $success = apc_delete($key); + Event::handle('EndCacheDelete', array($key)); + return false; + } +} + diff --git a/plugins/Authentication/AuthenticationPlugin.php b/plugins/Authentication/AuthenticationPlugin.php deleted file mode 100644 index a76848b04..000000000 --- a/plugins/Authentication/AuthenticationPlugin.php +++ /dev/null @@ -1,226 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Superclass for plugins that do authentication and/or 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 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->ensureTable('user_username', - 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'))); - 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 deleted file mode 100644 index 853fd5cb8..000000000 --- a/plugins/Authentication/User_username.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Table Definition for user_username - */ -require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; - -class User_username extends Memcached_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; - } - } - - function table() { - return array( - 'user_id' => DB_DATAOBJECT_INT, - 'username' => DB_DATAOBJECT_STR, - 'provider_name' => DB_DATAOBJECT_STR , - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME - ); - } - - // now define the keys. - function keys() { - return array('provider_name', 'username'); - } - -} diff --git a/plugins/Authorization/AuthorizationPlugin.php b/plugins/Authorization/AuthorizationPlugin.php deleted file mode 100644 index e4e046d08..000000000 --- a/plugins/Authorization/AuthorizationPlugin.php +++ /dev/null @@ -1,108 +0,0 @@ -<?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/BitlyUrl/BitlyUrlPlugin.php b/plugins/BitlyUrl/BitlyUrlPlugin.php index 65d0f70e6..f7f28b4d6 100644 --- a/plugins/BitlyUrl/BitlyUrlPlugin.php +++ b/plugins/BitlyUrl/BitlyUrlPlugin.php @@ -49,6 +49,18 @@ class BitlyUrlPlugin extends UrlShortenerPlugin if(!$response) return; return current(json_decode($response)->results)->hashUrl; } -} + function onPluginVersion(&$versions) + { + $versions[] = array('name' => sprintf('BitlyUrl (%s)', $this->shortenerName), + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:BitlyUrl', + 'rawdescription' => + sprintf(_m('Uses <a href="http://%1$s/">%1$s</a> URL-shortener service.'), + $this->shortenerName)); + + return true; + } +} diff --git a/plugins/Blacklist/BlacklistPlugin.php b/plugins/Blacklist/BlacklistPlugin.php index 655b0926b..84a2cb616 100644 --- a/plugins/Blacklist/BlacklistPlugin.php +++ b/plugins/Blacklist/BlacklistPlugin.php @@ -43,6 +43,8 @@ if (!defined('STATUSNET')) { class BlacklistPlugin extends Plugin { + const VERSION = STATUSNET_VERSION; + public $nicknames = array(); public $urls = array(); @@ -200,4 +202,15 @@ class BlacklistPlugin extends Plugin return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Blacklist', + 'version' => self::VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Blacklist', + 'description' => + _m('Keep a blacklist of forbidden nickname and URL patterns.')); + return true; + } } diff --git a/plugins/CacheLogPlugin.php b/plugins/CacheLogPlugin.php new file mode 100644 index 000000000..4c47de80e --- /dev/null +++ b/plugins/CacheLogPlugin.php @@ -0,0 +1,121 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Logs cache access + * + * PHP version 5 + * + * 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 Cache + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Log cache access + * + * Note that since most caching plugins return false for StartCache* + * methods, you should add this plugin before them, i.e. + * + * addPlugin('CacheLog'); + * addPlugin('XCache'); + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class CacheLogPlugin extends Plugin +{ + function onStartCacheGet(&$key, &$value) + { + $this->log(LOG_INFO, "Fetching key '$key'"); + return true; + } + + function onEndCacheGet($key, &$value) + { + if ($value === false) { + $this->log(LOG_INFO, "Cache MISS for key '$key'"); + } else { + $this->log(LOG_INFO, "Cache HIT for key '$key'"); + } + return true; + } + + function onStartCacheSet(&$key, &$value, &$flag, &$expiry, &$success) + { + if (empty($value)) { + if (is_array($value)) { + $this->log(LOG_INFO, "Setting empty array for key '$key'"); + } else if (is_null($value)) { + $this->log(LOG_INFO, "Setting null value for key '$key'"); + } else if (is_string($value)) { + $this->log(LOG_INFO, "Setting empty string for key '$key'"); + } else if (is_integer($value)) { + $this->log(LOG_INFO, "Setting integer 0 for key '$key'"); + } else { + $this->log(LOG_INFO, "Setting empty value '$value' for key '$key'"); + } + } else { + $this->log(LOG_INFO, "Setting non-empty value for key '$key'"); + } + return true; + } + + function onEndCacheSet($key, $value, $flag, $expiry) + { + $this->log(LOG_INFO, "Done setting cache value for key '$key'"); + return true; + } + + function onStartCacheDelete(&$key, &$success) + { + $this->log(LOG_INFO, "Deleting cache value for key '$key'"); + return true; + } + + function onEndCacheDelete($key) + { + $this->log(LOG_INFO, "Done deleting cache value for key '$key'"); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'CacheLog', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:CacheLog', + 'description' => + _m('Log reads and writes to the cache')); + return true; + } +} + diff --git a/plugins/CasAuthentication/CasAuthenticationPlugin.php b/plugins/CasAuthentication/CasAuthenticationPlugin.php index 428aafb02..818a11f77 100644 --- a/plugins/CasAuthentication/CasAuthenticationPlugin.php +++ b/plugins/CasAuthentication/CasAuthenticationPlugin.php @@ -39,6 +39,7 @@ class CasAuthenticationPlugin extends AuthenticationPlugin public $server; public $port = 443; public $path = ''; + public $takeOverLogin = false; function checkPassword($username, $password) { @@ -56,8 +57,14 @@ class CasAuthenticationPlugin extends AuthenticationPlugin case 'CasloginAction': require_once(INSTALLDIR.'/plugins/CasAuthentication/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); return false; - default: - return parent::onAutoload($cls); + } + } + + function onArgsInitialize(&$args) + { + if($this->takeOverLogin && $args['action'] == 'login') + { + $args['action'] = 'caslogin'; } } diff --git a/plugins/CasAuthentication/README b/plugins/CasAuthentication/README index 2ee54dc05..c17a28e54 100644 --- a/plugins/CasAuthentication/README +++ b/plugins/CasAuthentication/README @@ -21,6 +21,9 @@ password_changeable*: must be set to false. This plugin does not support changin server*: CAS server to authentication against port (443): Port the CAS server listens on. Almost always 443 path (): Path on the server to CAS. Usually blank. +takeOverLogin (false): Take over the main login action. If takeOverLogin is + set, anytime the standard username/password login form would be shown, + a CAS login will be done instead. * required default values are in (parenthesis) @@ -33,6 +36,7 @@ addPlugin('casAuthentication', array( 'autoregistration'=>true, 'server'=>'sso-cas.univ-rennes1.fr', 'port'=>443, - 'path'=>'' + 'path'=>'', + 'takeOverLogin'=>true )); diff --git a/plugins/Facebook/FacebookPlugin.php b/plugins/Facebook/FacebookPlugin.php index 39b2ef287..de91bf24a 100644 --- a/plugins/Facebook/FacebookPlugin.php +++ b/plugins/Facebook/FacebookPlugin.php @@ -32,6 +32,7 @@ if (!defined('STATUSNET')) { } define("FACEBOOK_CONNECT_SERVICE", 3); +define('FACEBOOKPLUGIN_VERSION', '0.9'); require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php'; @@ -554,4 +555,18 @@ class FacebookPlugin extends Plugin return true; } + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Facebook', + 'version' => FACEBOOKPLUGIN_VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:Facebook', + 'rawdescription' => + _m('The Facebook plugin allows you to integrate ' . + 'your StatusNet instance with ' . + '<a href="http://facebook.com/">Facebook</a> ' . + 'and Facebook Connect.')); + return true; + } + } diff --git a/plugins/Facebook/facebook/facebook.php b/plugins/Facebook/facebook/facebook.php index 016e8e8e0..440706cbc 100644 --- a/plugins/Facebook/facebook/facebook.php +++ b/plugins/Facebook/facebook/facebook.php @@ -82,7 +82,8 @@ class Facebook { if (isset($this->fb_params['friends'])) { - $this->api_client->friends_list = explode(',', $this->fb_params['friends']); + $this->api_client->friends_list = + array_filter(explode(',', $this->fb_params['friends'])); } if (isset($this->fb_params['added'])) { $this->api_client->added = $this->fb_params['added']; @@ -215,11 +216,15 @@ class Facebook { // 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()) { + try { + if ($this->api_client->auth_expireSession()) { + $this->clear_cookie_state(); + return true; + } else { + return false; + } + } catch (Exception $e) { $this->clear_cookie_state(); - return true; - } else { - return false; } } @@ -249,10 +254,14 @@ class Facebook { 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); + setcookie($this->api_key . '_' . $name, + false, + time() - 3600, + '', + $this->base_domain); unset($_COOKIE[$this->api_key . '_' . $name]); } - setcookie($this->api_key, false, time() - 3600); + setcookie($this->api_key, false, time() - 3600, '', $this->base_domain); unset($_COOKIE[$this->api_key]); } diff --git a/plugins/Facebook/facebook/facebook_desktop.php b/plugins/Facebook/facebook/facebook_desktop.php index e79a2ca34..ed4762215 100644 --- a/plugins/Facebook/facebook/facebook_desktop.php +++ b/plugins/Facebook/facebook/facebook_desktop.php @@ -60,7 +60,7 @@ class FacebookDesktop extends Facebook { public function set_session_secret($session_secret) { $this->secret = $session_secret; - $this->api_client->secret = $session_secret; + $this->api_client->use_session_secret($session_secret); } public function require_login() { diff --git a/plugins/Facebook/facebook/facebook_mobile.php b/plugins/Facebook/facebook/facebook_mobile.php new file mode 100644 index 000000000..5ee7f4ed5 --- /dev/null +++ b/plugins/Facebook/facebook/facebook_mobile.php @@ -0,0 +1,260 @@ +<?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 wap + * apps. Since there is no javascript support, we need to use server redirect + * to implement Facebook connect functionalities such as authenticate, + * authorize, feed form etc.. This library provide many helper functions for + * wap developer to locate the right wap url. The url here is targed at + * facebook wap site or wap-friendly url. + */ +class FacebookMobile extends Facebook { + // the application secret, which differs from the session secret + + public function __construct($api_key, $secret, $generate_session_secret=false) { + parent::__construct($api_key, $secret, $generate_session_secret); + } + + public function redirect($url) { + header('Location: '. $url); + } + + public function get_m_url($action, $params) { + $page = parent::get_facebook_url('m'). '/' .$action; + foreach($params as $key => $val) { + if (!$val) { + unset($params[$key]); + } + } + return $page . '?' . http_build_query($params); + } + + public function get_www_url($action, $params) { + $page = parent::get_facebook_url('www'). '/' .$action; + foreach($params as $key => $val) { + if (!$val) { + unset($params[$key]); + } + } + return $page . '?' . http_build_query($params); + } + + public function get_add_url($next=null) { + + return $this->get_m_url('add.php', array('api_key' => $this->api_key, + 'next' => $next)); + } + + public function get_tos_url($next=null, $cancel = null, $canvas=null) { + return $this->get_m_url('tos.php', array('api_key' => $this->api_key, + 'v' => '1.0', + 'next' => $next, + 'canvas' => $canvas, + 'cancel' => $cancel)); + } + + public function get_logout_url($next=null) { + $params = array('api_key' => $this->api_key, + 'session_key' => $this->api_client->session_key, + ); + + if ($next) { + $params['connect_next'] = 1; + $params['next'] = $next; + } + + return $this->get_m_url('logout.php', $params); + } + public function get_register_url($next=null, $cancel_url=null) { + return $this->get_m_url('r.php', + array('fbconnect' => 1, + 'api_key' => $this->api_key, + 'next' => $next ? $next : parent::current_url(), + 'cancel_url' => $cancel_url ? $cancel_url : parent::current_url())); + } + /** + * These set of fbconnect style url redirect back to the application current + * page when the action is done. Developer can also use the non fbconnect + * style url and provide their own redirect link by giving the right parameter + * to $next and/or $cancel_url + */ + public function get_fbconnect_register_url() { + return $this->get_register_url(parent::current_url(), parent::current_url()); + } + public function get_fbconnect_tos_url() { + return $this->get_tos_url(parent::current_url(), parent::current_url(), $this->in_frame()); + } + + public function get_fbconnect_logout_url() { + return $this->get_logout_url(parent::current_url()); + } + + public function logout_user() { + $this->user = null; + } + + public function get_prompt_permissions_url($ext_perm, + $next=null, + $cancel_url=null) { + + return $this->get_www_url('connect/prompt_permissions.php', + array('api_key' => $this->api_key, + 'ext_perm' => $ext_perm, + 'next' => $next ? $next : parent::current_url(), + 'cancel' => $cancel_url ? $cancel_url : parent::current_url(), + 'display' => 'wap')); + + } + + /** + * support both prompt_permissions.php and authorize.php for now. + * authorized.php is to be deprecate though. + */ + public function get_extended_permission_url($ext_perm, + $next=null, + $cancel_url=null) { + $next = $next ? $next : parent::current_url(); + $cancel_url = $cancel_url ? $cancel_url : parent::current_url(); + + return $this->get_m_url('authorize.php', + array('api_key' => $this->api_key, + 'ext_perm' => $ext_perm, + 'next' => $next, + 'cancel_url' => $cancel_url)); + + } + + public function render_prompt_feed_url($action_links=NULL, + $target_id=NULL, + $message='', + $user_message_prompt='', + $caption=NULL, + $callback ='', + $cancel='', + $attachment=NULL, + $preview=true) { + + $params = array('api_key' => $this->api_key, + 'session_key' => $this->api_client->session_key, + ); + if (!empty($attachment)) { + $params['attachment'] = urlencode(json_encode($attachment)); + } else { + $attachment = new stdClass(); + $app_display_info = $this->api_client->admin_getAppProperties(array('application_name', + 'callback_url', + 'description', + 'logo_url')); + $app_display_info = $app_display_info; + $attachment->name = $app_display_info['application_name']; + $attachment->caption = !empty($caption) ? $caption : 'Just see what\'s new!'; + $attachment->description = $app_display_info['description']; + $attachment->href = $app_display_info['callback_url']; + if (!empty($app_display_info['logo_url'])) { + $logo = new stdClass(); + $logo->type = 'image'; + $logo->src = $app_display_info['logo_url']; + $logo->href = $app_display_info['callback_url']; + $attachment->media = array($logo); + } + $params['attachment'] = urlencode(json_encode($attachment)); + } + $params['preview'] = $preview; + $params['message'] = $message; + $params['user_message_prompt'] = $user_message_prompt; + if (!empty($callback)) { + $params['callback'] = $callback; + } else { + $params['callback'] = $this->current_url(); + } + if (!empty($cancel)) { + $params['cancel'] = $cancel; + } else { + $params['cancel'] = $this->current_url(); + } + + if (!empty($target_id)) { + $params['target_id'] = $target_id; + } + if (!empty($action_links)) { + $params['action_links'] = urlencode(json_encode($action_links)); + } + + $params['display'] = 'wap'; + header('Location: '. $this->get_www_url('connect/prompt_feed.php', $params)); + } + +//use template_id + public function render_feed_form_url($template_id=NULL, + $template_data=NULL, + $user_message=NULL, + $body_general=NULL, + $user_message_prompt=NULL, + $target_id=NULL, + $callback=NULL, + $cancel=NULL, + $preview=true) { + + $params = array('api_key' => $this->api_key); + $params['preview'] = $preview; + if (isset($template_id) && $template_id) { + $params['template_id'] = $template_id; + } + $params['message'] = $user_message ? $user_message['value'] : ''; + if (isset($body_general) && $body_general) { + $params['body_general'] = $body_general; + } + if (isset($user_message_prompt) && $user_message_prompt) { + $params['user_message_prompt'] = $user_message_prompt; + } + if (isset($callback) && $callback) { + $params['callback'] = $callback; + } else { + $params['callback'] = $this->current_url(); + } + if (isset($cancel) && $cancel) { + $params['cancel'] = $cancel; + } else { + $params['cancel'] = $this->current_url(); + } + if (isset($template_data) && $template_data) { + $params['template_data'] = $template_data; + } + if (isset($target_id) && $target_id) { + $params['to_ids'] = $target_id; + } + $params['display'] = 'wap'; + header('Location: '. $this->get_www_url('connect/prompt_feed.php', $params)); + } +} diff --git a/plugins/Facebook/facebook/facebookapi_php5_restlib.php b/plugins/Facebook/facebook/facebookapi_php5_restlib.php index 55cb7fb86..fa1088cd0 100755 --- a/plugins/Facebook/facebook/facebookapi_php5_restlib.php +++ b/plugins/Facebook/facebook/facebookapi_php5_restlib.php @@ -56,6 +56,8 @@ class FacebookRestClient { private $call_as_apikey; private $use_curl_if_available; private $format = null; + private $using_session_secret = false; + private $rawData = null; const BATCH_MODE_DEFAULT = 0; const BATCH_MODE_SERVER_PARALLEL = 0; @@ -76,7 +78,10 @@ class FacebookRestClient { $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'; + $this->server_addr = + Facebook::get_facebook_url('api') . '/restserver.php'; + $this->photo_server_addr = + Facebook::get_facebook_url('api-photo') . '/restserver.php'; if (!empty($GLOBALS['facebook_config']['debug'])) { $this->cur_id = 0; @@ -128,6 +133,16 @@ function toggleDisplay(id, type) { $this->user = $uid; } + + /** + * Switch to use the session secret instead of the app secret, + * for desktop and unsecured environment + */ + public function use_session_secret($session_secret) { + $this->secret = $session_secret; + $this->using_session_secret = true; + } + /** * Normally, if the cURL library/PHP extension is available, it is used for * HTTP transactions. This allows that behavior to be overridden, falling @@ -270,25 +285,35 @@ function toggleDisplay(id, type) { /** * 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 + * @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 + * @param string $host_url the connect site URL for which the session is + * being generated. This parameter is optional, unless + * you want Facebook to determine which of several base domains + * to choose from. If this third argument isn't provided but + * there are several base domains, the first base domain is + * chosen. * * @return array An assoc array containing session_key, uid */ - public function auth_getSession($auth_token, $generate_session_secret=false) { + public function auth_getSession($auth_token, + $generate_session_secret = false, + $host_url = null) { if (!$this->pending_batch()) { - $result = $this->call_method('facebook.auth.getSession', - array('auth_token' => $auth_token, - 'generate_session_secret' => $generate_session_secret)); + $result = $this->call_method( + 'facebook.auth.getSession', + array('auth_token' => $auth_token, + 'generate_session_secret' => $generate_session_secret, + 'host_url' => $host_url)); $this->session_key = $result['session_key']; - if (!empty($result['secret']) && !$generate_session_secret) { - // desktop apps have a special secret - $this->secret = $result['secret']; - } + if (!empty($result['secret']) && !$generate_session_secret) { + // desktop apps have a special secret + $this->secret = $result['secret']; + } + return $result; } } @@ -519,7 +544,7 @@ function toggleDisplay(id, type) { return $this->call_upload_method('facebook.events.create', array('event_info' => $event_info), $file, - Facebook::get_facebook_url('api-photo') . '/restserver.php'); + $this->photo_server_addr); } else { return $this->call_method('facebook.events.create', array('event_info' => $event_info)); @@ -527,6 +552,27 @@ function toggleDisplay(id, type) { } /** + * Invites users to an event. If a session user exists, the session user + * must have permissions to invite friends to the event and $uids must contain + * a list of friend ids. Otherwise, the event must have been + * created by the app and $uids must contain users of the app. + * This method requires the 'create_event' extended permission to + * invite people on behalf of a user. + * + * @param $eid the event id + * @param $uids an array of users to invite + * @param $personal_message a string containing the user's message + * (text only) + * + */ + public function events_invite($eid, $uids, $personal_message) { + return $this->call_method('facebook.events.invite', + array('eid' => $eid, + 'uids' => $uids, + 'personal_message', $personal_message)); + } + + /** * Edits an existing event. Only works for events where application is admin. * * @param int $eid event id @@ -540,7 +586,7 @@ function toggleDisplay(id, type) { return $this->call_upload_method('facebook.events.edit', array('eid' => $eid, 'event_info' => $event_info), $file, - Facebook::get_facebook_url('api-photo') . '/restserver.php'); + $this->photo_server_addr); } else { return $this->call_method('facebook.events.edit', array('eid' => $eid, @@ -576,21 +622,7 @@ function toggleDisplay(id, type) { 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 @@ -668,7 +700,44 @@ function toggleDisplay(id, type) { array('tag_names' => json_encode($tag_names))); } + /** + * Gets the best translations for native strings submitted by an application + * for translation. If $locale is not specified, only native strings and their + * descriptions are returned. If $all is true, then unapproved translations + * are returned as well, otherwise only approved translations are returned. + * + * A mapping of locale codes -> language names is available at + * http://wiki.developers.facebook.com/index.php/Facebook_Locales + * + * @param string $locale the locale to get translations for, or 'all' for all + * locales, or 'en_US' for native strings + * @param bool $all whether to return all or only approved translations + * + * @return array (locale, array(native_strings, array('best translation + * available given enough votes or manual approval', approval + * status))) + * @error API_EC_PARAM + * @error API_EC_PARAM_BAD_LOCALE + */ + public function &intl_getTranslations($locale = 'en_US', $all = false) { + return $this->call_method('facebook.intl.getTranslations', + array('locale' => $locale, + 'all' => $all)); + } + /** + * 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 &intl_uploadNativeStrings($native_strings) { + return $this->call_method('facebook.intl.uploadNativeStrings', + array('native_strings' => json_encode($native_strings))); + } /** * This method is deprecated for calls made on behalf of users. This method @@ -1249,6 +1318,87 @@ function toggleDisplay(id, type) { } /** + * Gifts API + */ + + /** + * Get Gifts associated with an app + * + * @return array of gifts + */ + public function gifts_get() { + return json_decode( + $this->call_method('facebook.gifts.get', + array()), + true + ); + } + + /* + * Update gifts stored by an app + * + * @param array containing gift_id => gift_data to be updated + * @return array containing gift_id => true/false indicating success + * in updating that gift + */ + public function gifts_update($update_array) { + return json_decode( + $this->call_method('facebook.gifts.update', + array('update_str' => json_encode($update_array)) + ), + true + ); + } + + /** + * Dashboard API + */ + + /** + * Set the news for the specified user. + * + * @param int $uid The user for whom you are setting news for + * @param string $news Text of news to display + * + * @return bool Success + */ + public function dashboard_setNews($uid, $news) { + return $this->call_method('facebook.dashboard.setNews', + array('uid' => $uid, + 'news' => $news) + ); + } + + /** + * Get the current news of the specified user. + * + * @param int $uid The user to get the news of + * + * @return string The text of the current news for the user + */ + public function dashboard_getNews($uid) { + return json_decode( + $this->call_method('facebook.dashboard.getNews', + array('uid' => $uid) + ), true); + } + + /** + * Set the news for the specified user. + * + * @param int $uid The user you are clearing the news of + * + * @return bool Success + */ + public function dashboard_clearNews($uid) { + return $this->call_method('facebook.dashboard.clearNews', + array('uid' => $uid) + ); + } + + + + /** * Creates a note with the specified title and content. * * @param string $title Title of the note. @@ -1795,14 +1945,20 @@ function toggleDisplay(id, type) { $start_time = 0, $end_time = 0, $limit = 30, - $filter_key = '') { + $filter_key = '', + $exportable_only = false, + $metadata = null, + $post_ids = null) { $args = array( 'viewer_id' => $viewer_id, 'source_ids' => $source_ids, 'start_time' => $start_time, 'end_time' => $end_time, 'limit' => $limit, - 'filter_key' => $filter_key); + 'filter_key' => $filter_key, + 'exportable_only' => $exportable_only, + 'metadata' => $metadata, + 'post_ids' => $post_ids); return $this->call_method('facebook.stream.get', $args); } @@ -1949,97 +2105,6 @@ function toggleDisplay(id, type) { '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 @@ -2876,6 +2941,35 @@ function toggleDisplay(id, type) { } /** + * Sets href and text for a Live Stream Box xid's via link + * + * @param string $xid xid of the Live Stream + * @param string $via_href Href for the via link + * @param string $via_text Text for the via link + * + * @return boolWhether the set was successful + */ + public function admin_setLiveStreamViaLink($xid, $via_href, $via_text) { + return $this->call_method('facebook.admin.setLiveStreamViaLink', + array('xid' => $xid, + 'via_href' => $via_href, + 'via_text' => $via_text)); + } + + /** + * Gets href and text for a Live Stream Box xid's via link + * + * @param string $xid xid of the Live Stream + * + * @return Array Associative array with keys 'via_href' and 'via_text' + * False if there was an error. + */ + public function admin_getLiveStreamViaLink($xid) { + return $this->call_method('facebook.admin.getLiveStreamViaLink', + array('xid' => $xid)); + } + + /** * 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. @@ -3012,6 +3106,7 @@ function toggleDisplay(id, type) { $params['call_as_apikey'] = $this->call_as_apikey; } $data = $this->post_request($method, $params); + $this->rawData = $data; $result = $this->convert_result($data, $method, $params); if (is_array($result) && isset($result['error_code'])) { throw new FacebookRestClientException($result['error_msg'], @@ -3054,6 +3149,16 @@ function toggleDisplay(id, type) { } /** + * Returns the raw JSON or XML output returned by the server in the most + * recent API call. + * + * @return string + */ + public function getRawData() { + return $this->rawData; + } + + /** * Calls the specified file-upload POST method with the specified parameters * * @param string $method Name of the Facebook method to invoke @@ -3144,6 +3249,10 @@ function toggleDisplay(id, type) { if ($this->call_as_apikey) { $get['call_as_apikey'] = $this->call_as_apikey; } + if ($this->using_session_secret) { + $get['ss'] = '1'; + } + $get['method'] = $method; $get['session_key'] = $this->session_key; $get['api_key'] = $this->api_key; @@ -3241,7 +3350,7 @@ function toggleDisplay(id, type) { return $result; } - private function post_upload_request($method, $params, $file, $server_addr = null) { + protected 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); @@ -3345,6 +3454,8 @@ class FacebookAPIErrorCodes { const API_EC_VERSION = 12; const API_EC_INTERNAL_FQL_ERROR = 13; const API_EC_HOST_PUP = 14; + const API_EC_SESSION_SECRET_NOT_ALLOWED = 15; + const API_EC_HOST_READONLY = 16; /* * PARAMETER ERRORS @@ -3372,6 +3483,8 @@ class FacebookAPIErrorCodes { const API_EC_PARAM_BAD_EID = 150; const API_EC_PARAM_UNKNOWN_CITY = 151; const API_EC_PARAM_BAD_PAGE_TYPE = 152; + const API_EC_PARAM_BAD_LOCALE = 170; + const API_EC_PARAM_BLOCKED_NOTIFICATION = 180; /* * USER PERMISSIONS ERRORS @@ -3394,6 +3507,7 @@ class FacebookAPIErrorCodes { const API_EC_PERMISSION_EVENT = 290; const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291; const API_EC_PERMISSION_LIVEMESSAGE = 292; + const API_EC_PERMISSION_CREATE_EVENT = 296; const API_EC_PERMISSION_RSVP_EVENT = 299; /* @@ -3469,6 +3583,8 @@ class FacebookAPIErrorCodes { const FQL_EC_EXTENDED_PERMISSION = 612; const FQL_EC_RATE_LIMIT_EXCEEDED = 613; const FQL_EC_UNRESOLVED_DEPENDENCY = 614; + const FQL_EC_INVALID_SEARCH = 615; + const FQL_EC_CONTAINS_ERROR = 616; const API_EC_REF_SET_FAILED = 700; @@ -3506,6 +3622,7 @@ class FacebookAPIErrorCodes { * EVENT API ERRORS */ const API_EC_EVENT_INVALID_TIME = 1000; + const API_EC_EVENT_NAME_LOCKED = 1001; /* * INFO BOX ERRORS @@ -3566,6 +3683,21 @@ class FacebookAPIErrorCodes { const API_EC_COMMENTS_INVALID_POST = 1705; const API_EC_COMMENTS_INVALID_REMOVE = 1706; + /* + * GIFTS + */ + const API_EC_GIFTS_UNKNOWN = 1900; + + /* + * APPLICATION MORATORIUM ERRORS + */ + const API_EC_DISABLED_ALL = 2000; + const API_EC_DISABLED_STATUS = 2001; + const API_EC_DISABLED_FEED_STORIES = 2002; + const API_EC_DISABLED_NOTIFICATIONS = 2003; + const API_EC_DISABLED_REQUESTS = 2004; + const API_EC_DISABLED_EMAIL = 2005; + /** * 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 diff --git a/plugins/Facebook/facebookaction.php b/plugins/Facebook/facebookaction.php index 24bf215fd..bf9c037a5 100644 --- a/plugins/Facebook/facebookaction.php +++ b/plugins/Facebook/facebookaction.php @@ -32,7 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { } require_once INSTALLDIR . '/plugins/Facebook/facebookutil.php'; -require_once INSTALLDIR . '/lib/noticeform.php'; +require_once INSTALLDIR . '/plugins/Facebook/facebooknoticeform.php'; class FacebookAction extends Action { @@ -294,63 +294,7 @@ class FacebookAction extends Action $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>'; + $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(); @@ -455,42 +399,6 @@ class FacebookAction extends Action 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; } } diff --git a/plugins/Facebook/facebooknoticeform.php b/plugins/Facebook/facebooknoticeform.php new file mode 100644 index 000000000..5989147f4 --- /dev/null +++ b/plugins/Facebook/facebooknoticeform.php @@ -0,0 +1,206 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for posting a notice from within the Facebook App. + * + * This is a stripped down version of the normal NoticeForm (sans + * location stuff and media upload stuff). I'm not sure we can share the + * location (from FB) and they don't allow posting multipart form data + * to Facebook canvas pages, so that won't work anyway. --Zach + * + * 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> + * @author Sarven Capadisli <csarven@status.net> + * @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); +} + +require_once INSTALLDIR . '/lib/form.php'; + +/** + * Form for posting a notice from within the Facebook app + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copey <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/ + * + * @see HTMLOutputter + */ + +class FacebookNoticeForm extends Form +{ + /** + * Current action, used for returning to this page. + */ + + var $action = null; + + /** + * Pre-filled content of the form + */ + + var $content = null; + + /** + * The current user + */ + + var $user = null; + + /** + * The notice being replied to + */ + + var $inreplyto = 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, $inreplyto=null) + { + parent::__construct($out); + + $this->action = $action; + $this->post_action = $post_action; + $this->content = $content; + $this->inreplyto = $inreplyto; + + if ($user) { + $this->user = $user; + } else { + $this->user = common_current_user(); + } + + // Note: Facebook doesn't allow multipart/form-data posting to + // canvas pages, so don't try to set it--no file uploads, at + // least not this way. It can be done using multiple servers + // and iFrames, but it's a pretty hacky process. + } + + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'form_notice'; + } + + /** + * Class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_notice'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return $this->post_action; + } + + /** + * Legend of the Form + * + * @return void + */ + function formLegend() + { + $this->out->element('legend', null, _('Send a notice')); + } + + /** + * Data elements + * + * @return void + */ + + function formData() + { + if (Event::handle('StartShowNoticeFormData', array($this))) { + $this->out->element('label', array('for' => 'notice_data-text'), + sprintf(_('What\'s up, %s?'), $this->user->nickname)); + // XXX: vary by defined max size + $this->out->element('textarea', array('id' => 'notice_data-text', + 'cols' => 35, + 'rows' => 4, + 'name' => 'status_textarea'), + ($this->content) ? $this->content : ''); + + $contentLimit = Notice::maxContent(); + + if ($contentLimit > 0) { + $this->out->elementStart('dl', 'form_note'); + $this->out->element('dt', null, _('Available characters')); + $this->out->element('dd', array('id' => 'notice_text-count'), + $contentLimit); + $this->out->elementEnd('dl'); + } + + if ($this->action) { + $this->out->hidden('notice_return-to', $this->action, 'returnto'); + } + $this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto'); + + Event::handle('StartShowNoticeFormData', array($this)); + } + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->element('input', array('id' => 'notice_action-submit', + 'class' => 'submit', + 'name' => 'status_submit', + 'type' => 'submit', + 'value' => _('Send'))); + } +} diff --git a/plugins/Facebook/facebookutil.php b/plugins/Facebook/facebookutil.php index 2ec6db6b8..ac532e18b 100644 --- a/plugins/Facebook/facebookutil.php +++ b/plugins/Facebook/facebookutil.php @@ -138,21 +138,23 @@ function facebookBroadcastNotice($notice) $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)!"); + $msg = "Facebook returned error code $code: " . + $e->getMessage() . ' - ' . + "Unable to update Facebook status (notice $notice->id) " . + "for $user->nickname (user id: $user->id)!"; - if ($code == 200 || $code == 250) { + common_log(LOG_WARNING, $msg); + if ($code == 100 || $code == 200 || $code == 250) { + + // 100 The account is 'inactive' (probably - this is not well documented) // 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 { + } else { // Try sending again later. diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php index 857a9794d..e49e2a648 100644 --- a/plugins/FeedSub/FeedSubPlugin.php +++ b/plugins/FeedSub/FeedSubPlugin.php @@ -105,12 +105,11 @@ class FeedSubPlugin extends Plugin return true; } - /* - // auto increment seems to be broken function onCheckSchema() { + // warning: the autoincrement doesn't seem to set. + // alter table feedinfo change column id id int(11) not null auto_increment; $schema = Schema::get(); - $schema->ensureDataObject('Feedinfo'); + $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); return true; } - */ } diff --git a/plugins/FeedSub/feedinfo.php b/plugins/FeedSub/feedinfo.php index fff66afe9..b166bd6e1 100644 --- a/plugins/FeedSub/feedinfo.php +++ b/plugins/FeedSub/feedinfo.php @@ -31,7 +31,7 @@ class FeedDBException extends FeedSubException } } -class Feedinfo extends Plugin_DataObject +class Feedinfo extends Memcached_DataObject { public $__table = 'feedinfo'; @@ -56,34 +56,90 @@ class Feedinfo extends Plugin_DataObject return parent::staticGet(__CLASS__, $k, $v); } - function tableDef() + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('id', 'integer', + /*size*/ null, + /*nullable*/ false, + /*key*/ 'PRI', + /*default*/ '0', + /*extra*/ null, + /*auto_increment*/ true), + new ColumnDef('profile_id', 'integer', + null, false), + new ColumnDef('feeduri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('homeuri', 'varchar', + 255, false), + new ColumnDef('huburi', 'varchar', + 255, false), + new ColumnDef('verify_token', 'varchar', + 32, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('lastupdate', 'datetime', + null, false)); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() { - class_exists('Schema'); // autoload hack - // warning: the autoincrement doesn't seem to set. - // alter table feedinfo change column id id int(11) not null auto_increment; - return new TableDef($this->__table, - array(new ColumnDef('id', 'integer', - null, false, 'PRI', '0', null, true), - new ColumnDef('profile_id', 'integer', - null, false), - new ColumnDef('feeduri', 'varchar', - 255, false, 'UNI'), - new ColumnDef('homeuri', 'varchar', - 255, false), - new ColumnDef('huburi', 'varchar', - 255, false), - new ColumnDef('verify_token', 'varchar', - 32, true), - new ColumnDef('sub_start', 'datetime', - null, true), - new ColumnDef('sub_end', 'datetime', - null, true), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('lastupdate', 'datetime', - null, false))); + return array('id' => 'P'); //? } + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ public function getProfile() { return Profile::staticGet('id', $this->profile_id); diff --git a/plugins/GeoURLPlugin.php b/plugins/GeoURLPlugin.php index 30ff2c278..01178f39c 100644 --- a/plugins/GeoURLPlugin.php +++ b/plugins/GeoURLPlugin.php @@ -116,4 +116,16 @@ class GeoURLPlugin extends Plugin return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'GeoURL', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:GeoURL', + 'rawdescription' => + _m('Ping <a href="http://geourl.org/">GeoURL</a> when '. + 'new geolocation-enhanced notices are posted.')); + return true; + } } diff --git a/plugins/GeonamesPlugin.php b/plugins/GeonamesPlugin.php index 805166eaa..52cc9c97f 100644 --- a/plugins/GeonamesPlugin.php +++ b/plugins/GeonamesPlugin.php @@ -426,4 +426,16 @@ class GeonamesPlugin extends Plugin return $document->geoname; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Geonames', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Geonames', + 'rawdescription' => + _m('Uses <a href="http://geonames.org/">Geonames</a> service to get human-readable '. + 'names for locations based on user-provided lat/long pairs.')); + return true; + } } diff --git a/plugins/GoogleAnalyticsPlugin.php b/plugins/GoogleAnalyticsPlugin.php index 6891ee6a7..c646bf113 100644 --- a/plugins/GoogleAnalyticsPlugin.php +++ b/plugins/GoogleAnalyticsPlugin.php @@ -70,4 +70,16 @@ class GoogleAnalyticsPlugin extends Plugin $action->inlineScript($js1); $action->inlineScript($js2); } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'GoogleAnalytics', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:GoogleAnalytics', + 'rawdescription' => + _m('Use <a href="http://www.google.com/analytics/">Google Analytics</a>'. + ' to track Web access.')); + return true; + } } diff --git a/plugins/Imap/ImapPlugin.php b/plugins/Imap/ImapPlugin.php new file mode 100644 index 000000000..034444222 --- /dev/null +++ b/plugins/Imap/ImapPlugin.php @@ -0,0 +1,85 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to add a StatusNet Facebook application + * + * 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 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); +} + +/** + * IMAP plugin to allow StatusNet to grab incoming emails and handle them as new user posts + * + * @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/ + */ +class ImapPlugin extends Plugin +{ + public $mailbox; + public $user; + public $password; + public $poll_frequency = 60; + public static $instances = array(); + public static $daemon_added = array(); + + function initialize(){ + if(!isset($this->mailbox)){ + throw new Exception("must specify a mailbox"); + } + if(!isset($this->user)){ + throw new Exception("must specify a user"); + } + if(!isset($this->password)){ + throw new Exception("must specify a password"); + } + if(!isset($this->poll_frequency)){ + throw new Exception("must specify a poll_frequency"); + } + + self::$instances[] = $this; + return true; + } + + function cleanup(){ + $index = array_search($this, self::$instances); + unset(self::$instances[$index]); + return true; + } + + function onGetValidDaemons($daemons) + { + if(! self::$daemon_added){ + array_push($daemons, INSTALLDIR . + '/plugins/Imap/imapdaemon.php'); + self::$daemon_added = true; + } + return true; + } +} diff --git a/plugins/Imap/README b/plugins/Imap/README new file mode 100644 index 000000000..640a411a8 --- /dev/null +++ b/plugins/Imap/README @@ -0,0 +1,32 @@ +The IMAP plugin allows for StatusNet to check a POP or IMAP mailbox for +incoming mail containing user posts. + +Installation +============ +addPlugin('imap', array( + 'mailbox' => '...', + 'user' => '...', + 'password' => '...' +)); +to the bottom of your config.php + +Also, make sure: +$config['mail']['domain'] = 'yourdomain.example.net'; +is set in your config.php + +Create a catch-all account for your domain, and use this account with this +plugin. Whenever a user sends a message to their personal notice posting +address, the message should end up in this mailbox, and then the plugin daemon +will pick it up and post the notice on the user's behalf. + +The daemon included with this plugin must be running. It 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. + +Settings +======== +mailbox*: the mailbox specifier. + See http://www.php.net/manual/en/function.imap-open.php for details +user*: username to use when authenticating to the mailbox +password*: password to use when authenticating to the mailbox +poll_frequency: how often (in seconds) to check for new messages diff --git a/plugins/Imap/imapdaemon.php b/plugins/Imap/imapdaemon.php new file mode 100755 index 000000000..a45c603ce --- /dev/null +++ b/plugins/Imap/imapdaemon.php @@ -0,0 +1,147 @@ +#!/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 = 'fi::'; +$longoptions = array('id::', 'foreground'); + +$helptext = <<<END_OF_IMAP_HELP +Daemon script for receiving new notices from users via a mail box (IMAP, POP3, etc) + + -i --id Identity (default none) + -f --foreground Stay in the foreground (default background) + +END_OF_IMAP_HELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +require_once INSTALLDIR . '/lib/common.php'; +require_once INSTALLDIR . '/lib/daemon.php'; +require_once INSTALLDIR.'/lib/mailhandler.php'; + +class IMAPDaemon extends Daemon +{ + function __construct($resource=null, $daemonize=true, $attrs) + { + parent::__construct($daemonize); + + foreach ($attrs as $attr=>$value) + { + $this->$attr = $value; + } + + $this->log(LOG_INFO, "INITIALIZE IMAPDaemon {" . $this->name() . "}"); + } + + function name() + { + return strtolower('imapdaemon.'.$this->user.'.'.crc32($this->mailbox)); + } + + function run() + { + $this->connect(); + while(true) + { + if(imap_ping($this->conn) || $this->connect()) + { + $this->check_mailbox(); + } + sleep($this->poll_frequency); + } + } + + function check_mailbox() + { + $count = imap_num_msg($this->conn); + $this->log(LOG_INFO, "Found $count messages"); + if($count > 0){ + $handler = new IMAPMailHandler(); + for($i=1; $i <= $count; $i++) + { + $rawmessage = imap_fetchheader($this->conn, $count, FT_PREFETCHTEXT) . imap_body($this->conn, $i); + $handler->handle_message($rawmessage); + imap_delete($this->conn, $i); + } + imap_expunge($this->conn); + $this->log(LOG_INFO, "Finished processing messages"); + } + } + + function log($level, $msg) + { + $text = $this->name() . ': '.$msg; + common_log($level, $text); + if (!$this->daemonize) + { + $line = common_log_line($level, $text); + echo $line; + echo "\n"; + } + } + + function connect() + { + $this->conn = imap_open($this->mailbox, $this->user, $this->password); + if($this->conn){ + $this->log(LOG_INFO, "Connected"); + return true; + }else{ + $this->log(LOG_INFO, "Failed to connect: " . imap_last_error()); + return false; + } + } +} + +class IMAPMailHandler extends MailHandler +{ + function error($from, $msg) + { + $this->log(LOG_INFO, "Error: $from $msg"); + $headers['To'] = $from; + $headers['Subject'] = "Error"; + + return mail_send(array($from), $headers, $msg); + } +} + +if (have_option('i', 'id')) { + $id = get_option_value('i', 'id'); +} else if (count($args) > 0) { + $id = $args[0]; +} else { + $id = null; +} + +$foreground = have_option('f', 'foreground'); + +foreach(ImapPlugin::$instances as $pluginInstance){ + + $daemon = new IMAPDaemon($id, !$foreground, array( + 'mailbox' => $pluginInstance->mailbox, + 'user' => $pluginInstance->user, + 'password' => $pluginInstance->password, + 'poll_frequency' => $pluginInstance->poll_frequency + )); + + $daemon->runOnce(); + +} diff --git a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php index 39967fe42..c14fa21a9 100644 --- a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php +++ b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php @@ -31,7 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/plugins/Authentication/AuthenticationPlugin.php'; require_once 'Net/LDAP2.php'; class LdapAuthenticationPlugin extends AuthenticationPlugin @@ -75,8 +74,6 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin case 'MemcacheSchemaCache': require_once(INSTALLDIR.'/plugins/LdapAuthentication/MemcacheSchemaCache.php'); return false; - default: - return parent::onAutoload($cls); } } diff --git a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php index 5e759c379..e5e22c0dd 100644 --- a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php +++ b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php @@ -31,7 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/plugins/Authorization/AuthorizationPlugin.php'; require_once 'Net/LDAP2.php'; class LdapAuthorizationPlugin extends AuthorizationPlugin @@ -53,7 +52,6 @@ class LdapAuthorizationPlugin extends AuthorizationPlugin public $attributes = array(); function onInitializePlugin(){ - parent::onInitializePlugin(); if(!isset($this->host)){ throw new Exception("must specify a host"); } diff --git a/plugins/LilUrl/LilUrlPlugin.php b/plugins/LilUrl/LilUrlPlugin.php index 4a6f1cdc7..c3e37c0c0 100644 --- a/plugins/LilUrl/LilUrlPlugin.php +++ b/plugins/LilUrl/LilUrlPlugin.php @@ -46,9 +46,9 @@ class LilUrlPlugin extends UrlShortenerPlugin 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; @@ -57,5 +57,18 @@ class LilUrlPlugin extends UrlShortenerPlugin return strval($x['href']); } } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => sprintf('LilUrl (%s)', $this->shortenerName), + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:LilUrl', + 'rawdescription' => + sprintf(_m('Uses <a href="http://%1$s/">%1$s</a> URL-shortener service.'), + $this->shortenerName)); + + return true; + } } diff --git a/plugins/LinkbackPlugin.php b/plugins/LinkbackPlugin.php index f220fff8f..15e57ab0e 100644 --- a/plugins/LinkbackPlugin.php +++ b/plugins/LinkbackPlugin.php @@ -231,4 +231,18 @@ class LinkbackPlugin extends Plugin return 'LinkbackPlugin/'.LINKBACKPLUGIN_VERSION . ' StatusNet/' . STATUSNET_VERSION; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Linkback', + 'version' => LINKBACKPLUGIN_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Linkback', + 'rawdescription' => + _m('Notify blog authors when their posts have been linked in '. + 'microblog notices using '. + '<a href="http://www.hixie.ch/specs/pingback/pingback">Pingback</a> '. + 'or <a href="http://www.movabletype.org/docs/mttrackback.html">Trackback</a> protocols.')); + return true; + } } diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php index 93679e56c..868933fd4 100644 --- a/plugins/Mapstraction/MapstractionPlugin.php +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -47,6 +47,8 @@ if (!defined('STATUSNET')) { class MapstractionPlugin extends Plugin { + const VERSION = STATUSNET_VERSION; + /** provider name, one of: 'cloudmade', 'google', 'microsoft', 'openlayers', 'yahoo' */ public $provider = 'openlayers'; @@ -192,4 +194,17 @@ class MapstractionPlugin extends Plugin $action->elementEnd('div'); } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Mapstraction', + 'version' => self::VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Mapstraction', + 'rawdescription' => + _m('Show maps of users\' and friends\' notices '. + 'with <a href="http://www.mapstraction.com/">Mapstraction</a> '. + 'JavaScript library.')); + return true; + } } diff --git a/plugins/MemcachePlugin.php b/plugins/MemcachePlugin.php new file mode 100644 index 000000000..5f93e9a83 --- /dev/null +++ b/plugins/MemcachePlugin.php @@ -0,0 +1,191 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Plugin to implement cache interface for memcache + * + * PHP version 5 + * + * 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 Cache + * @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')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A plugin to use memcache for the cache interface + * + * This used to be encoded as config-variable options in the core code; + * it's now broken out to a separate plugin. The same interface can be + * implemented by other plugins. + * + * @category Cache + * @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/ + */ + +class MemcachePlugin extends Plugin +{ + private $_conn = null; + public $servers = array('127.0.0.1;11211'); + + public $compressThreshold = 20480; + public $compressMinSaving = 0.2; + + public $persistent = null; + + /** + * Initialize the plugin + * + * Note that onStartCacheGet() may have been called before this! + * + * @return boolean flag value + */ + + function onInitializePlugin() + { + if (is_null($this->persistent)) { + $this->persistent = (php_sapi_name() == 'cli') ? false : true; + } + $this->_ensureConn(); + return true; + } + + /** + * Get a value associated with a key + * + * The value should have been set previously. + * + * @param string &$key in; Lookup key + * @param mixed &$value out; value associated with key + * + * @return boolean hook success + */ + + function onStartCacheGet(&$key, &$value) + { + $this->_ensureConn(); + $value = $this->_conn->get($key); + Event::handle('EndCacheGet', array($key, &$value)); + return false; + } + + /** + * Associate a value with a key + * + * @param string &$key in; Key to use for lookups + * @param mixed &$value in; Value to associate + * @param integer &$flag in; Flag (passed through to Memcache) + * @param integer &$expiry in; Expiry (passed through to Memcache) + * @param boolean &$success out; Whether the set was successful + * + * @return boolean hook success + */ + + function onStartCacheSet(&$key, &$value, &$flag, &$expiry, &$success) + { + $this->_ensureConn(); + $success = $this->_conn->set($key, $value, $flag, $expiry); + Event::handle('EndCacheSet', array($key, $value, $flag, + $expiry)); + return false; + } + + /** + * Delete a value associated with a key + * + * @param string &$key in; Key to lookup + * @param boolean &$success out; whether it worked + * + * @return boolean hook success + */ + + function onStartCacheDelete(&$key, &$success) + { + $this->_ensureConn(); + $success = $this->_conn->delete($key); + Event::handle('EndCacheDelete', array($key)); + return false; + } + + /** + * Ensure that a connection exists + * + * Checks the instance $_conn variable and connects + * if it is empty. + * + * @return void + */ + + private function _ensureConn() + { + if (empty($this->_conn)) { + $this->_conn = new Memcache(); + + if (is_array($this->servers)) { + foreach ($this->servers as $server) { + list($host, $port) = explode(';', $server); + if (empty($port)) { + $port = 11211; + } + + $this->_conn->addServer($host, $port, $this->persistent); + } + } else { + $this->_conn->addServer($this->servers, $this->persistent); + list($host, $port) = explode(';', $this->servers); + if (empty($port)) { + $port = 11211; + } + $this->_conn->addServer($host, $port, $this->persistent); + } + + // Compress items stored in the cache if they're over threshold in size + // (default 2KiB) and the compression would save more than min savings + // ratio (default 0.2). + + // Allows the cache to store objects larger than 1MB (if they + // compress to less than 1MB), and improves cache memory efficiency. + + $this->_conn->setCompressThreshold($this->compressThreshold, + $this->compressMinSaving); + } + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Memcache', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou, Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:Memcache', + 'rawdescription' => + _m('Use <a href="http://memcached.org/">Memcached</a> to cache query results.')); + return true; + } +} + diff --git a/plugins/Minify/MinifyPlugin.php b/plugins/Minify/MinifyPlugin.php index 71fade19a..718bfd163 100644 --- a/plugins/Minify/MinifyPlugin.php +++ b/plugins/Minify/MinifyPlugin.php @@ -84,7 +84,7 @@ class MinifyPlugin extends Plugin function onStartScriptElement($action,&$src,&$type) { $url = parse_url($src); - if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) + if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { $src = $this->minifyUrl($src); } diff --git a/plugins/Mollom/MollomPlugin.php b/plugins/Mollom/MollomPlugin.php new file mode 100644 index 000000000..4c82c481a --- /dev/null +++ b/plugins/Mollom/MollomPlugin.php @@ -0,0 +1,893 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Plugin to check submitted notices with Mollom + * + * 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/>. + * + * Mollom is a bayesian spam checker, wrapped into a webservice + * This plugin is based on the Drupal Mollom module + * + * @category Plugin + * @package Laconica + * @author Brenda Wallace <brenda@cpan.org> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +define('MOLLOMPLUGIN_VERSION', '0.1'); +define('MOLLOM_API_VERSION', '1.0'); + +define('MOLLOM_ANALYSIS_UNKNOWN' , 0); +define('MOLLOM_ANALYSIS_HAM' , 1); +define('MOLLOM_ANALYSIS_SPAM' , 2); +define('MOLLOM_ANALYSIS_UNSURE' , 3); + +define('MOLLOM_MODE_DISABLED', 0); +define('MOLLOM_MODE_CAPTCHA' , 1); +define('MOLLOM_MODE_ANALYSIS', 2); + +define('MOLLOM_FALLBACK_BLOCK' , 0); +define('MOLLOM_FALLBACK_ACCEPT', 1); + +define('MOLLOM_ERROR' , 1000); +define('MOLLOM_REFRESH' , 1100); +define('MOLLOM_REDIRECT', 1200); + +/** + * Plugin to check submitted notices with Mollom + * + * Mollom is a bayesian spam filter provided by webservice. + * + * @category Plugin + * @package Laconica + * @author Brenda Wallace <shiny@cpan.org> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * + * @see Event + */ + + + +class MollomPlugin extends Plugin +{ + public $public_key; + public $private_key; + public $servers; + + function onStartNoticeSave($notice) + { + if ( $this->public_key ) { + //Check spam + $data = array( + 'post_body' => $notice->content, + 'author_name' => $profile->nickname, + 'author_url' => $profile->homepage, + 'author_id' => $profile->id, + 'author_ip' => $this->getClientIp(), + ); + $response = $this->mollom('mollom.checkContent', $data); + if ($response['spam'] == MOLLOM_ANALYSIS_SPAM) { + throw new ClientException(_("Spam Detected"), 400); + } + if ($response['spam'] == MOLLOM_ANALYSIS_UNSURE) { + //if unsure, let through + } + if($response['spam'] == MOLLOM_ANALYSIS_HAM) { + // all good! :-) + } + } + + return true; + } + + function getClientIP() { + if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + // Note: order matters here; use proxy-forwarded stuff first + foreach (array('HTTP_X_FORWARDED_FOR', 'CLIENT-IP', 'REMOTE_ADDR') as $k) { + if (isset($_SERVER[$k])) { + return $_SERVER[$k]; + } + } + } + return '127.0.0.1'; + } + /** + * Call a remote procedure at the Mollom server. This function will + * automatically add the information required to authenticate against + * Mollom. + */ + function mollom($method, $data = array()) { + if (!extension_loaded('xmlrpc')) { + if (!dl('xmlrpc.so')) { + common_log(LOG_ERR, "Can't pingback; xmlrpc extension not available."); + } + } + + // Construct the server URL: + $public_key = $this->public_key; + // Retrieve the list of Mollom servers from the database: + $servers = $this->servers; + + if ($servers == NULL) { + // Retrieve a list of valid Mollom servers from mollom.com: + $servers = $this->xmlrpc('http://xmlrpc.mollom.com/'. MOLLOM_API_VERSION, 'mollom.getServerList', $this->authentication()); + + // Store the list of servers in the database: + // TODO! variable_set('mollom_servers', $servers); + } + + if (is_array($servers)) { + // Send the request to the first server, if that fails, try the other servers in the list: + foreach ($servers as $server) { + $auth = $this->authentication(); + $data = array_merge($data, $auth); + $result = $this->xmlrpc($server .'/'. MOLLOM_API_VERSION, $method, $data); + + // Debug output: + if (isset($data['session_id'])) { + common_debug("called $method at server $server with session ID '". $data['session_id'] ."'"); + } + else { + common_debug("called $method at server $server with no session ID"); + } + + if ($errno = $this->xmlrpc_errno()) { + common_log(LOG_ERR, sprintf('Error @errno: %s - %s - %s - <pre>%s</pre>', $this->xmlrpc_errno(), $server, $this->xmlrpc_error_msg(), $method, print_r($data, TRUE))); + + if ($errno == MOLLOM_REFRESH) { + // Retrieve a list of valid Mollom servers from mollom.com: + $servers = $this->xmlrpc('http://xmlrpc.mollom.com/'. MOLLOM_API_VERSION, 'mollom.getServerList', $this->authentication()); + + // Store the updated list of servers in the database: + //tODO variable_set('mollom_servers', $servers); + } + else if ($errno == MOLLOM_ERROR) { + return $result; + } + else if ($errno == MOLLOM_REDIRECT) { + // Do nothing, we select the next client automatically. + } + + // Reset the XMLRPC error: + $this->xmlrpc_error(0); // FIXME: this is crazy. + } + else { + common_debug("Result = " . print_r($result, TRUE)); + return $result; + } + } + } + + // If none of the servers worked, activate the fallback mechanism: + common_debug("none of the servers worked"); + // _mollom_fallback(); + + // If everything failed, we reset the server list to force Mollom to request a new list: + //TODO variable_set('mollom_servers', array()); + } + + /** + * This function generate an array with all the information required to + * authenticate against Mollom. To prevent that requests are forged and + * that you are impersonated, each request is signed with a hash computed + * based on a private key and a timestamp. + * + * Both the client and the server share the secret key that is used to + * create the authentication hash based on a timestamp. They both hash + * the timestamp with the secret key, and if the hashes match, the + * authenticity of the message has been validated. + * + * To avoid that someone can intercept a (hash, timestamp)-pair and + * use that to impersonate a client, Mollom will reject the request + * when the timestamp is more than 15 minutes off. + * + * Make sure your server's time is synchronized with the world clocks, + * and that you don't share your private key with anyone else. + */ + private function authentication() { + + $public_key = $this->public_key; + $private_key = $this->private_key; + + // Generate a timestamp according to the dateTime format (http://www.w3.org/TR/xmlschema-2/#dateTime): + $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", time()); + + // Calculate a HMAC-SHA1 according to RFC2104 (http://www.ietf.org/rfc/rfc2104.txt): + $hash = base64_encode( + pack("H*", sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack("H*", sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . + $time)))) + ); + + // Store everything in an array. Elsewhere in the code, we'll add the + // acutal data before we pass it onto the XML-RPC library: + $data['public_key'] = $public_key; + $data['time'] = $time; + $data['hash'] = $hash; + + return $data; + } + + + function xmlrpc($url) { + //require_once './includes/xmlrpc.inc'; + $args = func_get_args(); + return call_user_func_array(array('MollomPlugin', '_xmlrpc'), $args); + } + + /** + * Recursively turn a data structure into objects with 'data' and 'type' attributes. + * + * @param $data + * The data structure. + * @param $type + * Optional type assign to $data. + * @return + * Object. + */ + function xmlrpc_value($data, $type = FALSE) { + $xmlrpc_value = new stdClass(); + $xmlrpc_value->data = $data; + if (!$type) { + $type = $this->xmlrpc_value_calculate_type($xmlrpc_value); + } + $xmlrpc_value->type = $type; + if ($type == 'struct') { + // Turn all the values in the array into new xmlrpc_values + foreach ($xmlrpc_value->data as $key => $value) { + $xmlrpc_value->data[$key] = $this->xmlrpc_value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($xmlrpc_value->data); $i < $j; $i++) { + $xmlrpc_value->data[$i] = $this->xmlrpc_value($xmlrpc_value->data[$i]); + } + } + return $xmlrpc_value; + } + + /** + * Map PHP type to XML-RPC type. + * + * @param $xmlrpc_value + * Variable whose type should be mapped. + * @return + * XML-RPC type as string. + * @see + * http://www.xmlrpc.com/spec#scalars + */ + function xmlrpc_value_calculate_type(&$xmlrpc_value) { + // http://www.php.net/gettype: Never use gettype() to test for a certain type [...] Instead, use the is_* functions. + if (is_bool($xmlrpc_value->data)) { + return 'boolean'; + } + if (is_double($xmlrpc_value->data)) { + return 'double'; + } + if (is_int($xmlrpc_value->data)) { + return 'int'; + } + if (is_array($xmlrpc_value->data)) { + // empty or integer-indexed arrays are 'array', string-indexed arrays 'struct' + return empty($xmlrpc_value->data) || range(0, count($xmlrpc_value->data) - 1) === array_keys($xmlrpc_value->data) ? 'array' : 'struct'; + } + if (is_object($xmlrpc_value->data)) { + if ($xmlrpc_value->data->is_date) { + return 'date'; + } + if ($xmlrpc_value->data->is_base64) { + return 'base64'; + } + $xmlrpc_value->data = get_object_vars($xmlrpc_value->data); + return 'struct'; + } + // default + return 'string'; + } + +/** + * Generate XML representing the given value. + * + * @param $xmlrpc_value + * @return + * XML representation of value. + */ +function xmlrpc_value_get_xml($xmlrpc_value) { + switch ($xmlrpc_value->type) { + case 'boolean': + return '<boolean>'. (($xmlrpc_value->data) ? '1' : '0') .'</boolean>'; + break; + case 'int': + return '<int>'. $xmlrpc_value->data .'</int>'; + break; + case 'double': + return '<double>'. $xmlrpc_value->data .'</double>'; + break; + case 'string': + // Note: we don't escape apostrophes because of the many blogging clients + // that don't support numerical entities (and XML in general) properly. + return '<string>'. htmlspecialchars($xmlrpc_value->data) .'</string>'; + break; + case 'array': + $return = '<array><data>'."\n"; + foreach ($xmlrpc_value->data as $item) { + $return .= ' <value>'. $this->xmlrpc_value_get_xml($item) ."</value>\n"; + } + $return .= '</data></array>'; + return $return; + break; + case 'struct': + $return = '<struct>'."\n"; + foreach ($xmlrpc_value->data as $name => $value) { + $return .= " <member><name>". htmlentities($name) ."</name><value>"; + $return .= $this->xmlrpc_value_get_xml($value) ."</value></member>\n"; + } + $return .= '</struct>'; + return $return; + break; + case 'date': + return $this->xmlrpc_date_get_xml($xmlrpc_value->data); + break; + case 'base64': + return $this->xmlrpc_base64_get_xml($xmlrpc_value->data); + break; + } + return FALSE; +} + + /** + * Perform an HTTP request. + * + * This is a flexible and powerful HTTP client implementation. Correctly handles + * GET, POST, PUT or any other HTTP requests. Handles redirects. + * + * @param $url + * A string containing a fully qualified URI. + * @param $headers + * An array containing an HTTP header => value pair. + * @param $method + * A string defining the HTTP request to use. + * @param $data + * A string containing data to include in the request. + * @param $retry + * An integer representing how many times to retry the request in case of a + * redirect. + * @return + * An object containing the HTTP request headers, response code, headers, + * data and redirect status. + */ + function http_request($url, $headers = array(), $method = 'GET', $data = NULL, $retry = 3) { + global $db_prefix; + + $result = new stdClass(); + + // Parse the URL and make sure we can handle the schema. + $uri = parse_url($url); + + if ($uri == FALSE) { + $result->error = 'unable to parse URL'; + return $result; + } + + if (!isset($uri['scheme'])) { + $result->error = 'missing schema'; + return $result; + } + + switch ($uri['scheme']) { + case 'http': + $port = isset($uri['port']) ? $uri['port'] : 80; + $host = $uri['host'] . ($port != 80 ? ':'. $port : ''); + $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15); + break; + case 'https': + // Note: Only works for PHP 4.3 compiled with OpenSSL. + $port = isset($uri['port']) ? $uri['port'] : 443; + $host = $uri['host'] . ($port != 443 ? ':'. $port : ''); + $fp = @fsockopen('ssl://'. $uri['host'], $port, $errno, $errstr, 20); + break; + default: + $result->error = 'invalid schema '. $uri['scheme']; + return $result; + } + + // Make sure the socket opened properly. + if (!$fp) { + // When a network error occurs, we use a negative number so it does not + // clash with the HTTP status codes. + $result->code = -$errno; + $result->error = trim($errstr); + + // Mark that this request failed. This will trigger a check of the web + // server's ability to make outgoing HTTP requests the next time that + // requirements checking is performed. + // @see system_requirements() + //TODO variable_set('drupal_http_request_fails', TRUE); + + return $result; + } + + // Construct the path to act on. + $path = isset($uri['path']) ? $uri['path'] : '/'; + if (isset($uri['query'])) { + $path .= '?'. $uri['query']; + } + + // Create HTTP request. + $defaults = array( + // RFC 2616: "non-standard ports MUST, default ports MAY be included". + // We don't add the port to prevent from breaking rewrite rules checking the + // host that do not take into account the port number. + 'Host' => "Host: $host", + 'User-Agent' => 'User-Agent: Drupal (+http://drupal.org/)', + 'Content-Length' => 'Content-Length: '. strlen($data) + ); + + // If the server url has a user then attempt to use basic authentication + if (isset($uri['user'])) { + $defaults['Authorization'] = 'Authorization: Basic '. base64_encode($uri['user'] . (!empty($uri['pass']) ? ":". $uri['pass'] : '')); + } + + // If the database prefix is being used by SimpleTest to run the tests in a copied + // database then set the user-agent header to the database prefix so that any + // calls to other Drupal pages will run the SimpleTest prefixed database. The + // user-agent is used to ensure that multiple testing sessions running at the + // same time won't interfere with each other as they would if the database + // prefix were stored statically in a file or database variable. + if (is_string($db_prefix) && preg_match("/^simpletest\d+$/", $db_prefix, $matches)) { + $defaults['User-Agent'] = 'User-Agent: ' . $matches[0]; + } + + foreach ($headers as $header => $value) { + $defaults[$header] = $header .': '. $value; + } + + $request = $method .' '. $path ." HTTP/1.0\r\n"; + $request .= implode("\r\n", $defaults); + $request .= "\r\n\r\n"; + $request .= $data; + + $result->request = $request; + + fwrite($fp, $request); + + // Fetch response. + $response = ''; + while (!feof($fp) && $chunk = fread($fp, 1024)) { + $response .= $chunk; + } + fclose($fp); + + // Parse response. + list($split, $result->data) = explode("\r\n\r\n", $response, 2); + $split = preg_split("/\r\n|\n|\r/", $split); + + list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3); + $result->headers = array(); + + // Parse headers. + while ($line = trim(array_shift($split))) { + list($header, $value) = explode(':', $line, 2); + if (isset($result->headers[$header]) && $header == 'Set-Cookie') { + // RFC 2109: the Set-Cookie response header comprises the token Set- + // Cookie:, followed by a comma-separated list of one or more cookies. + $result->headers[$header] .= ','. trim($value); + } + else { + $result->headers[$header] = trim($value); + } + } + + $responses = array( + 100 => 'Continue', 101 => 'Switching Protocols', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported' + ); + // RFC 2616 states that all unknown HTTP codes must be treated the same as the + // base code in their class. + if (!isset($responses[$code])) { + $code = floor($code / 100) * 100; + } + + switch ($code) { + case 200: // OK + case 304: // Not modified + break; + case 301: // Moved permanently + case 302: // Moved temporarily + case 307: // Moved temporarily + $location = $result->headers['Location']; + + if ($retry) { + $result = drupal_http_request($result->headers['Location'], $headers, $method, $data, --$retry); + $result->redirect_code = $result->code; + } + $result->redirect_url = $location; + + break; + default: + $result->error = $text; + } + + $result->code = $code; + return $result; + } + + /** + * Construct an object representing an XML-RPC message. + * + * @param $message + * String containing XML as defined at http://www.xmlrpc.com/spec + * @return + * Object + */ + function xmlrpc_message($message) { + $xmlrpc_message = new stdClass(); + $xmlrpc_message->array_structs = array(); // The stack used to keep track of the current array/struct + $xmlrpc_message->array_structs_types = array(); // The stack used to keep track of if things are structs or array + $xmlrpc_message->current_struct_name = array(); // A stack as well + $xmlrpc_message->message = $message; + return $xmlrpc_message; + } + + /** + * Parse an XML-RPC message. If parsing fails, the faultCode and faultString + * will be added to the message object. + * + * @param $xmlrpc_message + * Object generated by xmlrpc_message() + * @return + * TRUE if parsing succeeded; FALSE otherwise + */ + function xmlrpc_message_parse(&$xmlrpc_message) { + // First remove the XML declaration + $xmlrpc_message->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $xmlrpc_message->message); + if (trim($xmlrpc_message->message) == '') { + return FALSE; + } + $xmlrpc_message->_parser = xml_parser_create(); + // Set XML parser to take the case of tags into account. + xml_parser_set_option($xmlrpc_message->_parser, XML_OPTION_CASE_FOLDING, FALSE); + // Set XML parser callback functions + xml_set_element_handler($xmlrpc_message->_parser, array('MollomPlugin', 'xmlrpc_message_tag_open'), array('MollomPlugin', 'xmlrpc_message_tag_close')); + xml_set_character_data_handler($xmlrpc_message->_parser, array('MollomPlugin', 'xmlrpc_message_cdata')); + $this->xmlrpc_message_set($xmlrpc_message); + if (!xml_parse($xmlrpc_message->_parser, $xmlrpc_message->message)) { + return FALSE; + } + xml_parser_free($xmlrpc_message->_parser); + // Grab the error messages, if any + $xmlrpc_message = $this->xmlrpc_message_get(); + if ($xmlrpc_message->messagetype == 'fault') { + $xmlrpc_message->fault_code = $xmlrpc_message->params[0]['faultCode']; + $xmlrpc_message->fault_string = $xmlrpc_message->params[0]['faultString']; + } + return TRUE; + } + + /** + * Store a copy of the $xmlrpc_message object temporarily. + * + * @param $value + * Object + * @return + * The most recently stored $xmlrpc_message + */ + function xmlrpc_message_set($value = NULL) { + static $xmlrpc_message; + if ($value) { + $xmlrpc_message = $value; + } + return $xmlrpc_message; + } + + function xmlrpc_message_get() { + return $this->xmlrpc_message_set(); + } + + function xmlrpc_message_tag_open($parser, $tag, $attr) { + $xmlrpc_message = $this->xmlrpc_message_get(); + $xmlrpc_message->current_tag_contents = ''; + $xmlrpc_message->last_open = $tag; + switch ($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $xmlrpc_message->messagetype = $tag; + break; + // Deal with stacks of arrays and structs + case 'data': + $xmlrpc_message->array_structs_types[] = 'array'; + $xmlrpc_message->array_structs[] = array(); + break; + case 'struct': + $xmlrpc_message->array_structs_types[] = 'struct'; + $xmlrpc_message->array_structs[] = array(); + break; + } + $this->xmlrpc_message_set($xmlrpc_message); + } + + function xmlrpc_message_cdata($parser, $cdata) { + $xmlrpc_message = $this->xmlrpc_message_get(); + $xmlrpc_message->current_tag_contents .= $cdata; + $this->xmlrpc_message_set($xmlrpc_message); + } + + function xmlrpc_message_tag_close($parser, $tag) { + $xmlrpc_message = $this->xmlrpc_message_get(); + $value_flag = FALSE; + switch ($tag) { + case 'int': + case 'i4': + $value = (int)trim($xmlrpc_message->current_tag_contents); + $value_flag = TRUE; + break; + case 'double': + $value = (double)trim($xmlrpc_message->current_tag_contents); + $value_flag = TRUE; + break; + case 'string': + $value = $xmlrpc_message->current_tag_contents; + $value_flag = TRUE; + break; + case 'dateTime.iso8601': + $value = xmlrpc_date(trim($xmlrpc_message->current_tag_contents)); + // $value = $iso->getTimestamp(); + $value_flag = TRUE; + break; + case 'value': + // If no type is indicated, the type is string + // We take special care for empty values + if (trim($xmlrpc_message->current_tag_contents) != '' || (isset($xmlrpc_message->last_open) && ($xmlrpc_message->last_open == 'value'))) { + $value = (string)$xmlrpc_message->current_tag_contents; + $value_flag = TRUE; + } + unset($xmlrpc_message->last_open); + break; + case 'boolean': + $value = (boolean)trim($xmlrpc_message->current_tag_contents); + $value_flag = TRUE; + break; + case 'base64': + $value = base64_decode(trim($xmlrpc_message->current_tag_contents)); + $value_flag = TRUE; + break; + // Deal with stacks of arrays and structs + case 'data': + case 'struct': + $value = array_pop($xmlrpc_message->array_structs ); + array_pop($xmlrpc_message->array_structs_types); + $value_flag = TRUE; + break; + case 'member': + array_pop($xmlrpc_message->current_struct_name); + break; + case 'name': + $xmlrpc_message->current_struct_name[] = trim($xmlrpc_message->current_tag_contents); + break; + case 'methodName': + $xmlrpc_message->methodname = trim($xmlrpc_message->current_tag_contents); + break; + } + if ($value_flag) { + if (count($xmlrpc_message->array_structs ) > 0) { + // Add value to struct or array + if ($xmlrpc_message->array_structs_types[count($xmlrpc_message->array_structs_types)-1] == 'struct') { + // Add to struct + $xmlrpc_message->array_structs [count($xmlrpc_message->array_structs )-1][$xmlrpc_message->current_struct_name[count($xmlrpc_message->current_struct_name)-1]] = $value; + } + else { + // Add to array + $xmlrpc_message->array_structs [count($xmlrpc_message->array_structs )-1][] = $value; + } + } + else { + // Just add as a parameter + $xmlrpc_message->params[] = $value; + } + } + if (!in_array($tag, array("data", "struct", "member"))) { + $xmlrpc_message->current_tag_contents = ''; + } + $this->xmlrpc_message_set($xmlrpc_message); + } + + /** + * Construct an object representing an XML-RPC request + * + * @param $method + * The name of the method to be called + * @param $args + * An array of parameters to send with the method. + * @return + * Object + */ + function xmlrpc_request($method, $args) { + $xmlrpc_request = new stdClass(); + $xmlrpc_request->method = $method; + $xmlrpc_request->args = $args; + $xmlrpc_request->xml = <<<EOD + <?xml version="1.0"?> + <methodCall> + <methodName>{$xmlrpc_request->method}</methodName> + <params> + +EOD; + foreach ($xmlrpc_request->args as $arg) { + $xmlrpc_request->xml .= '<param><value>'; + $v = $this->xmlrpc_value($arg); + $xmlrpc_request->xml .= $this->xmlrpc_value_get_xml($v); + $xmlrpc_request->xml .= "</value></param>\n"; + } + $xmlrpc_request->xml .= '</params></methodCall>'; + return $xmlrpc_request; + } + + + function xmlrpc_error($code = NULL, $message = NULL, $reset = FALSE) { + static $xmlrpc_error; + if (isset($code)) { + $xmlrpc_error = new stdClass(); + $xmlrpc_error->is_error = TRUE; + $xmlrpc_error->code = $code; + $xmlrpc_error->message = $message; + } + elseif ($reset) { + $xmlrpc_error = NULL; + } + return $xmlrpc_error; + } + + function xmlrpc_error_get_xml($xmlrpc_error) { + return <<<EOD + <methodResponse> + <fault> + <value> + <struct> + <member> + <name>faultCode</name> + <value><int>{$xmlrpc_error->code}</int></value> + </member> + <member> + <name>faultString</name> + <value><string>{$xmlrpc_error->message}</string></value> + </member> + </struct> + </value> + </fault> + </methodResponse> + +EOD; + } + + function xmlrpc_date($time) { + $xmlrpc_date = new stdClass(); + $xmlrpc_date->is_date = TRUE; + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $xmlrpc_date->year = gmdate('Y', $time); + $xmlrpc_date->month = gmdate('m', $time); + $xmlrpc_date->day = gmdate('d', $time); + $xmlrpc_date->hour = gmdate('H', $time); + $xmlrpc_date->minute = gmdate('i', $time); + $xmlrpc_date->second = gmdate('s', $time); + $xmlrpc_date->iso8601 = gmdate('Ymd\TH:i:s', $time); + } + else { + $xmlrpc_date->iso8601 = $time; + $time = str_replace(array('-', ':'), '', $time); + $xmlrpc_date->year = substr($time, 0, 4); + $xmlrpc_date->month = substr($time, 4, 2); + $xmlrpc_date->day = substr($time, 6, 2); + $xmlrpc_date->hour = substr($time, 9, 2); + $xmlrpc_date->minute = substr($time, 11, 2); + $xmlrpc_date->second = substr($time, 13, 2); + } + return $xmlrpc_date; + } + + function xmlrpc_date_get_xml($xmlrpc_date) { + return '<dateTime.iso8601>'. $xmlrpc_date->year . $xmlrpc_date->month . $xmlrpc_date->day .'T'. $xmlrpc_date->hour .':'. $xmlrpc_date->minute .':'. $xmlrpc_date->second .'</dateTime.iso8601>'; + } + + function xmlrpc_base64($data) { + $xmlrpc_base64 = new stdClass(); + $xmlrpc_base64->is_base64 = TRUE; + $xmlrpc_base64->data = $data; + return $xmlrpc_base64; + } + + function xmlrpc_base64_get_xml($xmlrpc_base64) { + return '<base64>'. base64_encode($xmlrpc_base64->data) .'</base64>'; + } + + /** + * Execute an XML remote procedural call. This is private function; call xmlrpc() + * in common.inc instead of this function. + * + * @return + * A $xmlrpc_message object if the call succeeded; FALSE if the call failed + */ + function _xmlrpc() { + $args = func_get_args(); + $url = array_shift($args); + $this->xmlrpc_clear_error(); + if (is_array($args[0])) { + $method = 'system.multicall'; + $multicall_args = array(); + foreach ($args[0] as $call) { + $multicall_args[] = array('methodName' => array_shift($call), 'params' => $call); + } + $args = array($multicall_args); + } + else { + $method = array_shift($args); + } + $xmlrpc_request = $this->xmlrpc_request($method, $args); + $result = $this->http_request($url, array("Content-Type" => "text/xml"), 'POST', $xmlrpc_request->xml); + if ($result->code != 200) { + $this->xmlrpc_error($result->code, $result->error); + return FALSE; + } + $message = $this->xmlrpc_message($result->data); + // Now parse what we've got back + if (!$this->xmlrpc_message_parse($message)) { + // XML error + $this->xmlrpc_error(-32700, t('Parse error. Not well formed')); + return FALSE; + } + // Is the message a fault? + if ($message->messagetype == 'fault') { + $this->xmlrpc_error($message->fault_code, $message->fault_string); + return FALSE; + } + // Message must be OK + return $message->params[0]; + } + + /** + * Returns the last XML-RPC client error number + */ + function xmlrpc_errno() { + $error = $this->xmlrpc_error(); + return ($error != NULL ? $error->code : NULL); + } + + /** + * Returns the last XML-RPC client error message + */ + function xmlrpc_error_msg() { + $error = xmlrpc_error(); + return ($error != NULL ? $error->message : NULL); + } + + /** + * Clears any previous error. + */ + function xmlrpc_clear_error() { + $this->xmlrpc_error(NULL, NULL, TRUE); + } + +} diff --git a/plugins/Mollom/README b/plugins/Mollom/README new file mode 100644 index 000000000..2b8c2d8a0 --- /dev/null +++ b/plugins/Mollom/README @@ -0,0 +1,22 @@ +The mollom plugin uses mollom.com to filter SN notices for spam. + +== Dependencies == +Your webserver needs to have xmlrpc php extention loaded. +This is called php5-xmlrpc in Debian/Ubuntu + +== Installation == +Add the following to your config.php +<?php +addPlugin('Mollom', + array( + 'public_key' => '...', + 'private_key' => '...', + 'servers' => array('http://88.151.243.81', 'http://82.103.131.136') + ) +); + +?> + +replace '...' with your own public and private keys for your site, which you can get from mollom.com + +If you're using this plugin, i'd love to know about it -- shiny@cpan.org or shiny on freenode. diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index a37d5465e..248afe3fa 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -70,7 +70,7 @@ class OpenIDPlugin extends Plugin $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; } @@ -101,11 +101,11 @@ class OpenIDPlugin extends Plugin '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'), @@ -308,4 +308,15 @@ class OpenIDPlugin extends Plugin $tables[] = 'User_openid_trustroot'; return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'OpenID', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou, Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:OpenID', + 'rawdescription' => + _m('Use <a href="http://openid.net/">OpenID</a> to login to the site.')); + return true; + } } diff --git a/plugins/OpenID/User_openid_trustroot.php b/plugins/OpenID/User_openid_trustroot.php index 44288945b..0b411b8f7 100644 --- a/plugins/OpenID/User_openid_trustroot.php +++ b/plugins/OpenID/User_openid_trustroot.php @@ -22,7 +22,7 @@ class User_openid_trustroot extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - function &pkeyGet($kv) + function pkeyGet($kv) { return Memcached_DataObject::pkeyGet('User_openid_trustroot', $kv); } diff --git a/plugins/PiwikAnalyticsPlugin.php b/plugins/PiwikAnalyticsPlugin.php index fefd09867..b353d7255 100644 --- a/plugins/PiwikAnalyticsPlugin.php +++ b/plugins/PiwikAnalyticsPlugin.php @@ -97,4 +97,16 @@ ENDOFPIWIK; $action->inlineScript($piwikCode2); return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'PiwikAnalytics', + 'version' => STATUSNET_VERSION, + 'author' => 'Tobias Diekershoff, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Piwik', + 'rawdescription' => + _m('Use <a href="http://piwik.org/">Piwik</a> Open Source Web analytics software.')); + return true; + } + } diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php index 460550518..bae6c529d 100644 --- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php +++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php @@ -31,6 +31,16 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +/** + * Outputs 'powered by StatusNet' after site name + * + * @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 PoweredByStatusNetPlugin extends Plugin { function onEndAddressData($action) @@ -42,4 +52,15 @@ class PoweredByStatusNetPlugin extends Plugin return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'PoweredByStatusNet', + 'version' => STATUSNET_VERSION, + 'author' => 'Sarven Capdaisli', + 'homepage' => 'http://status.net/wiki/Plugin:PoweredByStatusNet', + 'rawdescription' => + _m('Outputs powered by <a href="http://status.net/">StatusNet</a> after site name.')); + return true; + } } diff --git a/plugins/PtitUrl/PtitUrlPlugin.php b/plugins/PtitUrl/PtitUrlPlugin.php index 76a438dd5..ddba942e6 100644 --- a/plugins/PtitUrl/PtitUrlPlugin.php +++ b/plugins/PtitUrl/PtitUrlPlugin.php @@ -56,5 +56,18 @@ class PtitUrlPlugin extends UrlShortenerPlugin return strval($xml['href']); } } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => sprintf('PtitUrl (%s)', $this->shortenerName), + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:PtitUrl', + 'rawdescription' => + sprintf(_m('Uses <a href="http://%1$s/">%1$s</a> URL-shortener service.'), + $this->shortenerName)); + + return true; + } } diff --git a/plugins/PubSubHubBub/PubSubHubBubPlugin.php b/plugins/PubSubHubBub/PubSubHubBubPlugin.php index d15a869cb..d9c047de8 100644 --- a/plugins/PubSubHubBub/PubSubHubBubPlugin.php +++ b/plugins/PubSubHubBub/PubSubHubBubPlugin.php @@ -95,14 +95,16 @@ class PubSubHubBubPlugin extends Plugin } //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')); + + $ni = $notice->whoGets(); + + foreach (array_keys($ni) as $user_id) { + $user = User::staticGet('id', $user_id); + if (empty($user)) { + continue; } + $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 diff --git a/plugins/RSSCloud/LoggingAggregator.php b/plugins/RSSCloud/LoggingAggregator.php new file mode 100644 index 000000000..e37eed16a --- /dev/null +++ b/plugins/RSSCloud/LoggingAggregator.php @@ -0,0 +1,140 @@ +<?php +/** + * This test class pretends to be an RSS aggregator. It logs notifications + * from the cloud. + * + * PHP version 5 + * + * @category Plugin + * @package StatusNet + * @author Zach Copley <zach@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); +} + +/** + * Dummy aggregator that acts as a proper notification handler. It + * doesn't do anything but respond correctly when notified via + * REST. Mostly, this is just and action I used to develop the plugin + * and easily test things end-to-end. I'm leaving it in here as it + * may be useful for developing the plugin further. + * + * @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://status.net/ + **/ +class LoggingAggregatorAction extends Action +{ + + var $challenge = null; + var $url = null; + + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean false if user doesn't exist + */ + + function prepare($args) + { + parent::prepare($args); + + $this->url = $this->arg('url'); + $this->challenge = $this->arg('challenge'); + + common_debug("args = " . var_export($this->args, true)); + common_debug('url = ' . $this->url . ' challenge = ' . $this->challenge); + + return true; + } + + /** + * Handle the request + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + if (empty($this->url)) { + $this->showError('Hey, you have to provide a url parameter.'); + return; + } + + if (!empty($this->challenge)) { + + // must be a GET + + if ($_SERVER['REQUEST_METHOD'] != 'GET') { + $this->showError('This resource requires an HTTP GET.'); + return; + } + + header('Content-Type: text/xml'); + echo $this->challenge; + + } else { + + // must be a POST + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->showError('This resource requires an HTTP POST.'); + return; + } + + header('Content-Type: text/xml'); + Echo "<notifyResult success='true' msg='Thanks for the update.' />\n"; + } + + $this->ip = $_SERVER['REMOTE_ADDR']; + + common_log(LOG_INFO, 'RSSCloud Logging Aggregator - ' . + $this->ip . ' claims the feed at ' . + $this->url . ' has been updated.'); + } + + /** + * Show an XML error when things go badly + * + * @param string $msg the error message + * + * @return void + */ + + function showError($msg) + { + header('HTTP/1.1 400 Bad Request'); + header('Content-Type: text/xml'); + echo "<?xml version='1.0'?>\n"; + echo "<notifyResult success='false' msg='$msg' />\n"; + } + +}
\ No newline at end of file diff --git a/plugins/RSSCloud/README b/plugins/RSSCloud/README new file mode 100644 index 000000000..1237e3e0e --- /dev/null +++ b/plugins/RSSCloud/README @@ -0,0 +1,54 @@ +This plugin enables RSSCloud (http://rsscloud.org/) publishing and +subscription handling for RSS 2.0 profile feeds (i.e: +http://SITE/PATH/api/statuses/user_timeline/USERNAME.rss). When the +plugin is enabled, StatusNet acts as both the publisher and hub ('writer' and +'cloud' in RSSCloud parlance), but only for local StatusNet feeds. It's +not possible to use it as a general purpose hub -- for instance you can't +subscribe and get updates to a Wordpress feed from StatusNet using this +plugin. + +To use the plugin, add the following to your config.php: + + addPlugin('RSSCloud'); + +Enabling the plugin will add a <cloud> element to your RSS 2.0 profile feeds +that looks like this: + + <cloud domain="SITE" port="80" path="/main/rsscloud/request_notify" + registerProcedure="" protocol="http-post"/> + +Aggregators may subscribe by sending a proper REST RSSCloud subscription +request (the optional 'domain' parameter with challenge is supported). +Subscribing aggregators will be notified ('pinged') when users they have +subscribed to post new notices. Currently, REST is the only protocol +supported for notifications. + +Deamon +------ + +There's also a daemon for offline processing of queued notices with +RSSCloud destinations, which will start automatically if/when you run +scripts/startdaemons.sh. + +Notes +----- + +- Again, only RSS 2.0 profile feeds may be subscribed to, and they have + to be the ones with user names in them, like: + http://SITE/PATH/api/statuses/user_timeline/USERNAME.rss +- Subscriptions are deleted after three notification failures in a row + (not sure this is optimal). +- The plugin includes a dummy LoggingAggregator class that can be used + for end-to-end testing. You probably don't want to mess with it. + +TODO +---- + +- Figure out why the RSSCloudSubcription can't ->delete() or ->update() +- Support pinging via XML-RPC and SOAP +- Automatically delete subscriptions? Point of reference: Dave's hub + implementation auto-deletes them after 25 hours. WordPress never deletes them. +- Support additional feed URL addresses for the same feed (e.g.: by numeric ID, + ?user_id=xxx, etc.) +- Support additional feeds that make sense (e.g: replies)? +- Possibly use "rssCloud" (like Dave) instead of "RSSCloud" everywhere diff --git a/plugins/RSSCloud/RSSCloudNotifier.php b/plugins/RSSCloud/RSSCloudNotifier.php new file mode 100644 index 000000000..d454691c8 --- /dev/null +++ b/plugins/RSSCloud/RSSCloudNotifier.php @@ -0,0 +1,240 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Class to ping an rssCloud endpoint when a feed has been updated + * + * 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 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); +} + +/** + * Class for notifying cloud-enabled RSS aggregators that StatusNet + * feeds have been updated. + * + * @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://status.net/ + **/ +class RSSCloudNotifier +{ + const MAX_FAILURES = 3; + + /** + * Send an HTTP GET to the notification handler with a + * challenge string to see if it repsonds correctly. + * + * @param string $endpoint URL of the notification handler + * @param string $feed the feed being subscribed to + * + * @return boolean success + */ + function challenge($endpoint, $feed) + { + $code = common_confirmation_code(128); + $params = array('url' => $feed, 'challenge' => $code); + $url = $endpoint . '?' . http_build_query($params); + + try { + $client = new HTTPClient(); + $response = $client->get($url); + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_INFO, + 'RSSCloud plugin - failure testing notify handler ' . + $endpoint . ' - ' . $e->getMessage()); + return false; + } + + // Check response is betweet 200 and 299 and body contains challenge data + + $status = $response->getStatus(); + $body = $response->getBody(); + + if ($status >= 200 && $status < 300) { + + // NOTE: the spec says that the body must contain the string + // challenge. It doesn't say that the body must contain the + // challenge string ONLY, although that seems to be the way + // the other implementors have interpreted it. + + if (strpos($body, $code) !== false) { + common_log(LOG_INFO, 'RSSCloud plugin - ' . + "success testing notify handler: $endpoint"); + return true; + } else { + common_log(LOG_INFO, 'RSSCloud plugin - ' . + 'challenge/repsonse failed for notify handler ' . + $endpoint); + common_debug('body = ' . var_export($body, true)); + return false; + } + } else { + common_log(LOG_INFO, 'RSSCloud plugin - ' . + "failure testing notify handler: $endpoint " . + ' - got HTTP ' . $status); + common_debug('body = ' . var_export($body, true)); + return false; + } + } + + /** + * HTTP POST a notification that a feed has been updated + * ('ping the cloud'). + * + * @param String $endpoint URL of the notification handler + * @param String $feed the feed being subscribed to + * + * @return boolean success + */ + function postUpdate($endpoint, $feed) + { + + $headers = array(); + $postdata = array('url' => $feed); + + try { + $client = new HTTPClient(); + $response = $client->post($endpoint, $headers, $postdata); + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_INFO, 'RSSCloud plugin - failure notifying ' . + $endpoint . ' that feed ' . $feed . + ' has changed: ' . $e->getMessage()); + return false; + } + + $status = $response->getStatus(); + + if ($status >= 200 && $status < 300) { + common_log(LOG_INFO, 'RSSCloud plugin - success notifying ' . + $endpoint . ' that feed ' . $feed . ' has changed.'); + return true; + } else { + common_log(LOG_INFO, 'RSSCloud plugin - failure notifying ' . + $endpoint . ' that feed ' . $feed . + ' has changed: got HTTP ' . $status); + return false; + } + } + + /** + * Notify all subscribers to a profile feed that it has changed. + * + * @param Profile $profile the profile whose feed has been + * updated + * + * @return boolean success + */ + function notify($profile) + { + $feed = common_path('api/statuses/user_timeline/') . + $profile->nickname . '.rss'; + + $cloudSub = new RSSCloudSubscription(); + + $cloudSub->subscribed = $profile->id; + + if ($cloudSub->find()) { + while ($cloudSub->fetch()) { + $result = $this->postUpdate($cloudSub->url, $feed); + if ($result == false) { + $this->handleFailure($cloudSub); + } + } + } + + return true; + } + + /** + * Handle problems posting cloud notifications. Increment the failure + * count, or delete the subscription if the maximum number of failures + * is exceeded. + * + * XXX: Redo with proper DB_DataObject methods once I figure out what + * what the problem is with pluginized DB_DataObjects. -Z + * + * @param RSSCloudSubscription $cloudSub the subscription in question + * + * @return boolean success + */ + function handleFailure($cloudSub) + { + $failCnt = $cloudSub->failures + 1; + + if ($failCnt == self::MAX_FAILURES) { + + common_log(LOG_INFO, + 'Deleting RSSCloud subcription ' . + '(max failure count reached), profile: ' . + $cloudSub->subscribed . + ' handler: ' . + $cloudSub->url); + + // XXX: WTF! ->delete() doesn't work. Clearly, there are some issues with + // the DB_DataObject, or my understanding of it. Have to drop into SQL. + + // $result = $cloudSub->delete(); + + $qry = 'DELETE from rsscloud_subscription' . + ' WHERE subscribed = ' . $cloudSub->subscribed . + ' AND url = \'' . $cloudSub->url . '\''; + + $result = $cloudSub->query($qry); + + if (!$result) { + common_log_db_error($cloudSub, 'DELETE', __FILE__); + common_log(LOG_ERR, 'Could not delete RSSCloud subscription.'); + } + + } else { + + common_debug('Updating failure count on RSSCloud subscription. ' . + $failCnt); + + $failCnt = $cloudSub->failures + 1; + + // XXX: ->update() not working either, gar! + + $qry = 'UPDATE rsscloud_subscription' . + ' SET failures = ' . $failCnt . + ' WHERE subscribed = ' . $cloudSub->subscribed . + ' AND url = \'' . $cloudSub->url . '\''; + + $result = $cloudSub->query($qry); + + if (!$result) { + common_log_db_error($cloudsub, 'UPDATE', __FILE__); + common_log(LOG_ERR, + 'Could not update failure ' . + 'count on RSSCloud subscription'); + } + } + } + +} + diff --git a/plugins/RSSCloud/RSSCloudPlugin.php b/plugins/RSSCloud/RSSCloudPlugin.php new file mode 100644 index 000000000..2de162628 --- /dev/null +++ b/plugins/RSSCloud/RSSCloudPlugin.php @@ -0,0 +1,295 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to support RSSCloud + * + * 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 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); +} + +define('RSSCLOUDPLUGIN_VERSION', '0.1'); + +/** + * Plugin class for adding RSSCloud capabilities to StatusNet + * + * @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://status.net/ + **/ + +class RSSCloudPlugin extends Plugin +{ + /** + * Our friend, the constructor + * + * @return void + */ + function __construct() + { + parent::__construct(); + } + + /** + * Setup the info for the subscription handler. Allow overriding + * to point at another cloud hub (not currently used). + * + * @return void + */ + + function onInitializePlugin() + { + $this->domain = common_config('rsscloud', 'domain'); + $this->port = common_config('rsscloud', 'port'); + $this->path = common_config('rsscloud', 'path'); + $this->funct = common_config('rsscloud', 'function'); + $this->protocol = common_config('rsscloud', 'protocol'); + + // set defaults + + $local_server = parse_url(common_path('main/rsscloud/request_notify')); + + if (empty($this->domain)) { + $this->domain = $local_server['host']; + } + + if (empty($this->port)) { + $this->port = '80'; + } + + if (empty($this->path)) { + $this->path = $local_server['path']; + } + + if (empty($this->funct)) { + $this->funct = ''; + } + + if (empty($this->protocol)) { + $this->protocol = 'http-post'; + } + } + + /** + * Add RSSCloud-related paths to the router table + * + * Hook for RouterInitialized event. + * + * @param Mapper &$m URL parser and mapper + * + * @return boolean hook return + */ + + function onRouterInitialized(&$m) + { + $m->connect('/main/rsscloud/request_notify', + array('action' => 'RSSCloudRequestNotify')); + + // XXX: This is just for end-to-end testing. Uncomment if you need to pretend + // to be a cloud hub for some reason. + //$m->connect('/main/rsscloud/notify', + // array('action' => 'LoggingAggregator')); + + return true; + } + + /** + * Automatically load the actions and libraries used by + * the RSSCloud plugin + * + * @param Class $cls the class + * + * @return boolean hook return + * + */ + + function onAutoload($cls) + { + switch ($cls) + { + case 'RSSCloudSubscription': + include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudSubscription.php'; + return false; + case 'RSSCloudNotifier': + include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudNotifier.php'; + return false; + case 'RSSCloudRequestNotifyAction': + case 'LoggingAggregatorAction': + include_once INSTALLDIR . '/plugins/RSSCloud/' . + mb_substr($cls, 0, -6) . '.php'; + return false; + default: + return true; + } + } + + /** + * Add a <cloud> element to the RSS feed (after the rss <channel> + * element is started). + * + * @param Action $action the ApiAction + * + * @return void + */ + + function onStartApiRss($action) + { + if (get_class($action) == 'ApiTimelineUserAction') { + + $attrs = array('domain' => $this->domain, + 'port' => $this->port, + 'path' => $this->path, + 'registerProcedure' => $this->funct, + 'protocol' => $this->protocol); + + // Dipping into XMLWriter to avoid a full end element (</cloud>). + + $action->xw->startElement('cloud'); + foreach ($attrs as $name => $value) { + $action->xw->writeAttribute($name, $value); + } + + $action->xw->endElement(); + } + } + + /** + * Add an RSSCloud 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, 'rsscloud'); + 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 == 'rsscloud') && ($this->_isLocal($notice))) { + + common_debug('broadcasting rssCloud bound notice ' . $notice->id); + + $profile = $notice->getProfile(); + + $notifier = new RSSCloudNotifier(); + $notifier->notify($profile); + + return false; + } + + return true; + } + + /** + * Determine whether the notice was locally created + * + * @param Notice $notice the notice in question + * + * @return boolean locality + */ + + function _isLocal($notice) + { + return ($notice->is_local == Notice::LOCAL_PUBLIC || + $notice->is_local == Notice::LOCAL_NONPUBLIC); + } + + /** + * Create the rsscloud_subscription table if it's not + * already in the DB + * + * @return boolean hook return + */ + + function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('rsscloud_subscription', + array(new ColumnDef('subscribed', 'integer', + null, false, 'PRI'), + new ColumnDef('url', 'varchar', + '255', false, 'PRI'), + new ColumnDef('failures', 'integer', + null, false, null, 0), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', 'timestamp', + null, false, null, + 'CURRENT_TIMESTAMP', + 'on update CURRENT_TIMESTAMP') + )); + return true; + } + + /** + * Add RSSCloudQueueHandler to the list of valid daemons to + * start + * + * @param array $daemons the list of daemons to run + * + * @return boolean hook return + * + */ + + function onGetValidDaemons($daemons) + { + array_push($daemons, INSTALLDIR . + '/plugins/RSSCloud/RSSCloudQueueHandler.php'); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'RSSCloud', + 'version' => RSSCLOUDPLUGIN_VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:RSSCloud', + 'rawdescription' => + _m('The RSSCloud plugin enables your StatusNet instance to publish ' . + 'real-time updates for profile RSS feeds using the ' . + '<a href="http://rsscloud.org/">RSSCloud protocol</a>".')); + + return true; + } + +} + diff --git a/plugins/RSSCloud/RSSCloudQueueHandler.php b/plugins/RSSCloud/RSSCloudQueueHandler.php new file mode 100755 index 000000000..693dd27c1 --- /dev/null +++ b/plugins/RSSCloud/RSSCloudQueueHandler.php @@ -0,0 +1,78 @@ +#!/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 RSSCloud subscribers. + + -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/RSSCloud/RSSCloudNotifier.php'; +require_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudSubscription.php'; + +class RSSCloudQueueHandler extends QueueHandler +{ + var $notifier = null; + + function transport() + { + return 'rsscloud'; + } + + function start() + { + $this->log(LOG_INFO, "INITIALIZE"); + $this->notifier = new RSSCloudNotifier(); + return true; + } + + function handle_notice($notice) + { + $profile = $notice->getProfile(); + return $this->notifier->notify($profile); + } + + 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 RSSCloudQueueHandler($id); + +$handler->runOnce(); diff --git a/plugins/RSSCloud/RSSCloudRequestNotify.php b/plugins/RSSCloud/RSSCloudRequestNotify.php new file mode 100644 index 000000000..d76c08d37 --- /dev/null +++ b/plugins/RSSCloud/RSSCloudRequestNotify.php @@ -0,0 +1,347 @@ +<?php +/** + * Action to let RSSCloud aggregators request update notification when + * user profile feeds change. + * + * PHP version 5 + * + * @category Plugin + * @package StatusNet + * @author Zach Copley <zach@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 class to handle RSSCloud notification (subscription) requests + * + * @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://status.net/ + **/ + +class RSSCloudRequestNotifyAction extends Action +{ + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean false if user doesn't exist + */ + + function prepare($args) + { + parent::prepare($args); + + $this->ip = $_SERVER['REMOTE_ADDR']; + $this->port = $this->arg('port'); + $this->path = $this->arg('path'); + + if ($this->path[0] != '/') { + $this->path = '/' . $this->path; + } + + $this->protocol = $this->arg('protocol'); + $this->procedure = $this->arg('notifyProcedure'); + $this->domain = $this->arg('domain'); + + $this->feeds = $this->getFeeds(); + + return true; + } + + /** + * Handle the request + * + * Checks for all the required parameters for a subscription, + * validates that the feed being subscribed to is real, and then + * saves the subsctiption. + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->showResult(false, 'Request must be POST.'); + return; + } + + $missing = array(); + + if (empty($this->port)) { + $missing[] = 'port'; + } + + if (empty($this->path)) { + $missing[] = 'path'; + } + + if (empty($this->protocol)) { + $missing[] = 'protocol'; + } else if (strtolower($this->protocol) != 'http-post') { + $msg = 'Only http-post notifications are supported at this time.'; + $this->showResult(false, $msg); + return; + } + + if (!isset($this->procedure)) { + $missing[] = 'notifyProcedure'; + } + + if (!empty($missing)) { + $msg = 'The following parameters were missing from the request body: ' . + implode(', ', $missing) . '.'; + $this->showResult(false, $msg); + return; + } + + if (empty($this->feeds)) { + $msg = 'You must provide at least one valid profile feed url ' . + '(url1, url2, url3 ... urlN).'; + $this->showResult(false, $msg); + return; + } + + // We have to validate everything before saving anything. + // We only return one success or failure no matter how + // many feeds the subscriber is trying to subscribe to + + foreach ($this->feeds as $feed) { + + if (!$this->validateFeed($feed)) { + + $nh = $this->getNotifyUrl(); + common_log(LOG_WARNING, + "RSSCloud plugin - $nh tried to subscribe to invalid feed: $feed"); + + $msg = 'Feed subscription failed - Not a valid feed.'; + $this->showResult(false, $msg); + return; + } + + if (!$this->testNotificationHandler($feed)) { + $msg = 'Feed subscription failed - ' . + 'notification handler doesn\'t respond correctly.'; + $this->showResult(false, $msg); + return; + } + + } + + foreach ($this->feeds as $feed) { + $this->saveSubscription($feed); + } + + // XXX: What to do about deleting stale subscriptions? + // 25 hours seems harsh. WordPress doesn't ever remove + // subscriptions. + + $msg = 'Thanks for the subscription. ' . + 'When the feed(s) update(s) we\'ll notify you.'; + + $this->showResult(true, $msg); + } + + /** + * Validate that the requested feed is one we serve + * up via RSSCloud. + * + * @param string $feed the feed in question + * + * @return void + */ + + function validateFeed($feed) + { + $user = $this->userFromFeed($feed); + + if (empty($user)) { + return false; + } + + return true; + } + + /** + * Pull all of the urls (url1, url2, url3...urlN) that + * the subscriber wants to subscribe to. + * + * @return array $feeds the list of feeds + */ + + function getFeeds() + { + $feeds = array(); + + while (list($key, $feed) = each($this->args)) { + if (preg_match('/^url\d*$/', $key)) { + $feeds[] = $feed; + } + } + + return $feeds; + } + + /** + * Test that a notification handler is there and is reponding + * correctly. This is called before adding a subscription. + * + * @param string $feed the feed to verify + * + * @return boolean success result + */ + + function testNotificationHandler($feed) + { + $notifyUrl = $this->getNotifyUrl(); + + $notifier = new RSSCloudNotifier(); + + if (isset($this->domain)) { + + // 'domain' param set, so we have to use GET and send a challenge + + common_log(LOG_INFO, + 'RSSCloud plugin - Testing notification handler with challenge: ' . + $notifyUrl); + return $notifier->challenge($notifyUrl, $feed); + + } else { + common_log(LOG_INFO, 'RSSCloud plugin - Testing notification handler: ' . + $notifyUrl); + + return $notifier->postUpdate($notifyUrl, $feed); + } + } + + /** + * Build the URL for the notification handler based on the + * parameters passed in with the subscription request. + * + * @return string notification handler url + */ + + function getNotifyUrl() + { + if (isset($this->domain)) { + return 'http://' . $this->domain . ':' . $this->port . $this->path; + } else { + return 'http://' . $this->ip . ':' . $this->port . $this->path; + } + } + + /** + * Uses the nickname part of the subscribed feed URL to figure out + * whethere there's really a user with such a feed. Used to + * validate feeds before adding a subscription. + * + * @param string $feed the feed in question + * + * @return boolean success + */ + + function userFromFeed($feed) + { + // We only do profile feeds + + $path = common_path('api/statuses/user_timeline/'); + $valid = '%^' . $path . '(?<nickname>.*)\.rss$%'; + + if (preg_match($valid, $feed, $matches)) { + $user = User::staticGet('nickname', $matches['nickname']); + if (!empty($user)) { + return $user; + } + } + + return false; + } + + /** + * Save an RSSCloud subscription + * + * @param string $feed a valid profile feed + * + * @return boolean success result + */ + + function saveSubscription($feed) + { + $user = $this->userFromFeed($feed); + + $notifyUrl = $this->getNotifyUrl(); + + $sub = RSSCloudSubscription::getSubscription($user->id, $notifyUrl); + + if ($sub) { + common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl refreshed subscription" . + " to user $user->nickname (id: $user->id)."); + } else { + + $sub = new RSSCloudSubscription(); + + $sub->subscribed = $user->id; + $sub->url = $notifyUrl; + $sub->created = common_sql_now(); + + if (!$sub->insert()) { + common_log_db_error($sub, 'INSERT', __FILE__); + return false; + } + + common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl subscribed" . + " to user $user->nickname (id: $user->id)"); + } + + return true; + } + + /** + * Show an XML message indicating the subscription + * was successful or failed. + * + * @param boolean $success whether it was good or bad + * @param string $msg the message to output + * + * @return boolean success result + */ + + function showResult($success, $msg) + { + $this->startXML(); + $this->elementStart('notifyResult', + array('success' => ($success) ? 'true' : 'false', + 'msg' => $msg)); + $this->endXML(); + } + +} + diff --git a/plugins/RSSCloud/RSSCloudSubscription.php b/plugins/RSSCloud/RSSCloudSubscription.php new file mode 100644 index 000000000..396c604e7 --- /dev/null +++ b/plugins/RSSCloud/RSSCloudSubscription.php @@ -0,0 +1,79 @@ +<?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); +} + +/** + * Table Definition for rsscloud_subscription + */ + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +class RSSCloudSubscription extends Memcached_DataObject { + + var $__table='rsscloud_subscription'; // table name + var $subscribed; // int primary key user id + var $url; // string primary key + var $failures; // int + var $created; // datestamp() + var $modified; // timestamp() not_null default_CURRENT_TIMESTAMP + + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('DataObjects_Grp',$k,$v); } + + function table() + { + + $db = $this->getDatabaseConnection(); + $dbtype = $db->phptype; + + $cols = array('subscribed' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'url' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'failures' => DB_DATAOBJECT_INT, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => ($dbtype == 'mysql' || $dbtype == 'mysqli') ? + DB_DATAOBJECT_MYSQLTIMESTAMP + DB_DATAOBJECT_NOTNULL : + DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + ); + + return $cols; + } + + function keys() + { + return array('subscribed' => 'N', 'url' => 'N'); + } + + static function getSubscription($subscribed, $url) + { + $sub = new RSSCloudSubscription(); + $sub->whereAdd("subscribed = $subscribed"); + $sub->whereAdd("url = '$url'"); + $sub->limit(1); + + if ($sub->find()) { + $sub->fetch(); + return $sub; + } + + return false; + } + +} diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index a810b7165..89640f5be 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -154,14 +154,11 @@ class RealtimePlugin extends Plugin // Add to inbox timelines // XXX: do a join - $inbox = new Notice_inbox(); - $inbox->notice_id = $notice->id; + $ni = $notice->whoGets(); - if ($inbox->find()) { - while ($inbox->fetch()) { - $user = User::staticGet('id', $inbox->user_id); - $paths[] = array('all', $user->nickname); - } + foreach (array_keys($ni) as $user_id) { + $user = User::staticGet('id', $user_id); + $paths[] = array('all', $user->nickname); } // Add to the replies timeline @@ -310,8 +307,7 @@ class RealtimePlugin extends Plugin function _getScripts() { - return array('plugins/Realtime/realtimeupdate.js', - 'plugins/Realtime/json2.js'); + return array('plugins/Realtime/realtimeupdate.js'); } function _updateInitialize($timeline, $user_id) diff --git a/plugins/Realtime/json2.js b/plugins/Realtime/json2.js deleted file mode 100644 index 7e27df518..000000000 --- a/plugins/Realtime/json2.js +++ /dev/null @@ -1,478 +0,0 @@ -/* - http://www.JSON.org/json2.js - 2009-04-16 - - Public Domain. - - NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. - - See http://www.JSON.org/js.html - - This file creates a global JSON object containing two methods: stringify - and parse. - - JSON.stringify(value, replacer, space) - value any JavaScript value, usually an object or array. - - replacer an optional parameter that determines how object - values are stringified for objects. It can be a - function or an array of strings. - - space an optional parameter that specifies the indentation - of nested structures. If it is omitted, the text will - be packed without extra whitespace. If it is a number, - it will specify the number of spaces to indent at each - level. If it is a string (such as '\t' or ' '), - it contains the characters used to indent at each level. - - This method produces a JSON text from a JavaScript value. - - When an object value is found, if the object contains a toJSON - method, its toJSON method will be called and the result will be - stringified. A toJSON method does not serialize: it returns the - value represented by the name/value pair that should be serialized, - or undefined if nothing should be serialized. The toJSON method - will be passed the key associated with the value, and this will be - bound to the object holding the key. - - For example, this would serialize Dates as ISO strings. - - Date.prototype.toJSON = function (key) { - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - return this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z'; - }; - - You can provide an optional replacer method. It will be passed the - key and value of each member, with this bound to the containing - object. The value that is returned from your method will be - serialized. If your method returns undefined, then the member will - be excluded from the serialization. - - If the replacer parameter is an array of strings, then it will be - used to select the members to be serialized. It filters the results - such that only members with keys listed in the replacer array are - stringified. - - Values that do not have JSON representations, such as undefined or - functions, will not be serialized. Such values in objects will be - dropped; in arrays they will be replaced with null. You can use - a replacer function to replace those with JSON values. - JSON.stringify(undefined) returns undefined. - - The optional space parameter produces a stringification of the - value that is filled with line breaks and indentation to make it - easier to read. - - If the space parameter is a non-empty string, then that string will - be used for indentation. If the space parameter is a number, then - the indentation will be that many spaces. - - Example: - - text = JSON.stringify(['e', {pluribus: 'unum'}]); - // text is '["e",{"pluribus":"unum"}]' - - - text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); - // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' - - text = JSON.stringify([new Date()], function (key, value) { - return this[key] instanceof Date ? - 'Date(' + this[key] + ')' : value; - }); - // text is '["Date(---current time---)"]' - - - JSON.parse(text, reviver) - This method parses a JSON text to produce an object or array. - It can throw a SyntaxError exception. - - The optional reviver parameter is a function that can filter and - transform the results. It receives each of the keys and values, - and its return value is used instead of the original value. - If it returns what it received, then the structure is not modified. - If it returns undefined then the member is deleted. - - Example: - - // Parse the text. Values that look like ISO date strings will - // be converted to Date objects. - - myData = JSON.parse(text, function (key, value) { - var a; - if (typeof value === 'string') { - a = -/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); - if (a) { - return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], - +a[5], +a[6])); - } - } - return value; - }); - - myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { - var d; - if (typeof value === 'string' && - value.slice(0, 5) === 'Date(' && - value.slice(-1) === ')') { - d = new Date(value.slice(5, -1)); - if (d) { - return d; - } - } - return value; - }); - - - This is a reference implementation. You are free to copy, modify, or - redistribute. - - This code should be minified before deployment. - See http://javascript.crockford.com/jsmin.html - - USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO - NOT CONTROL. -*/ - -/*jslint evil: true */ - -/*global JSON */ - -/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, - call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, - getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, - lastIndex, length, parse, prototype, push, replace, slice, stringify, - test, toJSON, toString, valueOf -*/ - -// Create a JSON object only if one does not already exist. We create the -// methods in a closure to avoid creating global variables. - -if (!this.JSON) { - JSON = {}; -} -(function () { - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - if (typeof Date.prototype.toJSON !== 'function') { - - Date.prototype.toJSON = function (key) { - - return this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z'; - }; - - String.prototype.toJSON = - Number.prototype.toJSON = - Boolean.prototype.toJSON = function (key) { - return this.valueOf(); - }; - } - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? - '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : - '"' + string + '"'; - } - - - function str(key, holder) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 ? '[]' : - gap ? '[\n' + gap + - partial.join(',\n' + gap) + '\n' + - mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - k = rep[i]; - if (typeof k === 'string') { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 ? '{}' : - gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + - mind + '}' : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - if (typeof JSON.stringify !== 'function') { - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - } - - -// If the JSON object does not yet have a parse method, give it one. - - if (typeof JSON.parse !== 'function') { - JSON.parse = function (text, reviver) { - -// The parse method takes a text and an optional reviver function, and returns -// a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - -// The walk method is used to recursively walk the resulting structure so -// that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - -// Parsing happens in four stages. In the first stage, we replace certain -// Unicode characters with escape sequences. JavaScript handles many characters -// incorrectly, either silently deleting them, or treating them as line endings. - - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - -// In the second stage, we run the text against regular expressions that look -// for non-JSON patterns. We are especially concerned with '()' and 'new' -// because they can cause invocation, and '=' because it can cause mutation. -// But just to be safe, we want to reject all unexpected forms. - -// We split the second stage into 4 regexp operations in order to work around -// crippling inefficiencies in IE's and Safari's regexp engines. First we -// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we -// replace all simple value tokens with ']' characters. Third, we delete all -// open brackets that follow a colon or comma or that begin the text. Finally, -// we look to see that the remaining characters are only whitespace or ']' or -// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/. -test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). -replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). -replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - -// In the third stage we use the eval function to compile the text into a -// JavaScript structure. The '{' operator is subject to a syntactic ambiguity -// in JavaScript: it can begin a block or an object literal. We wrap the text -// in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - -// In the optional fourth stage, we recursively walk the new structure, passing -// each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } - -// If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - } -}()); diff --git a/plugins/Recaptcha/RecaptchaPlugin.php b/plugins/Recaptcha/RecaptchaPlugin.php index db118dbb8..3665214f8 100644 --- a/plugins/Recaptcha/RecaptchaPlugin.php +++ b/plugins/Recaptcha/RecaptchaPlugin.php @@ -62,9 +62,8 @@ class RecaptchaPlugin extends Plugin function onEndRegistrationFormData($action) { - $action->style('#recaptcha_area{float:left;}'); $action->elementStart('li'); - $action->raw('<label for="recaptcha_area">Captcha</label>'); + $action->raw('<label for="recaptcha">Captcha</label>'); if($this->checkssl() === true) { $action->raw(recaptcha_get_html($this->public_key), null, true); } else { diff --git a/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php index d48283b2e..d157ea067 100644 --- a/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php +++ b/plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/plugins/Authentication/AuthenticationPlugin.php'; - class ReverseUsernameAuthenticationPlugin extends AuthenticationPlugin { //---interface implementation---// diff --git a/plugins/Sample/SamplePlugin.php b/plugins/Sample/SamplePlugin.php index 6e361aafb..913741226 100644 --- a/plugins/Sample/SamplePlugin.php +++ b/plugins/Sample/SamplePlugin.php @@ -1,8 +1,12 @@ <?php -/* +/** * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2009, StatusNet, Inc. * + * A sample module to show best practices for StatusNet plugins + * + * PHP version 5 + * * 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 @@ -15,44 +19,262 @@ * * 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 Sample + * @package StatusNet + * @author Brion Vibber <brionv@status.net> + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ */ -/** - * @package SamplePlugin - * @maintainer Your Name <you@example.com> - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { +if (!defined('STATUSNET')) { // This check helps protect against security problems; // your code file can't be executed directly from the web. exit(1); } +/** + * Sample plugin main class + * + * Each plugin requires a main class to interact with the StatusNet system. + * + * The main class usually extends the Plugin class that comes with StatusNet. + * + * The class has standard-named methods that will be called when certain events + * happen in the code base. These methods have names like 'onX' where X is an + * event name (see EVENTS.txt for the list of available events). Event handlers + * have pre-defined arguments, based on which event they're handling. A typical + * event handler: + * + * function onSomeEvent($paramA, &$paramB) + * { + * if ($paramA == 'jed') { + * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); + * } + * $paramB = 'spock'; + * return true; + * } + * + * Event handlers must return a boolean value. If they return false, all other + * event handlers for this event (in other plugins) will be skipped, and in some + * cases the default processing for that event would be skipped. This is great for + * replacing the default action of an event. + * + * If the handler returns true, processing of other event handlers and the default + * processing will continue. This is great for extending existing functionality. + * + * If the handler throws an exception, processing will stop, and the exception's + * error will be shown to the user. + * + * To install a plugin (like this one), site admins add the following code to + * their config.php file: + * + * addPlugin('Sample'); + * + * Plugins must be installed in one of the following directories: + * + * local/plugins/{$pluginclass}.php + * local/plugins/{$name}/{$pluginclass}.php + * local/{$pluginclass}.php + * local/{$name}/{$pluginclass}.php + * plugins/{$pluginclass}.php + * plugins/{$name}/{$pluginclass}.php + * + * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is + * the name of the main class, like 'SamplePlugin'. Plugins that are part of the + * main StatusNet distribution go in 'plugins' and third-party or local ones go + * in 'local'. + * + * Simple plugins can be implemented as a single module. Others are more complex + * and require additional modules; these should use their own directory, like + * 'local/plugins/{$name}/'. All files related to the plugin, including images, + * JavaScript, CSS, external libraries or PHP modules should go in the plugin + * directory. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber <brionv@status.net> + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class SamplePlugin extends Plugin { - function onInitializePlugin() + /** + * Plugins are configured using public instance attributes. To set + * their values, site administrators use this syntax: + * + * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); + * + * The same plugin class can be initialized multiple times with different + * arguments: + * + * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); + * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); + * + */ + + public $attr1 = null; + public $attr2 = null; + + /** + * Initializer for this plugin + * + * Plugins overload this method to do any initialization they need, + * like connecting to remote servers or creating paths or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function initialize() + { + return true; + } + + /** + * Cleanup for this plugin + * + * Plugins overload this method to do any cleanup they need, + * like disconnecting from remote servers or deleting temp files or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function cleanup() { - // Event handlers normally return true to indicate that all is well. - // - // Returning false will cancel further processing of any other - // plugins or core code hooking the same event. return true; } /** - * Hook for RouterInitialized event. + * Database schema setup + * + * Plugins can add their own tables to the StatusNet database. Plugins + * should use StatusNet's schema interface to add or delete tables. The + * ensureTable() method provides an easy way to ensure a table's structure + * and availability. + * + * By default, the schema is checked every time StatusNet is run (say, when + * a Web page is hit). Admins can configure their systems to only check the + * schema when the checkschema.php script is run, greatly improving performance. + * However, they need to remember to run that script after installing or + * upgrading a plugin! + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('user_greeting_count', + array(new ColumnDef('user_id', 'integer', null, + true, 'PRI'), + new ColumnDef('greeting_count', 'integer'))); + + return true; + } + + /** + * Load related modules when needed + * + * Most non-trivial plugins will require extra modules to do their work. Typically + * these include data classes, action classes, widget classes, or external libraries. + * + * This method receives a class name and loads the PHP file related to that class. By + * tradition, action classes typically have files named for the action, all lower-case. + * Data classes are in files with the data class name, initial letter capitalized. + * + * Note that this method will be called for *all* overloaded classes, not just ones + * in this plugin! So, make sure to return true by default to let other plugins, and + * the core code, get a chance. + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'HelloAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'User_greeting_count': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * This event handler lets the plugin map URLs on the site to actions (and + * thus an action handler class). Note that the action handler class for an + * action will be named 'FoobarAction', where action = 'foobar'. The class + * must be loaded in the onAutoload() method. * * @param Net_URL_Mapper $m path-to-action mapper - * @return boolean hook return + * + * @return boolean hook value; true means continue processing, false means stop. */ function onRouterInitialized($m) { - $m->connect(':nickname/samples', - array('action' => 'showsamples'), - array('feed' => '[A-Za-z0-9_-]+')); - $m->connect('settings/sample', - array('action' => 'samplesettings')); + $m->connect('main/hello', + array('action' => 'hello')); + return true; + } + + /** + * Modify the default menu to link to our custom action + * + * Using event handlers, it's possible to modify the default UI for pages + * almost without limit. In this method, we add a menu item to the default + * primary menu for the interface to link to our action. + * + * The Action class provides a rich set of events to hook, as well as output + * methods. + * + * @param Action $action The current action handler. Use this to + * do any output. + * + * @return boolean hook value; true means continue processing, false means stop. + * + * @see Action + */ + + function onEndPrimaryNav($action) + { + // common_local_url() gets the correct URL for the action name + // we provide + + $action->menuItem(common_local_url('hello'), + _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Sample', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Sample', + 'rawdescription' => + _m('A sample plugin to show basics of development for new hackers.')); return true; } } diff --git a/plugins/Sample/User_greeting_count.php b/plugins/Sample/User_greeting_count.php new file mode 100644 index 000000000..d9a59770d --- /dev/null +++ b/plugins/Sample/User_greeting_count.php @@ -0,0 +1,183 @@ +<?php +/** + * Data class for counting greetings + * + * PHP version 5 + * + * @category Data + * @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); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array('user_id' => 'K'); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + throw Exception(sprintf(_m("Could not save new greeting count for %d"), + $user_id)); + } + + } else { + + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + throw Exception(sprintf(_m("Could not increment greeting count for %d"), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Sample/hello.php b/plugins/Sample/hello.php new file mode 100644 index 000000000..0cfd8a1c3 --- /dev/null +++ b/plugins/Sample/hello.php @@ -0,0 +1,166 @@ +<?php +/** + * Give a warm greeting to our friendly user + * + * PHP version 5 + * + * @category Sample + * @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); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s'), $this->user->nickname); + } + } + + /** + * show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * @return void + */ + + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time(s).'), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return false; + } +} diff --git a/plugins/SimpleUrl/SimpleUrlPlugin.php b/plugins/SimpleUrl/SimpleUrlPlugin.php index 45b745b07..6eac7dbb1 100644 --- a/plugins/SimpleUrl/SimpleUrlPlugin.php +++ b/plugins/SimpleUrl/SimpleUrlPlugin.php @@ -47,5 +47,18 @@ class SimpleUrlPlugin extends UrlShortenerPlugin protected function shorten($url) { return $this->http_get(sprintf($this->serviceUrl,urlencode($url))); } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => sprintf('SimpleUrl (%s)', $this->shortenerName), + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:SimpleUrl', + 'rawdescription' => + sprintf(_m('Uses <a href="http://%1$s/">%1$s</a> URL-shortener service.'), + $this->shortenerName)); + + return true; + } } diff --git a/plugins/TemplatePlugin.php b/plugins/TemplatePlugin.php index 18aa8034c..80625c5b7 100644 --- a/plugins/TemplatePlugin.php +++ b/plugins/TemplatePlugin.php @@ -22,13 +22,13 @@ if (!defined('STATUSNET')) { define('TEMPLATEPLUGIN_VERSION', '0.1'); class TemplatePlugin extends Plugin { - + var $blocks = array(); - + function __construct() { parent::__construct(); } - + // capture the RouterInitialized event // and connect a new API method // for updating the template @@ -37,8 +37,7 @@ class TemplatePlugin extends Plugin { 'action' => 'template', )); } - - + // <%styles%> // <%scripts%> // <%search%> @@ -50,18 +49,18 @@ class TemplatePlugin extends Plugin { $act->extraHead(); $this->blocks['head'] = $act->xw->flush(); $act->showStylesheets(); - $this->blocks['styles'] = $act->xw->flush(); + $this->blocks['styles'] = $act->xw->flush(); $act->showScripts(); - $this->blocks['scripts'] = $act->xw->flush(); + $this->blocks['scripts'] = $act->xw->flush(); $act->showFeeds(); - $this->blocks['feeds'] = $act->xw->flush(); + $this->blocks['feeds'] = $act->xw->flush(); $act->showOpenSearch(); - $this->blocks['search'] = $act->xw->flush(); + $this->blocks['search'] = $act->xw->flush(); $act->showDescription(); $this->blocks['description'] = $act->xw->flush(); return false; } - + // <%bodytext%> function onStartShowContentBlock( &$act ) { $this->clear_xmlWriter($act); @@ -70,7 +69,7 @@ class TemplatePlugin extends Plugin { function onEndShowContentBlock( &$act ) { $this->blocks['bodytext'] = $act->xw->flush(); } - + // <%localnav%> function onStartShowLocalNavBlock( &$act ) { $this->clear_xmlWriter($act); @@ -79,7 +78,7 @@ class TemplatePlugin extends Plugin { function onEndShowLocalNavBlock( &$act ) { $this->blocks['localnav'] = $act->xw->flush(); } - + // <%export%> function onStartShowExportData( &$act ) { $this->clear_xmlWriter($act); @@ -88,7 +87,7 @@ class TemplatePlugin extends Plugin { function onEndShowExportData( &$act ) { $this->blocks['export'] = $act->xw->flush(); } - + // <%subscriptions%> // <%subscribers%> // <%groups%> @@ -149,7 +148,7 @@ class TemplatePlugin extends Plugin { } return false; } - + // <%logo%> // <%nav%> // <%notice%> @@ -170,7 +169,7 @@ class TemplatePlugin extends Plugin { $this->blocks['noticeform'] = $act->xw->flush(); return false; } - + // <%secondarynav%> // <%licenses%> function onStartShowFooter( &$act ) { @@ -181,19 +180,19 @@ class TemplatePlugin extends Plugin { $this->blocks['licenses'] = $act->xw->flush(); return false; } - + // capture the EndHTML event // and include the template function onEndEndHTML($act) { - + global $action, $tags; - + // set the action and title values $vars = array( 'action'=>$action, 'title'=>$act->title(). " - ". common_config('site', 'name') ); - + // use the PHP template // unless statusnet config: // $config['template']['mode'] = 'html'; @@ -203,55 +202,55 @@ class TemplatePlugin extends Plugin { include $tpl_file; return; } - + $tpl_file = $this->templateFolder() . '/index.html'; - + // read the static template $output = file_get_contents( $tpl_file ); - + $tags = array(); - + // get a list of the <%tags%> in the template $pattern='/<%([a-z]+)%>/'; - + if ( 1 <= preg_match_all( $pattern, $output, $found )) $tags[] = $found; - + // for each found tag, set its value from the rendered blocks foreach( $tags[0][1] as $pos=>$tag ) { if (isset($this->blocks[$tag])) $vars[$tag] = $this->blocks[$tag]; - + // didn't find a block for the tag elseif (!isset($vars[$tag])) $vars[$tag] = ''; } - + // replace the tags in the template foreach( $vars as $key=>$val ) $output = str_replace( '<%'.$key.'%>', $val, $output ); - + echo $output; - + return true; - + } function templateFolder() { return 'tpl'; } - + // catching the StartShowHTML event to halt the rendering function onStartShowHTML( &$act ) { $this->clear_xmlWriter($act); return true; } - + // clear the xmlWriter function clear_xmlWriter( &$act ) { $act->xw->openMemory(); $act->xw->setIndent(true); } - + } /** @@ -267,7 +266,7 @@ class TemplatePlugin extends Plugin { * @link http://megapump.com/ * */ - + class TemplateAction extends Action { @@ -275,54 +274,65 @@ class TemplateAction extends Action parent::prepare($args); return true; } - + function handle($args) { - + parent::handle($args); - + if (!isset($_SERVER['PHP_AUTH_USER'])) { - + // not authenticated, show login form header('WWW-Authenticate: Basic realm="StatusNet API"'); - + // cancelled the browser login form $this->clientError(_('Authentication error!'), $code = 401); - + } else { - + $nick = $_SERVER['PHP_AUTH_USER']; $pass = $_SERVER['PHP_AUTH_PW']; - + // check username and password $user = common_check_user($nick,$pass); - + if ($user) { - + // verify that user is admin if (!($user->id == 1)) $this->clientError(_('Only User #1 can update the template.'), $code = 401); - + // open the old template $tpl_file = $this->templateFolder() . '/index.html'; $fp = fopen( $tpl_file, 'w+' ); - + // overwrite with the new template fwrite($fp, $this->arg('template')); fclose($fp); - + header('HTTP/1.1 200 OK'); header('Content-type: text/plain'); print "Template Updated!"; - + } else { - + // bad username and password $this->clientError(_('Authentication error!'), $code = 401); - + } - + } } + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Template', + 'version' => TEMPLATEPLUGIN_VERSION, + 'author' => 'Brian Hendrickson', + 'homepage' => 'http://status.net/wiki/Plugin:Template', + 'rawdescription' => + _m('Use an HTML template for Web output.')); + return true; + } + } /** diff --git a/plugins/TightUrl/TightUrlPlugin.php b/plugins/TightUrl/TightUrlPlugin.php index 6ced9afdc..e2d494a7b 100644 --- a/plugins/TightUrl/TightUrlPlugin.php +++ b/plugins/TightUrl/TightUrlPlugin.php @@ -57,4 +57,16 @@ class TightUrlPlugin extends UrlShortenerPlugin return strval($xml['href']); } } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => sprintf('TightUrl (%s)', $this->shortenerName), + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:TightUrl', + 'rawdescription' => + sprintf(_m('Uses <a href="http://%1$s/">%1$s</a> URL-shortener service.'), + $this->shortenerName)); + return true; + } } diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index de1181903..a87ee2894 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -31,6 +31,8 @@ if (!defined('STATUSNET')) { require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; +define('TWITTERBRIDGEPLUGIN_VERSION', '0.9'); + /** * Plugin for sending and importing Twitter statuses * @@ -189,4 +191,17 @@ class TwitterBridgePlugin extends Plugin return true; } + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'TwitterBridge', + 'version' => TWITTERBRIDGEPLUGIN_VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge', + 'rawdescription' => + _m('The Twitter "bridge" plugin allows you to integrate ' . + 'your StatusNet instance with ' . + '<a href="http://twitter.com/">Twitter</a>.')); + return true; + } + } diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index b4ca12be2..36732ce46 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -268,19 +268,7 @@ class TwitterStatusFetcher extends ParallelizingDaemon } - 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(); - - } + Inbox::insertNotice($flink->user_id, $notice->id); $notice->blowCaches(); diff --git a/plugins/UserFlag/UserFlagPlugin.php b/plugins/UserFlag/UserFlagPlugin.php index 602a5bfa8..a33869c19 100644 --- a/plugins/UserFlag/UserFlagPlugin.php +++ b/plugins/UserFlag/UserFlagPlugin.php @@ -102,20 +102,20 @@ class UserFlagPlugin extends Plugin function onAutoload($cls) { - switch ($cls) + switch (strtolower($cls)) { - case 'FlagprofileAction': - case 'AdminprofileflagAction': - case 'ClearflagAction': + case 'flagprofileaction': + case 'adminprofileflagaction': + case 'clearflagaction': include_once INSTALLDIR.'/plugins/UserFlag/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'FlagProfileForm': - case 'ClearFlagForm': + case 'flagprofileform': + case 'clearflagform': include_once INSTALLDIR.'/plugins/UserFlag/' . strtolower($cls . '.php'); return false; - case 'User_flag_profile': - include_once INSTALLDIR.'/plugins/UserFlag/'.$cls.'.php'; + case 'user_flag_profile': + include_once INSTALLDIR.'/plugins/UserFlag/'.ucfirst(strtolower($cls)).'.php'; return false; default: return true; @@ -258,4 +258,39 @@ class UserFlagPlugin extends Plugin } return true; } + + /** + * Ensure that flag entries for a profile are deleted + * along with the profile when deleting users. + * This prevents breakage of the admin profile flag UI. + * + * @param Profile $profile + * @param array &$related list of related tables; entries + * with matching profile_id will be deleted. + * + * @return boolean hook result + */ + + function onProfileDeleteRelated($profile, &$related) + { + $related[] = 'user_flag_profile'; + return true; + } + + /** + * Ensure that flag entries created by a user are deleted + * when that user gets deleted. + * + * @param User $user + * @param array &$related list of related tables; entries + * with matching user_id will be deleted. + * + * @return boolean hook result + */ + + function onUserDeleteRelated($user, &$related) + { + $related[] = 'user_flag_profile'; + return true; + } } diff --git a/plugins/UserFlag/User_flag_profile.php b/plugins/UserFlag/User_flag_profile.php index 658259452..bc4251cf7 100644 --- a/plugins/UserFlag/User_flag_profile.php +++ b/plugins/UserFlag/User_flag_profile.php @@ -90,6 +90,17 @@ class User_flag_profile extends Memcached_DataObject } /** + * return key definitions for DB_DataObject + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + /** * Get a single object with multiple keys * * @param array $kv Map of key-value pairs @@ -97,7 +108,7 @@ class User_flag_profile extends Memcached_DataObject * @return User_flag_profile found object or null */ - function &pkeyGet($kv) + function pkeyGet($kv) { return Memcached_DataObject::pkeyGet('User_flag_profile', $kv); } diff --git a/plugins/WikiHashtagsPlugin.php b/plugins/WikiHashtagsPlugin.php index 334fc13ba..c6c976b8f 100644 --- a/plugins/WikiHashtagsPlugin.php +++ b/plugins/WikiHashtagsPlugin.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -define('WIKIHASHTAGSPLUGIN_VERSION', '0.1'); - /** * Plugin to use WikiHashtags * @@ -47,6 +45,8 @@ define('WIKIHASHTAGSPLUGIN_VERSION', '0.1'); class WikiHashtagsPlugin extends Plugin { + const VERSION = '0.1'; + function __construct($code=null) { parent::__construct(); @@ -99,4 +99,15 @@ class WikiHashtagsPlugin extends Plugin return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'WikiHashtags', + 'version' => self::VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:WikiHashtags', + 'rawdescription' => + _m('Gets hashtag descriptions from <a href="http://hashtags.wikia.com/">WikiHashtags</a>.')); + return true; + } } diff --git a/plugins/XCachePlugin.php b/plugins/XCachePlugin.php new file mode 100644 index 000000000..03cb0c06e --- /dev/null +++ b/plugins/XCachePlugin.php @@ -0,0 +1,113 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Plugin to implement cache interface for XCache variable cache + * + * PHP version 5 + * + * 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 Cache + * @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')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A plugin to use XCache's variable cache for the cache interface + * + * New plugin interface lets us use alternative cache systems + * for caching. This one uses XCache's variable cache. + * + * @category Cache + * @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/ + */ + +class XCachePlugin extends Plugin +{ + /** + * Get a value associated with a key + * + * The value should have been set previously. + * + * @param string &$key in; Lookup key + * @param mixed &$value out; value associated with key + * + * @return boolean hook success + */ + + function onStartCacheGet(&$key, &$value) + { + if (!xcache_isset($key)) { + $value = false; + } else { + $value = xcache_get($key); + $value = unserialize($value); + } + Event::handle('EndCacheGet', array($key, &$value)); + return false; + } + + /** + * Associate a value with a key + * + * @param string &$key in; Key to use for lookups + * @param mixed &$value in; Value to associate + * @param integer &$flag in; Flag (passed through to Memcache) + * @param integer &$expiry in; Expiry (passed through to Memcache) + * @param boolean &$success out; Whether the set was successful + * + * @return boolean hook success + */ + + function onStartCacheSet(&$key, &$value, &$flag, &$expiry, &$success) + { + $success = xcache_set($key, serialize($value)); + + Event::handle('EndCacheSet', array($key, $value, $flag, + $expiry)); + return false; + } + + /** + * Delete a value associated with a key + * + * @param string &$key in; Key to lookup + * @param boolean &$success out; whether it worked + * + * @return boolean hook success + */ + + function onStartCacheDelete(&$key, &$success) + { + $success = xcache_unset($key); + Event::handle('EndCacheDelete', array($key)); + return false; + } +} + |