summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/APCPlugin.php108
-rw-r--r--plugins/Authentication/AuthenticationPlugin.php226
-rw-r--r--plugins/Authentication/User_username.php61
-rw-r--r--plugins/Authorization/AuthorizationPlugin.php108
-rw-r--r--plugins/BitlyUrl/BitlyUrlPlugin.php14
-rw-r--r--plugins/Blacklist/BlacklistPlugin.php216
-rw-r--r--plugins/CacheLogPlugin.php121
-rw-r--r--plugins/CasAuthentication/CasAuthenticationPlugin.php11
-rw-r--r--plugins/CasAuthentication/README6
-rw-r--r--plugins/Facebook/FacebookPlugin.php15
-rw-r--r--plugins/Facebook/facebook/facebook.php23
-rw-r--r--plugins/Facebook/facebook/facebook_desktop.php2
-rw-r--r--plugins/Facebook/facebook/facebook_mobile.php260
-rwxr-xr-xplugins/Facebook/facebook/facebookapi_php5_restlib.php382
-rw-r--r--plugins/Facebook/facebookaction.php96
-rw-r--r--plugins/Facebook/facebooknoticeform.php206
-rw-r--r--plugins/Facebook/facebookutil.php16
-rw-r--r--plugins/FeedSub/FeedSubPlugin.php7
-rw-r--r--plugins/FeedSub/feedinfo.php108
-rw-r--r--plugins/GeoURLPlugin.php12
-rw-r--r--plugins/GeonamesPlugin.php12
-rw-r--r--plugins/GoogleAnalyticsPlugin.php12
-rw-r--r--plugins/Imap/ImapPlugin.php85
-rw-r--r--plugins/Imap/README32
-rwxr-xr-xplugins/Imap/imapdaemon.php147
-rw-r--r--plugins/LdapAuthentication/LdapAuthenticationPlugin.php3
-rw-r--r--plugins/LdapAuthorization/LdapAuthorizationPlugin.php2
-rw-r--r--plugins/LilUrl/LilUrlPlugin.php17
-rw-r--r--plugins/LinkbackPlugin.php14
-rw-r--r--plugins/Mapstraction/MapstractionPlugin.php15
-rw-r--r--plugins/MemcachePlugin.php191
-rw-r--r--plugins/Minify/MinifyPlugin.php2
-rw-r--r--plugins/Mollom/MollomPlugin.php893
-rw-r--r--plugins/Mollom/README22
-rw-r--r--plugins/OpenID/OpenIDPlugin.php17
-rw-r--r--plugins/OpenID/User_openid_trustroot.php2
-rw-r--r--plugins/PiwikAnalyticsPlugin.php12
-rw-r--r--plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php21
-rw-r--r--plugins/PtitUrl/PtitUrlPlugin.php13
-rw-r--r--plugins/RSSCloud/LoggingAggregator.php140
-rw-r--r--plugins/RSSCloud/README54
-rw-r--r--plugins/RSSCloud/RSSCloudNotifier.php240
-rw-r--r--plugins/RSSCloud/RSSCloudPlugin.php295
-rwxr-xr-xplugins/RSSCloud/RSSCloudQueueHandler.php78
-rw-r--r--plugins/RSSCloud/RSSCloudRequestNotify.php347
-rw-r--r--plugins/RSSCloud/RSSCloudSubscription.php79
-rw-r--r--plugins/Realtime/RealtimePlugin.php3
-rw-r--r--plugins/Realtime/json2.js478
-rw-r--r--plugins/Recaptcha/RecaptchaPlugin.php3
-rw-r--r--plugins/ReverseUsernameAuthentication/ReverseUsernameAuthenticationPlugin.php2
-rw-r--r--plugins/Sample/SamplePlugin.php260
-rw-r--r--plugins/Sample/User_greeting_count.php183
-rw-r--r--plugins/Sample/hello.php166
-rw-r--r--plugins/SimpleUrl/SimpleUrlPlugin.php13
-rw-r--r--plugins/TemplatePlugin.php112
-rw-r--r--plugins/TightUrl/TightUrlPlugin.php12
-rw-r--r--plugins/TwitterBridge/TwitterBridgePlugin.php15
-rw-r--r--plugins/UserFlag/UserFlagPlugin.php185
-rw-r--r--plugins/UserFlag/User_flag_profile.php91
-rw-r--r--plugins/UserFlag/adminprofileflag.php218
-rw-r--r--plugins/UserFlag/clearflag.php138
-rw-r--r--plugins/UserFlag/clearflagform.php92
-rw-r--r--plugins/UserFlag/flagprofile.php26
-rw-r--r--plugins/WikiHashtagsPlugin.php15
-rw-r--r--plugins/XCachePlugin.php113
65 files changed, 5591 insertions, 1277 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
new file mode 100644
index 000000000..84a2cb616
--- /dev/null
+++ b/plugins/Blacklist/BlacklistPlugin.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Plugin to prevent use of nicknames or URLs on a blacklist
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Plugin to prevent use of nicknames or URLs on a blacklist
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class BlacklistPlugin extends Plugin
+{
+ const VERSION = STATUSNET_VERSION;
+
+ public $nicknames = array();
+ public $urls = array();
+
+ /**
+ * Hook registration to prevent blacklisted homepages or nicknames
+ *
+ * Throws an exception if there's a blacklisted homepage or nickname.
+ *
+ * @param Action $action Action being called (usually register)
+ *
+ * @return boolean hook value
+ */
+
+ function onStartRegistrationTry($action)
+ {
+ $homepage = strtolower($action->trimmed('homepage'));
+
+ if (!empty($homepage)) {
+ if (!$this->_checkUrl($homepage)) {
+ $msg = sprintf(_m("You may not register with homepage '%s'"),
+ $homepage);
+ throw new ClientException($msg);
+ }
+ }
+
+ $nickname = strtolower($action->trimmed('nickname'));
+
+ if (!empty($nickname)) {
+ if (!$this->_checkNickname($nickname)) {
+ $msg = sprintf(_m("You may not register with nickname '%s'"),
+ $nickname);
+ throw new ClientException($msg);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook profile update to prevent blacklisted homepages or nicknames
+ *
+ * Throws an exception if there's a blacklisted homepage or nickname.
+ *
+ * @param Action $action Action being called (usually register)
+ *
+ * @return boolean hook value
+ */
+
+ function onStartProfileSaveForm($action)
+ {
+ $homepage = strtolower($action->trimmed('homepage'));
+
+ if (!empty($homepage)) {
+ if (!$this->_checkUrl($homepage)) {
+ $msg = sprintf(_m("You may not use homepage '%s'"),
+ $homepage);
+ throw new ClientException($msg);
+ }
+ }
+
+ $nickname = strtolower($action->trimmed('nickname'));
+
+ if (!empty($nickname)) {
+ if (!$this->_checkNickname($nickname)) {
+ $msg = sprintf(_m("You may not use nickname '%s'"),
+ $nickname);
+ throw new ClientException($msg);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook notice save to prevent blacklisted urls
+ *
+ * Throws an exception if there's a blacklisted url in the content.
+ *
+ * @param Notice &$notice Notice being saved
+ *
+ * @return boolean hook value
+ */
+
+ function onStartNoticeSave(&$notice)
+ {
+ common_replace_urls_callback($notice->content,
+ array($this, 'checkNoticeUrl'));
+ return true;
+ }
+
+ /**
+ * Helper callback for notice save
+ *
+ * Throws an exception if there's a blacklisted url in the content.
+ *
+ * @param string $url URL in the notice content
+ *
+ * @return boolean hook value
+ */
+
+ function checkNoticeUrl($url)
+ {
+ // It comes in special'd, so we unspecial it
+ // before comparing against patterns
+
+ $url = htmlspecialchars_decode($url);
+
+ if (!$this->_checkUrl($url)) {
+ $msg = sprintf(_m("You may not use url '%s' in notices"),
+ $url);
+ throw new ClientException($msg);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Helper for checking URLs
+ *
+ * Checks an URL against our patterns for a match.
+ *
+ * @param string $url URL to check
+ *
+ * @return boolean true means it's OK, false means it's bad
+ */
+
+ private function _checkUrl($url)
+ {
+ foreach ($this->urls as $pattern) {
+ if (preg_match("/$pattern/", $url)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper for checking nicknames
+ *
+ * Checks a nickname against our patterns for a match.
+ *
+ * @param string $nickname nickname to check
+ *
+ * @return boolean true means it's OK, false means it's bad
+ */
+
+ private function _checkNickname($nickname)
+ {
+ foreach ($this->nicknames as $pattern) {
+ if (preg_match("/$pattern/", $nickname)) {
+ return false;
+ }
+ }
+
+ 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/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..21e465b53 100644
--- a/plugins/Realtime/RealtimePlugin.php
+++ b/plugins/Realtime/RealtimePlugin.php
@@ -310,8 +310,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 '&nbsp;'),
- 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/UserFlag/UserFlagPlugin.php b/plugins/UserFlag/UserFlagPlugin.php
index 75dcca4fc..a33869c19 100644
--- a/plugins/UserFlag/UserFlagPlugin.php
+++ b/plugins/UserFlag/UserFlagPlugin.php
@@ -27,7 +27,7 @@
* @link http://status.net/
*/
-if (!defined('STATUSNET') && !defined('LACONICA')) {
+if (!defined('STATUSNET')) {
exit(1);
}
@@ -43,6 +43,20 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
class UserFlagPlugin extends Plugin
{
+ const REVIEWFLAGS = 'UserFlagPlugin::reviewflags';
+ const CLEARFLAGS = 'UserFlagPlugin::clearflags';
+
+ public $flagOnBlock = true;
+
+ /**
+ * Hook for ensuring our tables are created
+ *
+ * Ensures that the user_flag_profile table exists
+ * and has the right columns.
+ *
+ * @return boolean hook return
+ */
+
function onCheckSchema()
{
$schema = Schema::get();
@@ -62,37 +76,61 @@ class UserFlagPlugin extends Plugin
return true;
}
- function onInitializePlugin()
- {
- // XXX: do something here?
- return true;
- }
+ /**
+ * Add our actions to the URL router
+ *
+ * @param Net_URL_Mapper $m URL mapper for this hit
+ *
+ * @return boolean hook return
+ */
- function onRouterInitialized($m) {
+ function onRouterInitialized($m)
+ {
$m->connect('main/flag/profile', array('action' => 'flagprofile'));
+ $m->connect('main/flag/clear', array('action' => 'clearflag'));
$m->connect('admin/profile/flag', array('action' => 'adminprofileflag'));
return true;
}
- function onAutoload($cls)
+ /**
+ * Auto-load our classes if called
+ *
+ * @param string $cls Class to load
+ *
+ * @return boolean hook return
+ */
+
+ function onAutoload($cls)
{
- switch ($cls)
+ switch (strtolower($cls))
{
- case 'FlagprofileAction':
- case 'AdminprofileflagAction':
- require_once(INSTALLDIR.'/plugins/UserFlag/' . strtolower(mb_substr($cls, 0, -6)) . '.php');
+ case 'flagprofileaction':
+ case 'adminprofileflagaction':
+ case 'clearflagaction':
+ include_once INSTALLDIR.'/plugins/UserFlag/' .
+ strtolower(mb_substr($cls, 0, -6)) . '.php';
return false;
- case 'FlagProfileForm':
- require_once(INSTALLDIR.'/plugins/UserFlag/' . strtolower($cls . '.php'));
+ case 'flagprofileform':
+ case 'clearflagform':
+ include_once INSTALLDIR.'/plugins/UserFlag/' . strtolower($cls . '.php');
return false;
- case 'User_flag_profile':
- require_once(INSTALLDIR.'/plugins/UserFlag/'.$cls.'.php');
+ case 'user_flag_profile':
+ include_once INSTALLDIR.'/plugins/UserFlag/'.ucfirst(strtolower($cls)).'.php';
return false;
default:
return true;
}
}
+ /**
+ * Add a 'flag' button to profile page
+ *
+ * @param Action &$action The action being called
+ * @param Profile $profile Profile being shown
+ *
+ * @return boolean hook result
+ */
+
function onEndProfilePageActionsElements(&$action, $profile)
{
$user = common_current_user();
@@ -105,8 +143,8 @@ class UserFlagPlugin extends Plugin
$action->element('p', 'flagged', _('Flagged'));
} else {
$form = new FlagProfileForm($action, $profile,
- array('action' => 'showstream',
- 'nickname' => $profile->nickname));
+ array('action' => 'showstream',
+ 'nickname' => $profile->nickname));
$form->show();
}
@@ -116,6 +154,14 @@ class UserFlagPlugin extends Plugin
return true;
}
+ /**
+ * Add a 'flag' button to profiles in a list
+ *
+ * @param ProfileListItem $item item being shown
+ *
+ * @return boolean hook result
+ */
+
function onEndProfileListItemActionElements($item)
{
$user = common_current_user();
@@ -136,16 +182,115 @@ class UserFlagPlugin extends Plugin
return true;
}
+ /**
+ * Add our plugin's CSS to page output
+ *
+ * @param Action $action action being shown
+ *
+ * @return boolean hook result
+ */
+
function onEndShowStatusNetStyles($action)
{
- $action->cssLink(common_path('plugins/UserFlag/userflag.css'),
+ $action->cssLink(common_path('plugins/UserFlag/userflag.css'),
null, 'screen, projection, tv');
return true;
}
+ /**
+ * Initialize any flagging buttons on the page
+ *
+ * @param Action $action action being shown
+ *
+ * @return boolean hook result
+ */
+
function onEndShowScripts($action)
{
- $action->inlineScript('if ($(".form_entity_flag").length > 0) { SN.U.FormXHR($(".form_entity_flag")); }');
+ $action->inlineScript('if ($(".form_entity_flag").length > 0) { '.
+ 'SN.U.FormXHR($(".form_entity_flag")); '.
+ '}');
+ return true;
+ }
+
+ /**
+ * Check whether a user has one of our defined rights
+ *
+ * We define extra rights; this function checks to see if a
+ * user has one of them.
+ *
+ * @param User $user User being checked
+ * @param string $right Right we're checking
+ * @param boolean &$result out, result of the check
+ *
+ * @return boolean hook result
+ */
+
+ function onUserRightsCheck($user, $right, &$result)
+ {
+ switch ($right) {
+ case self::REVIEWFLAGS:
+ case self::CLEARFLAGS:
+ $result = $user->hasRole('moderator');
+ return false; // done processing!
+ }
+
+ return true; // unchanged!
+ }
+
+ /**
+ * Optionally flag profile when a block happens
+ *
+ * We optionally add a flag when a profile has been blocked
+ *
+ * @param User $user User doing the block
+ * @param Profile $profile Profile being blocked
+ *
+ * @return boolean hook result
+ */
+
+ function onEndBlockProfile($user, $profile)
+ {
+ if ($this->flagOnBlock && !User_flag_profile::exists($profile->id,
+ $user->id)) {
+
+ User_flag_profile::create($user->id, $profile->id);
+ }
+ 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 30bd4ae68..bc4251cf7 100644
--- a/plugins/UserFlag/User_flag_profile.php
+++ b/plugins/UserFlag/User_flag_profile.php
@@ -1,5 +1,15 @@
<?php
-/*
+/**
+ * Data class for profile flags
+ *
+ * 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.
*
@@ -23,6 +33,18 @@ if (!defined('STATUSNET')) {
require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+/**
+ * Data class for profile flags
+ *
+ * A class representing a user flagging another profile for review.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
class User_flag_profile extends Memcached_DataObject
{
###START_AUTOCODE
@@ -40,7 +62,14 @@ class User_flag_profile extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
- function table() {
+ /**
+ * return table definition for DB_DataObject
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
return array(
'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
@@ -49,15 +78,50 @@ class User_flag_profile extends Memcached_DataObject
);
}
- function keys() {
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * @return array key definitions
+ */
+
+ function keys()
+ {
return array('profile_id' => 'N', 'user_id' => 'N');
}
- function &pkeyGet($kv)
+ /**
+ * 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
+ *
+ * @return User_flag_profile found object or null
+ */
+
+ function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('User_flag_profile', $kv);
}
+ /**
+ * Check if a flag exists for given profile and user
+ *
+ * @param integer $profile_id Profile to check for
+ * @param integer $user_id User to check for
+ *
+ * @return boolean true if exists, else false
+ */
+
static function exists($profile_id, $user_id)
{
$ufp = User_flag_profile::pkeyGet(array('profile_id' => $profile_id,
@@ -65,4 +129,23 @@ class User_flag_profile extends Memcached_DataObject
return !empty($ufp);
}
+
+ static function create($user_id, $profile_id)
+ {
+ $ufp = new User_flag_profile();
+
+ $ufp->profile_id = $profile_id;
+ $ufp->user_id = $user_id;
+ $ufp->created = common_sql_now();
+
+ if (!$ufp->insert()) {
+ $msg = sprintf(_("Couldn't flag profile '%d' for review."),
+ $profile_id);
+ throw new ServerException($msg);
+ }
+
+ $ufp->free();
+
+ return true;
+ }
}
diff --git a/plugins/UserFlag/adminprofileflag.php b/plugins/UserFlag/adminprofileflag.php
index 20b808637..17374927b 100644
--- a/plugins/UserFlag/adminprofileflag.php
+++ b/plugins/UserFlag/adminprofileflag.php
@@ -43,6 +43,9 @@ if (!defined('STATUSNET')) {
class AdminprofileflagAction extends Action
{
+ var $page = null;
+ var $profiles = null;
+
/**
* Take arguments for running
*
@@ -55,6 +58,47 @@ class AdminprofileflagAction extends Action
{
parent::prepare($args);
+ $user = common_current_user();
+
+ // User must be logged in.
+
+ if (!common_logged_in()) {
+ $this->clientError(_('Not logged in.'));
+ return;
+ }
+
+ $user = common_current_user();
+
+ // ...because they're logged in
+
+ assert(!empty($user));
+
+ // It must be a "real" login, not saved cookie login
+
+ if (!common_is_real_login()) {
+ // Cookie theft is too easy; we require automatic
+ // logins to re-authenticate before admining the site
+ common_set_returnto($this->selfUrl());
+ if (Event::handle('RedirectToLogin', array($this, $user))) {
+ common_redirect(common_local_url('login'), 303);
+ }
+ }
+
+ // User must have the right to review flags
+
+ if (!$user->hasRight(UserFlagPlugin::REVIEWFLAGS)) {
+ $this->clientError(_('You cannot review profile flags.'));
+ return false;
+ }
+
+ $this->page = $this->trimmed('page');
+
+ if (empty($this->page)) {
+ $this->page = 1;
+ }
+
+ $this->profiles = $this->getProfiles();
+
return true;
}
@@ -73,7 +117,14 @@ class AdminprofileflagAction extends Action
$this->showPage();
}
- function title() {
+ /**
+ * Title of this page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
return _('Flagged profiles');
}
@@ -85,13 +136,20 @@ class AdminprofileflagAction extends Action
function showContent()
{
- $profile = $this->getProfiles();
+ $pl = new FlaggedProfileList($this->profiles, $this);
- $pl = new FlaggedProfileList($profile, $this);
+ $cnt = $pl->show();
- $pl->show();
+ $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE,
+ $this->page, 'adminprofileflag');
}
+ /**
+ * Retrieve this action's profiles
+ *
+ * @return Profile $profile Profile query results
+ */
+
function getProfiles()
{
$ufp = new User_flag_profile();
@@ -103,7 +161,12 @@ class AdminprofileflagAction extends Action
$ufp->whereAdd('cleared is NULL');
$ufp->groupBy('profile_id');
- $ufp->orderBy('flag_count DESC');
+ $ufp->orderBy('flag_count DESC, profile_id DESC');
+
+ $offset = ($this->page-1) * PROFILES_PER_PAGE;
+ $limit = PROFILES_PER_PAGE + 1;
+
+ $ufp->limit($offset, $limit);
$profiles = array();
@@ -122,7 +185,27 @@ class AdminprofileflagAction extends Action
}
}
-class FlaggedProfileList extends ProfileList {
+/**
+ * Specialization of ProfileList to show flagging information
+ *
+ * Most of the hard part is done in FlaggedProfileListItem.
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+class FlaggedProfileList extends ProfileList
+{
+ /**
+ * Factory method for creating new list items
+ *
+ * @param Profile $profile Profile to create an item for
+ *
+ * @return ProfileListItem newly-created item
+ */
function newListItem($profile)
{
@@ -130,11 +213,29 @@ class FlaggedProfileList extends ProfileList {
}
}
+/**
+ * Specialization of ProfileListItem to show flagging information
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
class FlaggedProfileListItem extends ProfileListItem
{
- var $user = null;
+ const MAX_FLAGGERS = 5;
+
+ var $user = null;
var $r2args = null;
+ /**
+ * Overload parent's action list with our own moderation-oriented buttons
+ *
+ * @return void
+ */
+
function showActions()
{
$this->user = common_current_user();
@@ -159,6 +260,12 @@ class FlaggedProfileListItem extends ProfileListItem
$this->endActions();
}
+ /**
+ * Show a button to sandbox the profile
+ *
+ * @return void
+ */
+
function showSandboxButton()
{
if ($this->user->hasRight(Right::SANDBOXUSER)) {
@@ -174,6 +281,12 @@ class FlaggedProfileListItem extends ProfileListItem
}
}
+ /**
+ * Show a button to silence the profile
+ *
+ * @return void
+ */
+
function showSilenceButton()
{
if ($this->user->hasRight(Right::SILENCEUSER)) {
@@ -189,6 +302,12 @@ class FlaggedProfileListItem extends ProfileListItem
}
}
+ /**
+ * Show a button to delete user and profile
+ *
+ * @return void
+ */
+
function showDeleteButton()
{
@@ -200,7 +319,92 @@ class FlaggedProfileListItem extends ProfileListItem
}
}
+ /**
+ * Show a button to clear flags
+ *
+ * @return void
+ */
+
function showClearButton()
{
+ if ($this->user->hasRight(UserFlagPlugin::CLEARFLAGS)) {
+ $this->out->elementStart('li', 'entity_clear');
+ $cf = new ClearFlagForm($this->out, $this->profile, $this->r2args);
+ $cf->show();
+ $this->out->elementEnd('li');
+ }
+ }
+
+ /**
+ * Overload parent function to add flaggers list
+ *
+ * @return void
+ */
+
+ function endProfile()
+ {
+ $this->showFlaggersList();
+ parent::endProfile();
+ }
+
+ /**
+ * Show a list of people who've flagged this profile
+ *
+ * @return void
+ */
+
+ function showFlaggersList()
+ {
+ $flaggers = array();
+
+ $ufp = new User_flag_profile();
+
+ $ufp->selectAdd();
+ $ufp->selectAdd('user_id');
+ $ufp->profile_id = $this->profile->id;
+ $ufp->orderBy('created');
+
+ if ($ufp->find()) { // XXX: this should always happen
+ while ($ufp->fetch()) {
+ $user = User::staticGet('id', $ufp->user_id);
+ if (!empty($user)) { // XXX: this would also be unusual
+ $flaggers[] = clone($user);
+ }
+ }
+ }
+
+ $cnt = count($flaggers);
+ $others = 0;
+
+ if ($cnt > self::MAX_FLAGGERS) {
+ $flaggers = array_slice($flaggers, 0, self::MAX_FLAGGERS);
+ $others = $cnt - self::MAX_FLAGGERS;
+ }
+
+ $lnks = array();
+
+ foreach ($flaggers as $flagger) {
+
+ $url = common_local_url('showstream',
+ array('nickname' => $flagger->nickname));
+
+ $lnks[] = XMLStringer::estring('a', array('href' => $url,
+ 'class' => 'flagger'),
+ $flagger->nickname);
+ }
+
+ if ($cnt > 0) {
+ $text = _('Flagged by ');
+
+ $text .= implode(', ', $lnks);
+
+ if ($others > 0) {
+ $text .= sprintf(_(' and %d others'), $others);
+ }
+
+ $this->out->elementStart('p', array('class' => 'flaggers'));
+ $this->out->raw($text);
+ $this->out->elementEnd('p');
+ }
}
}
diff --git a/plugins/UserFlag/clearflag.php b/plugins/UserFlag/clearflag.php
new file mode 100644
index 000000000..bd6732e2d
--- /dev/null
+++ b/plugins/UserFlag/clearflag.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Clear all flags for a profile
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Action to clear flags for a profile
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ClearflagAction extends ProfileFormAction
+{
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ if (!parent::prepare($args)) {
+ return false;
+ }
+
+ $user = common_current_user();
+
+ assert(!empty($user)); // checked above
+ assert(!empty($this->profile)); // checked above
+
+ return true;
+ }
+
+ /**
+ * Handle request
+ *
+ * Overriding the base Action's handle() here to deal check
+ * for Ajax and return an HXR response if necessary
+ *
+ * @param array $args $_REQUEST args; handled in prepare()
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost();
+ if (!$this->boolean('ajax')) {
+ $this->returnToArgs();
+ }
+ }
+ }
+
+ /**
+ * Handle POST
+ *
+ * Executes the actions; deletes all flags
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ $ufp = new User_flag_profile();
+
+ $result = $ufp->query('UPDATE user_flag_profile ' .
+ 'SET cleared = now() ' .
+ 'WHERE cleared is null ' .
+ 'AND profile_id = ' . $this->profile->id);
+
+ if ($result == false) {
+ $msg = sprintf(_("Couldn't clear flags for profile '%s'."),
+ $this->profile->nickname);
+ throw new ServerException($msg);
+ }
+
+ $ufp->free();
+
+ if ($this->boolean('ajax')) {
+ $this->ajaxResults();
+ }
+ }
+
+ /**
+ * Return results in ajax form
+ *
+ * @return void
+ */
+
+ function ajaxResults()
+ {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ $this->element('title', null, _('Flags cleared'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->element('p', 'cleared', _('Cleared'));
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ }
+}
diff --git a/plugins/UserFlag/clearflagform.php b/plugins/UserFlag/clearflagform.php
new file mode 100644
index 000000000..5ad6055d3
--- /dev/null
+++ b/plugins/UserFlag/clearflagform.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Form for clearing profile flags
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/form.php';
+
+/**
+ * Form for clearing profile flags
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ClearFlagForm extends ProfileActionForm
+{
+ /**
+ * class of the form
+ * Action this form provides
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_entity_clearflag';
+ }
+
+ /**
+ * Action this form provides
+ *
+ * @return string Name of the action, lowercased.
+ */
+
+ function target()
+ {
+ return 'clearflag';
+ }
+
+ /**
+ * Title of the form
+ *
+ * @return string Title of the form, internationalized
+ */
+
+ function title()
+ {
+ return _('Clear');
+ }
+
+ /**
+ * Description of the form
+ *
+ * @return string description of the form, internationalized
+ */
+
+ function description()
+ {
+ return _('Clear all flags');
+ }
+}
diff --git a/plugins/UserFlag/flagprofile.php b/plugins/UserFlag/flagprofile.php
index 9bce7865b..2d0f0abb9 100644
--- a/plugins/UserFlag/flagprofile.php
+++ b/plugins/UserFlag/flagprofile.php
@@ -63,8 +63,7 @@ class FlagprofileAction extends ProfileFormAction
assert(!empty($this->profile)); // checked above
if (User_flag_profile::exists($this->profile->id,
- $user->id))
- {
+ $user->id)) {
$this->clientError(_('Flag already exists.'));
return false;
}
@@ -72,7 +71,6 @@ class FlagprofileAction extends ProfileFormAction
return true;
}
-
/**
* Handle request
*
@@ -107,25 +105,23 @@ class FlagprofileAction extends ProfileFormAction
assert(!empty($user));
assert(!empty($this->profile));
- $ufp = new User_flag_profile();
-
- $ufp->profile_id = $this->profile->id;
- $ufp->user_id = $user->id;
- $ufp->created = common_sql_now();
+ // throws an exception on error
- if (!$ufp->insert()) {
- throw new ServerException(sprintf(_("Couldn't flag profile '%s' for review."),
- $this->profile->nickname));
- }
-
- $ufp->free();
+ User_flag_profile::create($user->id, $this->profile->id);
if ($this->boolean('ajax')) {
$this->ajaxResults();
}
}
- function ajaxResults() {
+ /**
+ * Return results as AJAX message
+ *
+ * @return void
+ */
+
+ function ajaxResults()
+ {
header('Content-Type: text/xml;charset=utf-8');
$this->xw->startDocument('1.0', 'UTF-8');
$this->elementStart('html');
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;
+ }
+}
+