summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/action.php6
-rw-r--r--lib/adminpanelaction.php46
-rw-r--r--lib/api.php4
-rw-r--r--lib/authenticationplugin.php231
-rw-r--r--lib/authorizationplugin.php105
-rw-r--r--lib/cache.php201
-rw-r--r--lib/columndef.php1
-rw-r--r--lib/command.php69
-rw-r--r--lib/common.php185
-rw-r--r--lib/dbqueuemanager.php161
-rw-r--r--lib/default.php32
-rw-r--r--lib/event.php8
-rw-r--r--lib/htmloutputter.php2
-rw-r--r--lib/iomanager.php193
-rw-r--r--lib/iomaster.php361
-rw-r--r--lib/jabber.php122
-rw-r--r--lib/jabberqueuehandler.php47
-rw-r--r--lib/language.php16
-rw-r--r--lib/liberalstomp.php133
-rw-r--r--lib/mail.php11
-rw-r--r--lib/mailhandler.php275
-rw-r--r--lib/mediafile.php12
-rw-r--r--lib/noticeform.php25
-rw-r--r--lib/noticelist.php2
-rw-r--r--lib/ombqueuehandler.php56
-rw-r--r--lib/ping.php4
-rw-r--r--lib/pingqueuehandler.php37
-rw-r--r--lib/plugin.php11
-rw-r--r--lib/pluginqueuehandler.php50
-rw-r--r--lib/publicqueuehandler.php47
-rw-r--r--lib/queuehandler.php100
-rw-r--r--lib/queuemanager.php149
-rw-r--r--lib/queuemonitor.php116
-rw-r--r--lib/repeatform.php4
-rw-r--r--lib/router.php18
-rw-r--r--lib/schema.php8
-rw-r--r--lib/smsqueuehandler.php39
-rw-r--r--lib/statusnet.php298
-rw-r--r--lib/stompqueuemanager.php286
-rw-r--r--lib/subs.php88
-rw-r--r--lib/subscriptionlist.php2
-rw-r--r--lib/unqueuemanager.php58
-rw-r--r--lib/util.php93
-rw-r--r--lib/xmppconfirmmanager.php168
-rw-r--r--lib/xmppmanager.php273
-rw-r--r--lib/xmppqueuehandler.php142
46 files changed, 3502 insertions, 793 deletions
diff --git a/lib/action.php b/lib/action.php
index dac0e2583..a521bcb50 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -141,7 +141,7 @@ class Action extends HTMLOutputter // lawsuit
function showTitle()
{
$this->element('title', null,
- sprintf(_("%s - %s"),
+ sprintf(_("%1\$s - %2\$s"),
$this->title(),
common_config('site', 'name')));
}
@@ -252,6 +252,8 @@ class Action extends HTMLOutputter // lawsuit
if (Event::handle('StartShowJQueryScripts', array($this))) {
$this->script('js/jquery.min.js');
$this->script('js/jquery.form.js');
+ $this->script('js/jquery.cookie.js');
+ $this->script('js/json2.js');
$this->script('js/jquery.joverlay.min.js');
Event::handle('EndShowJQueryScripts', array($this));
}
@@ -735,6 +737,8 @@ class Action extends HTMLOutputter // lawsuit
_('Privacy'));
$this->menuItem(common_local_url('doc', array('title' => 'source')),
_('Source'));
+ $this->menuItem(common_local_url('version'),
+ _('Version'));
$this->menuItem(common_local_url('doc', array('title' => 'contact')),
_('Contact'));
$this->menuItem(common_local_url('doc', array('title' => 'badge')),
diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php
index 7997eb2b1..a6981ac61 100644
--- a/lib/adminpanelaction.php
+++ b/lib/adminpanelaction.php
@@ -70,7 +70,7 @@ class AdminPanelAction extends Action
if (!common_logged_in()) {
$this->clientError(_('Not logged in.'));
- return;
+ return false;
}
$user = common_current_user();
@@ -94,7 +94,18 @@ class AdminPanelAction extends Action
if (!$user->hasRight(Right::CONFIGURESITE)) {
$this->clientError(_('You cannot make changes to this site.'));
- return;
+ return false;
+ }
+
+ // This panel must be enabled
+
+ $name = $this->trimmed('action');
+
+ $name = mb_substr($name, 0, -10);
+
+ if (!in_array($name, common_config('admin', 'panels'))) {
+ $this->clientError(_('Changes to that panel are not allowed.'), 403);
+ return false;
}
return true;
@@ -224,7 +235,7 @@ class AdminPanelAction extends Action
$this->clientError(_('saveSettings() not implemented.'));
return;
}
-
+
/**
* Delete a design setting
*
@@ -296,20 +307,33 @@ class AdminPanelNav extends Widget
if (Event::handle('StartAdminPanelNav', array($this))) {
- $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'),
- _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel');
+ if ($this->canAdmin('site')) {
+ $this->out->menuItem(common_local_url('siteadminpanel'), _('Site'),
+ _('Basic site configuration'), $action_name == 'siteadminpanel', 'nav_site_admin_panel');
+ }
- $this->out->menuItem(common_local_url('designadminpanel'), _('Design'),
- _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel');
+ if ($this->canAdmin('design')) {
+ $this->out->menuItem(common_local_url('designadminpanel'), _('Design'),
+ _('Design configuration'), $action_name == 'designadminpanel', 'nav_design_admin_panel');
+ }
- $this->out->menuItem(common_local_url('useradminpanel'), _('User'),
- _('Paths configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel');
+ if ($this->canAdmin('user')) {
+ $this->out->menuItem(common_local_url('useradminpanel'), _('User'),
+ _('Paths configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel');
+ }
- $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
- _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
+ if ($this->canAdmin('paths')) {
+ $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
+ _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
+ }
Event::handle('EndAdminPanelNav', array($this));
}
$this->action->elementEnd('ul');
}
+
+ function canAdmin($name)
+ {
+ return in_array($name, common_config('admin', 'panels'));
+ }
}
diff --git a/lib/api.php b/lib/api.php
index 06d7c079d..707e4ac21 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -168,7 +168,7 @@ class ApiAction extends Action
$timezone = 'UTC';
- if ($user->timezone) {
+ if (!empty($user) && $user->timezone) {
$timezone = $user->timezone;
}
@@ -789,7 +789,7 @@ class ApiAction extends Action
$from = $message->getFrom();
- $entry['title'] = sprintf('Message from %s to %s',
+ $entry['title'] = sprintf('Message from %1$s to %2$s',
$from->nickname, $message->getTo()->nickname);
$entry['content'] = common_xml_safe_str($message->rendered);
diff --git a/lib/authenticationplugin.php b/lib/authenticationplugin.php
new file mode 100644
index 000000000..de479a576
--- /dev/null
+++ b/lib/authenticationplugin.php
@@ -0,0 +1,231 @@
+<?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");
+ }
+ }
+
+ /**
+ * Internal AutoRegister event handler
+ * @param nickname
+ * @param provider_name
+ * @param user - the newly registered user
+ */
+ function onAutoRegister($nickname, $provider_name, &$user)
+ {
+ if($provider_name == $this->provider_name && $this->autoregistration){
+ $user = $this->autoregister($nickname);
+ if($user){
+ User_username::register($user,$nickname,$this->provider_name);
+ return false;
+ }
+ }
+ }
+
+ 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{
+ $authenticated = $this->checkPassword($nickname, $password);
+ if($authenticated){
+ if(! Event::handle('AutoRegister', array($nickname, $this->provider_name, &$authenticatedUser))){
+ //unlike most Event::handle lines of code, this one has a ! (not)
+ //we want to do this if the event *was* handled - this isn't a "default" implementation
+ //like most code of this form.
+ if($authenticatedUser){
+ 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 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/lib/authorizationplugin.php b/lib/authorizationplugin.php
new file mode 100644
index 000000000..733b0c065
--- /dev/null
+++ b/lib/authorizationplugin.php
@@ -0,0 +1,105 @@
+<?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 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/lib/cache.php b/lib/cache.php
new file mode 100644
index 000000000..635c96ad4
--- /dev/null
+++ b/lib/cache.php
@@ -0,0 +1,201 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Cache interface plus default in-memory cache implementation
+ *
+ * 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 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/
+ */
+
+/**
+ * Interface for caching
+ *
+ * An abstract interface for caching. Because we originally used the
+ * Memcache plugin directly, the interface uses a small subset of the
+ * Memcache interface.
+ *
+ * @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 Cache
+{
+ var $_items = array();
+ static $_inst = null;
+
+ /**
+ * Singleton constructor
+ *
+ * Use this to get the singleton instance of Cache.
+ *
+ * @return Cache cache object
+ */
+
+ static function instance()
+ {
+ if (is_null(self::$_inst)) {
+ self::$_inst = new Cache();
+ }
+
+ return self::$_inst;
+ }
+
+ /**
+ * Create a cache key from input text
+ *
+ * Builds a cache key from input text. Helps to namespace
+ * the cache area (if shared with other applications or sites)
+ * and prevent conflicts.
+ *
+ * @param string $extra the real part of the key
+ *
+ * @return string full key
+ */
+
+ static function key($extra)
+ {
+ $base_key = common_config('cache', 'base');
+
+ if (empty($base_key)) {
+ $base_key = common_keyize(common_config('site', 'name'));
+ }
+
+ return 'statusnet:' . $base_key . ':' . $extra;
+ }
+
+ /**
+ * Make a string suitable for use as a key
+ *
+ * Useful for turning primary keys of tables into cache keys.
+ *
+ * @param string $str string to turn into a key
+ *
+ * @return string keyized string
+ */
+
+ static function keyize($str)
+ {
+ $str = strtolower($str);
+ $str = preg_replace('/\s/', '_', $str);
+ return $str;
+ }
+
+ /**
+ * Get a value associated with a key
+ *
+ * The value should have been set previously.
+ *
+ * @param string $key Lookup key
+ *
+ * @return string retrieved value or null if unfound
+ */
+
+ function get($key)
+ {
+ $value = false;
+
+ if (Event::handle('StartCacheGet', array(&$key, &$value))) {
+ if (array_key_exists($key, $this->_items)) {
+ $value = unserialize($this->_items[$key]);
+ }
+ Event::handle('EndCacheGet', array($key, &$value));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Set the value associated with a key
+ *
+ * @param string $key The key to use for lookups
+ * @param string $value The value to store
+ * @param integer $flag Flags to use, mostly ignored
+ * @param integer $expiry Expiry value, mostly ignored
+ *
+ * @return boolean success flag
+ */
+
+ function set($key, $value, $flag=null, $expiry=null)
+ {
+ $success = false;
+
+ if (Event::handle('StartCacheSet', array(&$key, &$value, &$flag,
+ &$expiry, &$success))) {
+
+ $this->_items[$key] = serialize($value);
+
+ $success = true;
+
+ Event::handle('EndCacheSet', array($key, $value, $flag,
+ $expiry));
+ }
+
+ return $success;
+ }
+
+ /**
+ * Delete the value associated with a key
+ *
+ * @param string $key Key to delete
+ *
+ * @return boolean success flag
+ */
+
+ function delete($key)
+ {
+ $success = false;
+
+ if (Event::handle('StartCacheDelete', array(&$key, &$success))) {
+ if (array_key_exists($key, $this->_items)) {
+ unset($this->_items[$key]);
+ }
+ $success = true;
+ Event::handle('EndCacheDelete', array($key));
+ }
+
+ return $success;
+ }
+
+ /**
+ * Close or reconnect any remote connections, such as to give
+ * daemon processes a chance to reconnect on a fresh socket.
+ *
+ * @return boolean success flag
+ */
+
+ function reconnect()
+ {
+ $success = false;
+
+ if (Event::handle('StartCacheReconnect', array(&$success))) {
+ $success = true;
+ Event::handle('EndCacheReconnect', array());
+ }
+
+ return $success;
+ }
+}
diff --git a/lib/columndef.php b/lib/columndef.php
index 1bae6b33b..ac2fcd23e 100644
--- a/lib/columndef.php
+++ b/lib/columndef.php
@@ -74,6 +74,7 @@ class ColumnDef
* @param string $key type of key
* @param value $default default value
* @param value $extra unused
+ * @param boolean $auto_increment
*/
function __construct($name=null, $type=null, $size=null,
diff --git a/lib/command.php b/lib/command.php
index 67140c348..c0a32e1b1 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -222,18 +222,15 @@ class JoinCommand extends Command
return;
}
- $member = new Group_member();
-
- $member->group_id = $group->id;
- $member->profile_id = $cur->id;
- $member->created = common_sql_now();
-
- $result = $member->insert();
- if (!$result) {
- common_log_db_error($member, 'INSERT', __FILE__);
- $channel->error($cur, sprintf(_('Could not join user %s to group %s'),
- $cur->nickname, $group->nickname));
- return;
+ try {
+ if (Event::handle('StartJoinGroup', array($group, $cur))) {
+ Group_member::join($group->id, $cur->id);
+ Event::handle('EndJoinGroup', array($group, $cur));
+ }
+ } catch (Exception $e) {
+ $channel->error($cur, sprintf(_('Could not join user %s to group %s'),
+ $cur->nickname, $group->nickname));
+ return;
}
$channel->output($cur, sprintf(_('%s joined group %s'),
@@ -269,21 +266,15 @@ class DropCommand extends Command
return;
}
- $member = new Group_member();
-
- $member->group_id = $group->id;
- $member->profile_id = $cur->id;
-
- if (!$member->find(true)) {
- $channel->error($cur,_('Could not find membership record.'));
- return;
- }
- $result = $member->delete();
- if (!$result) {
- common_log_db_error($member, 'INSERT', __FILE__);
- $channel->error($cur, sprintf(_('Could not remove user %s to group %s'),
- $cur->nickname, $group->nickname));
- return;
+ try {
+ if (Event::handle('StartLeaveGroup', array($group, $cur))) {
+ Group_member::leave($group->id, $cur->id);
+ Event::handle('EndLeaveGroup', array($group, $cur));
+ }
+ } catch (Exception $e) {
+ $channel->error($cur, sprintf(_('Could not remove user %s to group %s'),
+ $cur->nickname, $group->nickname));
+ return;
}
$channel->output($cur, sprintf(_('%s left group %s'),
@@ -650,25 +641,17 @@ class LoginCommand extends Command
$channel->error($this->user, _('Login command is disabled'));
return;
}
- $login_token = Login_token::staticGet('user_id',$this->user->id);
- if($login_token){
- $login_token->delete();
- }
- $login_token = new Login_token();
- $login_token->user_id = $this->user->id;
- $login_token->token = common_good_rand(16);
- $login_token->created = common_sql_now();
- $result = $login_token->insert();
- if (!$result) {
- common_log_db_error($login_token, 'INSERT', __FILE__);
- $channel->error($this->user, sprintf(_('Could not create login token for %s'),
- $this->user->nickname));
- return;
+
+ try {
+ $login_token = Login_token::makeNew($this->user);
+ } catch (Exception $e) {
+ $channel->error($this->user, $e->getMessage());
}
+
$channel->output($this->user,
sprintf(_('This link is useable only once, and is good for only 2 minutes: %s'),
- common_local_url('login',
- array('user_id'=>$login_token->user_id, 'token'=>$login_token->token))));
+ common_local_url('otp',
+ array('user_id' => $login_token->user_id, 'token' => $login_token->token))));
}
}
diff --git a/lib/common.php b/lib/common.php
index 7fa1910af..243ac3e79 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
-define('STATUSNET_VERSION', '0.9.0rc2');
+define('STATUSNET_VERSION', '0.9.0rc3');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
define('STATUSNET_CODENAME', 'Stand');
@@ -41,15 +41,30 @@ define('FOREIGN_NOTICE_SEND_REPLY', 4);
define('FOREIGN_FRIEND_SEND', 1);
define('FOREIGN_FRIEND_RECV', 2);
+define('NOTICE_INBOX_SOURCE_SUB', 1);
+define('NOTICE_INBOX_SOURCE_GROUP', 2);
+define('NOTICE_INBOX_SOURCE_REPLY', 3);
+define('NOTICE_INBOX_SOURCE_FORWARD', 4);
+define('NOTICE_INBOX_SOURCE_GATEWAY', -1);
+
# append our extlib dir as the last-resort place to find libs
set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/extlib/');
-# To protect against upstream libraries which haven't updated
-# for PHP 5.3 where dl() function may not be present...
+// To protect against upstream libraries which haven't updated
+// for PHP 5.3 where dl() function may not be present...
if (!function_exists('dl')) {
- function dl($library) {
- return false;
+ // function_exists() returns false for things in disable_functions,
+ // but they still exist and we'll die if we try to redefine them.
+ //
+ // Fortunately trying to call the disabled one will only trigger
+ // a warning, not a fatal, so it's safe to leave it for our case.
+ // Callers will be suppressing warnings anyway.
+ $disabled = array_filter(array_map('trim', explode(',', ini_get('disable_functions'))));
+ if (!in_array('dl', $disabled)) {
+ function dl($library) {
+ return false;
+ }
}
}
@@ -67,147 +82,14 @@ require_once(INSTALLDIR.'/lib/language.php');
require_once(INSTALLDIR.'/lib/event.php');
require_once(INSTALLDIR.'/lib/plugin.php');
-function _sn_to_path($sn)
-{
- $past_root = substr($sn, 1);
- $last_slash = strrpos($past_root, '/');
- if ($last_slash > 0) {
- $p = substr($past_root, 0, $last_slash);
- } else {
- $p = '';
- }
- return $p;
-}
-
-// Save our sanity when code gets loaded through subroutines such as PHPUnit tests
-global $default, $config, $_server, $_path;
-
-// try to figure out where we are. $server and $path
-// can be set by including module, else we guess based
-// on HTTP info.
-
-if (isset($server)) {
- $_server = $server;
-} else {
- $_server = array_key_exists('SERVER_NAME', $_SERVER) ?
- strtolower($_SERVER['SERVER_NAME']) :
- null;
-}
-
-if (isset($path)) {
- $_path = $path;
-} else {
- $_path = (array_key_exists('SERVER_NAME', $_SERVER) && array_key_exists('SCRIPT_NAME', $_SERVER)) ?
- _sn_to_path($_SERVER['SCRIPT_NAME']) :
- null;
-}
-
-require_once(INSTALLDIR.'/lib/default.php');
-
-// Set config values initially to default values
-
-$config = $default;
-
-// default configuration, overwritten in config.php
-
-$config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
-
-$config['db'] = $default['db'];
-
-// Backward compatibility
-
-$config['site']['design'] =& $config['design'];
-
-if (function_exists('date_default_timezone_set')) {
- /* Work internally in UTC */
- date_default_timezone_set('UTC');
-}
-
function addPlugin($name, $attrs = null)
{
- $name = ucfirst($name);
- $pluginclass = "{$name}Plugin";
-
- if (!class_exists($pluginclass)) {
-
- $files = array("local/plugins/{$pluginclass}.php",
- "local/plugins/{$name}/{$pluginclass}.php",
- "local/{$pluginclass}.php",
- "local/{$name}/{$pluginclass}.php",
- "plugins/{$pluginclass}.php",
- "plugins/{$name}/{$pluginclass}.php");
-
- foreach ($files as $file) {
- $fullpath = INSTALLDIR.'/'.$file;
- if (@file_exists($fullpath)) {
- include_once($fullpath);
- break;
- }
- }
- }
-
- $inst = new $pluginclass();
-
- if (!empty($attrs)) {
- foreach ($attrs as $aname => $avalue) {
- $inst->$aname = $avalue;
- }
- }
- return $inst;
-}
-
-// From most general to most specific:
-// server-wide, then vhost-wide, then for a path,
-// finally for a dir (usually only need one of the last two).
-
-if (isset($conffile)) {
- $_config_files = array($conffile);
-} else {
- $_config_files = array('/etc/statusnet/statusnet.php',
- '/etc/statusnet/laconica.php',
- '/etc/laconica/laconica.php',
- '/etc/statusnet/'.$_server.'.php',
- '/etc/laconica/'.$_server.'.php');
-
- if (strlen($_path) > 0) {
- $_config_files[] = '/etc/statusnet/'.$_server.'_'.$_path.'.php';
- $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
- }
-
- $_config_files[] = INSTALLDIR.'/config.php';
-}
-
-global $_have_a_config;
-$_have_a_config = false;
-
-foreach ($_config_files as $_config_file) {
- if (@file_exists($_config_file)) {
- include_once($_config_file);
- $_have_a_config = true;
- }
+ return StatusNet::addPlugin($name, $attrs);
}
function _have_config()
{
- global $_have_a_config;
- return $_have_a_config;
-}
-
-// XXX: Throw a conniption if database not installed
-// XXX: Find a way to use htmlwriter for this instead of handcoded markup
-if (!_have_config()) {
- echo '<p>'. _('No configuration file found. ') .'</p>';
- echo '<p>'. _('I looked for configuration files in the following places: ') .'<br/> '. implode($_config_files, '<br/>');
- echo '<p>'. _('You may wish to run the installer to fix this.') .'</p>';
- echo '<a href="install.php">'. _('Go to the installer.') .'</a>';
- exit;
-}
-// Fixup for statusnet.ini
-
-$_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1);
-
-if ($_db_name != 'statusnet' && !array_key_exists('ini_'.$_db_name, $config['db'])) {
- $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/statusnet.ini';
+ return StatusNet::haveConfig();
}
function __autoload($cls)
@@ -241,20 +123,19 @@ require_once INSTALLDIR.'/lib/subs.php';
require_once INSTALLDIR.'/lib/clientexception.php';
require_once INSTALLDIR.'/lib/serverexception.php';
-// Load settings from database; note we need autoload for this
-
-Config::loadSettings();
-
-// XXX: if plugins should check the schema at runtime, do that here.
-
-if ($config['db']['schemacheck'] == 'runtime') {
- Event::handle('CheckSchema');
+try {
+ StatusNet::init(@$server, @$path, @$conffile);
+} catch (NoConfigException $e) {
+ // XXX: Throw a conniption if database not installed
+ // XXX: Find a way to use htmlwriter for this instead of handcoded markup
+ echo '<p>'. _('No configuration file found. ') .'</p>';
+ echo '<p>'. _('I looked for configuration files in the following places: ') .'<br/> ';
+ echo implode($e->configFiles, '<br/>');
+ echo '<p>'. _('You may wish to run the installer to fix this.') .'</p>';
+ echo '<a href="install.php">'. _('Go to the installer.') .'</a>';
+ exit;
}
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
-
-// Give plugins a chance to initialize in a fully-prepared environment
-
-Event::handle('InitializePlugin');
diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php
index 750300928..889365b64 100644
--- a/lib/dbqueuemanager.php
+++ b/lib/dbqueuemanager.php
@@ -22,16 +22,20 @@
* @category QueueManager
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
- * @copyright 2009 StatusNet, Inc.
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2009-2010 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 DBQueueManager extends QueueManager
{
- var $qis = array();
-
- function enqueue($object, $queue)
+ /**
+ * Saves a notice object reference into the queue item table.
+ * @return boolean true on success
+ * @throws ServerException on failure
+ */
+ public function enqueue($object, $queue)
{
$notice = $object;
@@ -47,70 +51,95 @@ class DBQueueManager extends QueueManager
throw new ServerException('DB error inserting queue item');
}
+ $this->stats('enqueued', $queue);
+
return true;
}
- function service($queue, $handler)
+ /**
+ * Poll every minute for new events during idle periods.
+ * We'll look in more often when there's data available.
+ *
+ * @return int seconds
+ */
+ public function pollInterval()
+ {
+ return 60;
+ }
+
+ /**
+ * Run a polling cycle during idle processing in the input loop.
+ * @return boolean true if we had a hit
+ */
+ public function poll()
{
- while (true) {
- $this->_log(LOG_DEBUG, 'Checking for notices...');
- $timeout = $handler->timeout();
- $notice = $this->_nextItem($queue, $timeout);
- if (empty($notice)) {
- $this->_log(LOG_DEBUG, 'No notices waiting; idling.');
- // Nothing in the queue. Do you
- // have other tasks, like servicing your
- // XMPP connection, to do?
- $handler->idle(QUEUE_HANDLER_MISS_IDLE);
+ $this->_log(LOG_DEBUG, 'Checking for notices...');
+ $item = $this->_nextItem();
+ if ($item === false) {
+ $this->_log(LOG_DEBUG, 'No notices waiting; idling.');
+ return false;
+ }
+ if ($item === true) {
+ // We dequeued an entry for a deleted or invalid notice.
+ // Consider it a hit for poll rate purposes.
+ return true;
+ }
+
+ list($queue, $notice) = $item;
+ $this->_log(LOG_INFO, 'Got notice '. $notice->id . ' for transport ' . $queue);
+
+ // Yay! Got one!
+ $handler = $this->getHandler($queue);
+ if ($handler) {
+ if ($handler->handle_notice($notice)) {
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] Successfully handled notice");
+ $this->_done($notice, $queue);
} else {
- $this->_log(LOG_INFO, 'Got notice '. $notice->id);
- // Yay! Got one!
- if ($handler->handle_notice($notice)) {
- $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id);
- $this->_done($notice, $queue);
- } else {
- $this->_log(LOG_INFO, 'Failed to handle notice '. $notice->id);
- $this->_fail($notice, $queue);
- }
- // Chance to e.g. service your XMPP connection
- $this->_log(LOG_DEBUG, 'Idling after success.');
- $handler->idle(QUEUE_HANDLER_HIT_IDLE);
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] Failed to handle notice");
+ $this->_fail($notice, $queue);
}
- // XXX: when do we give up?
+ } else {
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] No handler for queue $queue; discarding.");
+ $this->_done($notice, $queue);
}
+ return true;
}
- function _nextItem($queue, $timeout=null)
+ /**
+ * Pop the oldest unclaimed item off the queue set and claim it.
+ *
+ * @return mixed false if no items; true if bogus hit; otherwise array(string, Notice)
+ * giving the queue transport name.
+ */
+ protected function _nextItem()
{
$start = time();
$result = null;
- $sleeptime = 1;
+ $qi = Queue_item::top();
+ if (empty($qi)) {
+ return false;
+ }
- do {
- $qi = Queue_item::top($queue);
- if (empty($qi)) {
- $this->_log(LOG_DEBUG, "No new queue items, sleeping $sleeptime seconds.");
- sleep($sleeptime);
- $sleeptime *= 2;
- } else {
- $notice = Notice::staticGet('id', $qi->notice_id);
- if (!empty($notice)) {
- $result = $notice;
- } else {
- $this->_log(LOG_INFO, 'dequeued non-existent notice ' . $notice->id);
- $qi->delete();
- $qi->free();
- $qi = null;
- }
- $sleeptime = 1;
- }
- } while (empty($result) && (is_null($timeout) || (time() - $start) < $timeout));
+ $queue = $qi->transport;
+ $notice = Notice::staticGet('id', $qi->notice_id);
+ if (empty($notice)) {
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] dequeued non-existent notice");
+ $qi->delete();
+ return true;
+ }
- return $result;
+ $result = $notice;
+ return array($queue, $notice);
}
- function _done($object, $queue)
+ /**
+ * Delete our claimed item from the queue after successful processing.
+ *
+ * @param Notice $object
+ * @param string $queue
+ */
+ protected function _done($object, $queue)
{
// XXX: right now, we only handle notices
@@ -120,24 +149,29 @@ class DBQueueManager extends QueueManager
'transport' => $queue));
if (empty($qi)) {
- $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] Cannot find queue item");
} else {
if (empty($qi->claimed)) {
- $this->_log(LOG_WARNING, 'Reluctantly releasing unclaimed queue item '.
- 'for '.$notice->id.', queue '.$queue);
+ $this->_log(LOG_WARNING, "[$queue:notice $notice->id] Reluctantly releasing unclaimed queue item");
}
$qi->delete();
$qi->free();
- $qi = null;
}
- $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] done with item");
+ $this->stats('handled', $queue);
$notice->free();
- $notice = null;
}
- function _fail($object, $queue)
+ /**
+ * Free our claimed queue item for later reprocessing in case of
+ * temporary failure.
+ *
+ * @param Notice $object
+ * @param string $queue
+ */
+ protected function _fail($object, $queue)
{
// XXX: right now, we only handle notices
@@ -147,11 +181,10 @@ class DBQueueManager extends QueueManager
'transport' => $queue));
if (empty($qi)) {
- $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue);
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] Cannot find queue item");
} else {
if (empty($qi->claimed)) {
- $this->_log(LOG_WARNING, 'Ignoring failure for unclaimed queue item '.
- 'for '.$notice->id.', queue '.$queue);
+ $this->_log(LOG_WARNING, "[$queue:notice $notice->id] Ignoring failure for unclaimed queue item");
} else {
$orig = clone($qi);
$qi->claimed = null;
@@ -160,13 +193,13 @@ class DBQueueManager extends QueueManager
}
}
- $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id);
+ $this->_log(LOG_INFO, "[$queue:notice $notice->id] done with queue item");
+ $this->stats('error', $queue);
$notice->free();
- $notice = null;
}
- function _log($level, $msg)
+ protected function _log($level, $msg)
{
common_log($level, 'DBQueueManager: '.$msg);
}
diff --git a/lib/default.php b/lib/default.php
index 42d4623b1..f7f4777a2 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -54,6 +54,7 @@ $default =
'dupelimit' => 60, # default for same person saying the same thing
'textlimit' => 140,
'indent' => true,
+ 'use_x_sendfile' => false,
),
'db' =>
array('database' => 'YOU HAVE TO SET THIS IN config.php',
@@ -78,6 +79,8 @@ $default =
'queue_basename' => '/queue/statusnet/',
'stomp_username' => null,
'stomp_password' => null,
+ 'monitor' => null, // URL to monitor ping endpoint (work in progress)
+ 'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully
),
'license' =>
array('url' => 'http://creativecommons.org/licenses/by/3.0/',
@@ -147,11 +150,8 @@ $default =
array('enabled' => true,
'consumer_key' => null,
'consumer_secret' => null),
- 'memcached' =>
- array('enabled' => false,
- 'server' => 'localhost',
- 'base' => null,
- 'port' => 11211),
+ 'cache' =>
+ array('base' => null),
'ping' =>
array('notify' => array()),
'inboxes' =>
@@ -226,9 +226,29 @@ $default =
'message' =>
array('contentlimit' => null),
'location' =>
- array('namespace' => 1), // 1 = geonames, 2 = Yahoo Where on Earth
+ array('share' => 'user', // whether to share location; 'always', 'user', 'never'
+ 'sharedefault' => true),
'omb' =>
array('timeout' => 5), // HTTP request timeout in seconds when contacting remote hosts for OMB updates
'logincommand' =>
array('disabled' => true),
+ 'plugins' =>
+ array('default' => array('LilUrl' => array('shortenerName'=>'ur1.ca',
+ 'freeService' => true,
+ 'serviceUrl'=>'http://ur1.ca/'),
+ 'PtitUrl' => array('shortenerName' => 'ptiturl.com',
+ 'serviceUrl' => 'http://ptiturl.com/?creer=oui&action=Reduire&url=%1$s'),
+ 'SimpleUrl' => array(array('shortenerName' => 'is.gd', 'serviceUrl' => 'http://is.gd/api.php?longurl=%1$s'),
+ array('shortenerName' => 'snipr.com', 'serviceUrl' => 'http://snipr.com/site/snip?r=simple&link=%1$s'),
+ array('shortenerName' => 'metamark.net', 'serviceUrl' => 'http://metamark.net/api/rest/simple?long_url=%1$s'),
+ array('shortenerName' => 'tinyurl.com', 'serviceUrl' => 'http://tinyurl.com/api-create.php?url=%1$s')),
+ 'TightUrl' => array('shortenerName' => '2tu.us', 'freeService' => true,'serviceUrl'=>'http://2tu.us/?save=y&url=%1$s'),
+ 'Geonames' => null,
+ 'Mapstraction' => null,
+ 'Linkback' => null,
+ 'WikiHashtags' => null,
+ 'OpenID' => null),
+ ),
+ 'admin' =>
+ array('panels' => array('design', 'site', 'user', 'paths')),
);
diff --git a/lib/event.php b/lib/event.php
index 4819b71b4..41fb53ffe 100644
--- a/lib/event.php
+++ b/lib/event.php
@@ -138,4 +138,12 @@ class Event {
}
return false;
}
+
+ /**
+ * Disables any and all handlers that have been set up so far;
+ * use only if you know it's safe to reinitialize all plugins.
+ */
+ public static function clearHandlers() {
+ Event::$_handlers = array();
+ }
}
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index 2091c6e2c..31660ce95 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -352,7 +352,7 @@ class HTMLOutputter extends XMLOutputter
{
if(Event::handle('StartScriptElement', array($this,&$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 = common_path($src) . '?version=' . STATUSNET_VERSION;
}
diff --git a/lib/iomanager.php b/lib/iomanager.php
new file mode 100644
index 000000000..ee2ff958b
--- /dev/null
+++ b/lib/iomanager.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Abstract class for i/o managers
+ *
+ * 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 QueueManager
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @author Sarven Capadisli <csarven@status.net>
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2009-2010 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/
+ */
+
+abstract class IoManager
+{
+ const SINGLE_ONLY = 0;
+ const INSTANCE_PER_SITE = 1;
+ const INSTANCE_PER_PROCESS = 2;
+
+ /**
+ * Factory function to get an appropriate subclass.
+ */
+ public abstract static function get();
+
+ /**
+ * Tell the i/o queue master if and how we can handle multi-site
+ * processes.
+ *
+ * Return one of:
+ * IoManager::SINGLE_ONLY
+ * IoManager::INSTANCE_PER_SITE
+ * IoManager::INSTANCE_PER_PROCESS
+ */
+ public static function multiSite()
+ {
+ return IoManager::SINGLE_ONLY;
+ }
+
+ /**
+ * If in a multisite configuration, the i/o master will tell
+ * your manager about each site you'll have to handle so you
+ * can do any necessary per-site setup.
+ *
+ * @param string $site target site server name
+ */
+ public function addSite($site)
+ {
+ /* no-op */
+ }
+
+ /**
+ * This method is called when data is available on one of your
+ * i/o manager's sockets. The socket with data is passed in,
+ * in case you have multiple sockets.
+ *
+ * If your i/o manager is based on polling during idle processing,
+ * you don't need to implement this.
+ *
+ * @param resource $socket
+ * @return boolean true on success, false on failure
+ */
+ public function handleInput($socket)
+ {
+ return true;
+ }
+
+ /**
+ * Return any open sockets that the run loop should listen
+ * for input on. If input comes in on a listed socket,
+ * the matching manager's handleInput method will be called.
+ *
+ * @return array of resources
+ */
+ function getSockets()
+ {
+ return array();
+ }
+
+ /**
+ * Maximum planned time between poll() calls when input isn't waiting.
+ * Actual time may vary!
+ *
+ * When we get a polling hit, the timeout will be cut down to 0 while
+ * input is coming in, then will back off to this amount if no further
+ * input shows up.
+ *
+ * By default polling is disabled; you must override this to enable
+ * polling for this manager.
+ *
+ * @return int max poll interval in seconds, or 0 to disable polling
+ */
+ function pollInterval()
+ {
+ return 0;
+ }
+
+ /**
+ * Request a maximum timeout for listeners before the next idle period.
+ * Actual wait may be shorter, so don't go crazy in your idle()!
+ * Wait could be longer if other handlers performed some slow activity.
+ *
+ * Return 0 to request that listeners return immediately if there's no
+ * i/o and speed up the idle as much as possible; but don't do that all
+ * the time as this will burn CPU.
+ *
+ * @return int seconds
+ */
+ function timeout()
+ {
+ return 60;
+ }
+
+ /**
+ * Called by IoManager after each handled item or empty polling cycle.
+ * This is a good time to e.g. service your XMPP connection.
+ *
+ * Doesn't need to be overridden if there's no maintenance to do.
+ */
+ function idle()
+ {
+ return true;
+ }
+
+ /**
+ * The meat of a polling manager... check for something to do
+ * and do it! Note that you should not take too long, as other
+ * i/o managers may need to do some work too!
+ *
+ * On a successful hit, the next poll() call will come as soon
+ * as possible followed by exponential backoff up to pollInterval()
+ * if no more data is available.
+ *
+ * @return boolean true if events were hit
+ */
+ public function poll()
+ {
+ return false;
+ }
+
+ /**
+ * Initialization, run when the queue manager starts.
+ * If this function indicates failure, the handler run will be aborted.
+ *
+ * @param IoMaster $master process/event controller
+ * @return boolean true on success, false on failure
+ */
+ public function start($master)
+ {
+ $this->master = $master;
+ return true;
+ }
+
+ /**
+ * Cleanup, run when the queue manager ends.
+ * If this function indicates failure, a warning will be logged.
+ *
+ * @return boolean true on success, false on failure
+ */
+ public function finish()
+ {
+ return true;
+ }
+
+ /**
+ * Ping iomaster's queue status monitor with a stats update.
+ * Only valid during input loop!
+ *
+ * @param string $counter keyword for counter to increment
+ */
+ public function stats($counter, $owners=array())
+ {
+ $this->master->stats($counter, $owners);
+ }
+}
+
diff --git a/lib/iomaster.php b/lib/iomaster.php
new file mode 100644
index 000000000..aff5b145c
--- /dev/null
+++ b/lib/iomaster.php
@@ -0,0 +1,361 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * I/O manager to wrap around socket-reading and polling queue & connection managers.
+ *
+ * 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 QueueManager
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class IoMaster
+{
+ public $id;
+
+ protected $multiSite = false;
+ protected $managers = array();
+ protected $singletons = array();
+
+ protected $pollTimeouts = array();
+ protected $lastPoll = array();
+
+ /**
+ * @param string $id process ID to use in logging/monitoring
+ */
+ public function __construct($id)
+ {
+ $this->id = $id;
+ $this->monitor = new QueueMonitor();
+ }
+
+ public function init($multiSite=null)
+ {
+ if ($multiSite !== null) {
+ $this->multiSite = $multiSite;
+ }
+ if ($this->multiSite) {
+ $this->sites = $this->findAllSites();
+ } else {
+ $this->sites = array(common_config('site', 'server'));
+ }
+
+ if (empty($this->sites)) {
+ throw new Exception("Empty status_network table, cannot init");
+ }
+
+ foreach ($this->sites as $site) {
+ if ($site != common_config('site', 'server')) {
+ StatusNet::init($site);
+ }
+
+ $classes = array();
+ if (Event::handle('StartIoManagerClasses', array(&$classes))) {
+ $classes[] = 'QueueManager';
+ if (common_config('xmpp', 'enabled')) {
+ $classes[] = 'XmppManager'; // handles pings/reconnects
+ $classes[] = 'XmppConfirmManager'; // polls for outgoing confirmations
+ }
+ }
+ Event::handle('EndIoManagerClasses', array(&$classes));
+
+ foreach ($classes as $class) {
+ $this->instantiate($class);
+ }
+ }
+ }
+
+ /**
+ * Pull all local sites from status_network table.
+ * @return array of hostnames
+ */
+ protected function findAllSites()
+ {
+ $hosts = array();
+ $sn = new Status_network();
+ $sn->find();
+ while ($sn->fetch()) {
+ $hosts[] = $sn->hostname;
+ }
+ return $hosts;
+ }
+
+ /**
+ * Instantiate an i/o manager class for the current site.
+ * If a multi-site capable handler is already present,
+ * we don't need to build a new one.
+ *
+ * @param string $class
+ */
+ protected function instantiate($class)
+ {
+ if (isset($this->singletons[$class])) {
+ // Already instantiated a multi-site-capable handler.
+ // Just let it know it should listen to this site too!
+ $this->singletons[$class]->addSite(common_config('site', 'server'));
+ return;
+ }
+
+ $manager = $this->getManager($class);
+
+ if ($this->multiSite) {
+ $caps = $manager->multiSite();
+ if ($caps == IoManager::SINGLE_ONLY) {
+ throw new Exception("$class can't run with --all; aborting.");
+ }
+ if ($caps == IoManager::INSTANCE_PER_PROCESS) {
+ // Save this guy for later!
+ // We'll only need the one to cover multiple sites.
+ $this->singletons[$class] = $manager;
+ $manager->addSite(common_config('site', 'server'));
+ }
+ }
+
+ $this->managers[] = $manager;
+ }
+
+ protected function getManager($class)
+ {
+ return call_user_func(array($class, 'get'));
+ }
+
+ /**
+ * Basic run loop...
+ *
+ * Initialize all io managers, then sit around waiting for input.
+ * Between events or timeouts, pass control back to idle() method
+ * to allow for any additional background processing.
+ */
+ function service()
+ {
+ $this->logState('init');
+ $this->start();
+
+ while (true) {
+ $timeouts = array_values($this->pollTimeouts);
+ $timeouts[] = 60; // default max timeout
+
+ // Wait for something on one of our sockets
+ $sockets = array();
+ $managers = array();
+ foreach ($this->managers as $manager) {
+ foreach ($manager->getSockets() as $socket) {
+ $sockets[] = $socket;
+ $managers[] = $manager;
+ }
+ $timeouts[] = intval($manager->timeout());
+ }
+
+ $timeout = min($timeouts);
+ if ($sockets) {
+ $read = $sockets;
+ $write = array();
+ $except = array();
+ $this->logState('listening');
+ common_log(LOG_INFO, "Waiting up to $timeout seconds for socket data...");
+ $ready = stream_select($read, $write, $except, $timeout, 0);
+
+ if ($ready === false) {
+ common_log(LOG_ERR, "Error selecting on sockets");
+ } else if ($ready > 0) {
+ foreach ($read as $socket) {
+ $index = array_search($socket, $sockets, true);
+ if ($index !== false) {
+ $this->logState('queue');
+ $managers[$index]->handleInput($socket);
+ } else {
+ common_log(LOG_ERR, "Saw input on a socket we didn't listen to");
+ }
+ }
+ }
+ }
+
+ if ($timeout > 0 && empty($sockets)) {
+ // If we had no listeners, sleep until the pollers' next requested wakeup.
+ common_log(LOG_INFO, "Sleeping $timeout seconds until next poll cycle...");
+ $this->logState('sleep');
+ sleep($timeout);
+ }
+
+ $this->logState('poll');
+ $this->poll();
+
+ $this->logState('idle');
+ $this->idle();
+
+ $memoryLimit = $this->softMemoryLimit();
+ if ($memoryLimit > 0) {
+ $usage = memory_get_usage();
+ if ($usage > $memoryLimit) {
+ common_log(LOG_INFO, "Queue thread hit soft memory limit ($usage > $memoryLimit); gracefully restarting.");
+ break;
+ }
+ }
+ }
+
+ $this->logState('shutdown');
+ $this->finish();
+ }
+
+ /**
+ * Return fully-parsed soft memory limit in bytes.
+ * @return intval 0 or -1 if not set
+ */
+ function softMemoryLimit()
+ {
+ $softLimit = trim(common_config('queue', 'softlimit'));
+ if (substr($softLimit, -1) == '%') {
+ $limit = trim(ini_get('memory_limit'));
+ $limit = $this->parseMemoryLimit($limit);
+ if ($limit > 0) {
+ return intval(substr($softLimit, 0, -1) * $limit / 100);
+ } else {
+ return -1;
+ }
+ } else {
+ return $this->parseMemoryLimit($limit);
+ }
+ return $softLimit;
+ }
+
+ /**
+ * Interpret PHP shorthand for memory_limit and friends.
+ * Why don't they just expose the actual numeric value? :P
+ * @param string $mem
+ * @return int
+ */
+ protected function parseMemoryLimit($mem)
+ {
+ // http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ $size = array('k' => 1024,
+ 'm' => 1024*1024,
+ 'g' => 1024*1024*1024);
+ if (empty($mem)) {
+ return 0;
+ } else if (is_numeric($mem)) {
+ return intval($mem);
+ } else {
+ $mult = strtolower(substr($mem, -1));
+ if (isset($size[$mult])) {
+ return substr($mem, 0, -1) * $size[$mult];
+ } else {
+ return intval($mem);
+ }
+ }
+ }
+
+ function start()
+ {
+ foreach ($this->managers as $index => $manager) {
+ $manager->start($this);
+ // @fixme error check
+ if ($manager->pollInterval()) {
+ // We'll want to check for input on the first pass
+ $this->pollTimeouts[$index] = 0;
+ $this->lastPoll[$index] = 0;
+ }
+ }
+ }
+
+ function finish()
+ {
+ foreach ($this->managers as $manager) {
+ $manager->finish();
+ // @fixme error check
+ }
+ }
+
+ /**
+ * Called during the idle portion of the runloop to see which handlers
+ */
+ function poll()
+ {
+ foreach ($this->managers as $index => $manager) {
+ $interval = $manager->pollInterval();
+ if ($interval <= 0) {
+ // Not a polling manager.
+ continue;
+ }
+
+ if (isset($this->pollTimeouts[$index])) {
+ $timeout = $this->pollTimeouts[$index];
+ if (time() - $this->lastPoll[$index] < $timeout) {
+ // Not time to poll yet.
+ continue;
+ }
+ } else {
+ $timeout = 0;
+ }
+ $hit = $manager->poll();
+
+ $this->lastPoll[$index] = time();
+ if ($hit) {
+ // Do the next poll quickly, there may be more input!
+ $this->pollTimeouts[$index] = 0;
+ } else {
+ // Empty queue. Exponential backoff up to the maximum poll interval.
+ if ($timeout > 0) {
+ $timeout = min($timeout * 2, $interval);
+ } else {
+ $timeout = 1;
+ }
+ $this->pollTimeouts[$index] = $timeout;
+ }
+ }
+ }
+
+ /**
+ * Called after each handled item or empty polling cycle.
+ * This is a good time to e.g. service your XMPP connection.
+ */
+ function idle()
+ {
+ foreach ($this->managers as $manager) {
+ $manager->idle();
+ }
+ }
+
+ /**
+ * Send thread state update to the monitoring server, if configured.
+ *
+ * @param string $state ('init', 'queue', 'shutdown' etc)
+ * @param string $substate (optional, eg queue name 'omb' 'sms' etc)
+ */
+ protected function logState($state, $substate='')
+ {
+ $this->monitor->logState($this->id, $state, $substate);
+ }
+
+ /**
+ * Send thread stats.
+ * Thread ID will be implicit; other owners can be listed as well
+ * for per-queue and per-site records.
+ *
+ * @param string $key counter name
+ * @param array $owners list of owner keys like 'queue:jabber' or 'site:stat01'
+ */
+ public function stats($key, $owners=array())
+ {
+ $owners[] = "thread:" . $this->id;
+ $this->monitor->stats($key, $owners);
+ }
+}
+
diff --git a/lib/jabber.php b/lib/jabber.php
index 01aed8ffa..4cdfa6746 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -86,7 +86,11 @@ class Sharing_XMPP extends XMPPHP_XMPP
}
/**
- * connect the configured Jabber account to the configured server
+ * Lazy-connect the configured Jabber account to the configured server;
+ * if already opened, the same connection will be returned.
+ *
+ * In a multi-site background process, each site configuration
+ * will get its own connection.
*
* @param string $resource Resource to connect (defaults to configured resource)
*
@@ -95,16 +99,19 @@ class Sharing_XMPP extends XMPPHP_XMPP
function jabber_connect($resource=null)
{
- static $conn = null;
- if (!$conn) {
+ static $connections = array();
+ $site = common_config('site', 'server');
+ if (empty($connections[$site])) {
+ if (empty($resource)) {
+ $resource = common_config('xmpp', 'resource');
+ }
$conn = new Sharing_XMPP(common_config('xmpp', 'host') ?
common_config('xmpp', 'host') :
common_config('xmpp', 'server'),
common_config('xmpp', 'port'),
common_config('xmpp', 'user'),
common_config('xmpp', 'password'),
- ($resource) ? $resource :
- common_config('xmpp', 'resource'),
+ $resource,
common_config('xmpp', 'server'),
common_config('xmpp', 'debug') ?
true : false,
@@ -115,12 +122,16 @@ function jabber_connect($resource=null)
if (!$conn) {
return false;
}
+ $connections[$site] = $conn;
$conn->autoSubscribe();
$conn->useEncryption(common_config('xmpp', 'encryption'));
try {
- $conn->connect(true); // true = persistent connection
+ common_log(LOG_INFO, __METHOD__ . ": connecting " .
+ common_config('xmpp', 'user') . '/' . $resource);
+ //$conn->connect(true); // true = persistent connection
+ $conn->connect(); // persistent connections break multisite
} catch (XMPPHP_Exception $e) {
common_log(LOG_ERR, $e->getMessage());
return false;
@@ -128,7 +139,7 @@ function jabber_connect($resource=null)
$conn->processUntil('session_start');
}
- return $conn;
+ return $connections[$site];
}
/**
@@ -345,77 +356,42 @@ function jabber_broadcast_notice($notice)
$conn = jabber_connect();
- // First, get users to whom this is a direct reply
- $user = new User();
- $UT = common_config('db','type')=='pgsql'?'"user"':'user';
- $user->query("SELECT $UT.id, $UT.jabber " .
- "FROM $UT JOIN reply ON $UT.id = reply.profile_id " .
- 'WHERE reply.notice_id = ' . $notice->id . ' ' .
- "AND $UT.jabber is not null " .
- "AND $UT.jabbernotify = 1 " .
- "AND $UT.jabberreplies = 1 ");
-
- while ($user->fetch()) {
+ $ni = $notice->whoGets();
+
+ foreach ($ni as $user_id => $reason) {
+ $user = User::staticGet($user_id);
+ if (empty($user) ||
+ empty($user->jabber) ||
+ !$user->jabbernotify) {
+ // either not a local user, or just not found
+ continue;
+ }
+ switch ($reason) {
+ case NOTICE_INBOX_SOURCE_REPLY:
+ if (!$user->jabberreplies) {
+ continue 2;
+ }
+ break;
+ case NOTICE_INBOX_SOURCE_SUB:
+ $sub = Subscription::pkeyGet(array('subscriber' => $user->id,
+ 'subscribed' => $notice->profile_id));
+ if (empty($sub) || !$sub->jabber) {
+ continue 2;
+ }
+ break;
+ case NOTICE_INBOX_SOURCE_GROUP:
+ break;
+ default:
+ throw new Exception(sprintf(_("Unknown inbox source %d."), $reason));
+ }
+
common_log(LOG_INFO,
- 'Sending reply notice ' . $notice->id . ' to ' . $user->jabber,
+ 'Sending notice ' . $notice->id . ' to ' . $user->jabber,
__FILE__);
$conn->message($user->jabber, $msg, 'chat', null, $entry);
$conn->processTime(0);
- $sent_to[$user->id] = 1;
}
- $user->free();
-
- // Now, get users subscribed to this profile
-
- $user = new User();
- $user->query("SELECT $UT.id, $UT.jabber " .
- "FROM $UT JOIN subscription " .
- "ON $UT.id = subscription.subscriber " .
- 'WHERE subscription.subscribed = ' . $notice->profile_id . ' ' .
- "AND $UT.jabber is not null " .
- "AND $UT.jabbernotify = 1 " .
- 'AND subscription.jabber = 1 ');
-
- while ($user->fetch()) {
- if (!array_key_exists($user->id, $sent_to)) {
- common_log(LOG_INFO,
- 'Sending notice ' . $notice->id . ' to ' . $user->jabber,
- __FILE__);
- $conn->message($user->jabber, $msg, 'chat', null, $entry);
- // To keep the incoming queue from filling up,
- // we service it after each send.
- $conn->processTime(0);
- $sent_to[$user->id] = 1;
- }
- }
-
- // Now, get users who have it in their inbox because of groups
-
- $user = new User();
- $user->query("SELECT $UT.id, $UT.jabber " .
- "FROM $UT JOIN notice_inbox " .
- "ON $UT.id = notice_inbox.user_id " .
- 'WHERE notice_inbox.notice_id = ' . $notice->id . ' ' .
- 'AND notice_inbox.source = 2 ' .
- "AND $UT.jabber is not null " .
- "AND $UT.jabbernotify = 1 ");
-
- while ($user->fetch()) {
- if (!array_key_exists($user->id, $sent_to)) {
- common_log(LOG_INFO,
- 'Sending notice ' . $notice->id . ' to ' . $user->jabber,
- __FILE__);
- $conn->message($user->jabber, $msg, 'chat', null, $entry);
- // To keep the incoming queue from filling up,
- // we service it after each send.
- $conn->processTime(0);
- $sent_to[$user->id] = 1;
- }
- }
-
- $user->free();
-
return true;
}
@@ -440,7 +416,7 @@ function jabber_public_notice($notice)
// XXX: should we send out non-local messages if public,localonly
// = false? I think not
- if ($public && $notice->is_local) {
+ if ($public && $notice->is_local == Notice::LOCAL_PUBLIC) {
$profile = Profile::staticGet($notice->profile_id);
if (!$profile) {
diff --git a/lib/jabberqueuehandler.php b/lib/jabberqueuehandler.php
new file mode 100644
index 000000000..b1518866d
--- /dev/null
+++ b/lib/jabberqueuehandler.php
@@ -0,0 +1,47 @@
+<?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);
+}
+
+/**
+ * Queue handler for pushing new notices to Jabber users.
+ * @fixme this exception handling doesn't look very good.
+ */
+class JabberQueueHandler extends QueueHandler
+{
+ var $conn = null;
+
+ function transport()
+ {
+ return 'jabber';
+ }
+
+ function handle_notice($notice)
+ {
+ require_once(INSTALLDIR.'/lib/jabber.php');
+ try {
+ return jabber_broadcast_notice($notice);
+ } catch (XMPPHP_Exception $e) {
+ $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+ exit(1);
+ }
+ }
+}
diff --git a/lib/language.php b/lib/language.php
index d8f529201..f5ee7fac5 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -32,6 +32,21 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
+// Locale category constants are usually predefined, but may not be
+// on some systems such as Win32.
+$LC_CATEGORIES = array('LC_CTYPE',
+ 'LC_NUMERIC',
+ 'LC_TIME',
+ 'LC_COLLATE',
+ 'LC_MONETARY',
+ 'LC_MESSAGES',
+ 'LC_ALL');
+foreach ($LC_CATEGORIES as $key => $name) {
+ if (!defined($name)) {
+ define($name, $key);
+ }
+}
+
if (!function_exists('gettext')) {
require_once("php-gettext/gettext.inc");
}
@@ -283,6 +298,7 @@ function get_all_languages() {
'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'),
'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
+ 'fa' => array('q' => 1, 'lang' => 'fa', 'name' => 'Persian', 'direction' => 'rtl'),
'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'),
'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Galician', 'direction' => 'ltr'),
'he' => array('q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'),
diff --git a/lib/liberalstomp.php b/lib/liberalstomp.php
new file mode 100644
index 000000000..c9233843a
--- /dev/null
+++ b/lib/liberalstomp.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * Based on code from Stomp PHP library, working around bugs in the base class.
+ *
+ * Original code is copyright 2005-2006 The Apache Software Foundation
+ * Modifications copyright 2009 StatusNet Inc by Brion Vibber <brion@status.net>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class LiberalStomp extends Stomp
+{
+ /**
+ * We need to be able to get the socket so advanced daemons can
+ * do a select() waiting for input both from the queue and from
+ * other sources such as an XMPP connection.
+ *
+ * @return resource
+ */
+ function getSocket()
+ {
+ return $this->_socket;
+ }
+
+ /**
+ * Make socket connection to the server
+ * We also set the stream to non-blocking mode, since we'll be
+ * select'ing to wait for updates. In blocking mode it seems
+ * to get confused sometimes.
+ *
+ * @throws StompException
+ */
+ protected function _makeConnection ()
+ {
+ parent::_makeConnection();
+ stream_set_blocking($this->_socket, 0);
+ }
+
+ /**
+ * Version 1.0.0 of the Stomp library gets confused if messages
+ * come in too fast over the connection. This version will read
+ * out as many frames as are ready to be read from the socket.
+ *
+ * Modified from Stomp::readFrame()
+ *
+ * @return StompFrame False when no frame to read
+ */
+ public function readFrames ()
+ {
+ if (!$this->hasFrameToRead()) {
+ return false;
+ }
+
+ $rb = 1024;
+ $data = '';
+ $end = false;
+ $frames = array();
+
+ do {
+ // @fixme this sometimes hangs in blocking mode...
+ // shouldn't we have been idle until we found there's more data?
+ $read = fread($this->_socket, $rb);
+ if ($read === false) {
+ $this->_reconnect();
+ // @fixme this will lose prior items
+ return $this->readFrames();
+ }
+ $data .= $read;
+ if (strpos($data, "\x00") !== false) {
+ // Frames are null-delimited, but some servers
+ // may append an extra \n according to old bug reports.
+ $data = str_replace("\x00\n", "\x00", $data);
+ $chunks = explode("\x00", $data);
+
+ $data = array_pop($chunks);
+ $frames = array_merge($frames, $chunks);
+ if ($data == '') {
+ // We're at the end of a frame; stop reading.
+ break;
+ } else {
+ // In the middle of a frame; keep going.
+ }
+ }
+ // @fixme find out why this len < 2 check was there
+ //$len = strlen($data);
+ } while (true);//$len < 2 || $end == false);
+
+ return array_map(array($this, 'parseFrame'), $frames);
+ }
+
+ /**
+ * Parse a raw Stomp frame into an object.
+ * Extracted from Stomp::readFrame()
+ *
+ * @param string $data
+ * @return StompFrame
+ */
+ function parseFrame($data)
+ {
+ list ($header, $body) = explode("\n\n", $data, 2);
+ $header = explode("\n", $header);
+ $headers = array();
+ $command = null;
+ foreach ($header as $v) {
+ if (isset($command)) {
+ list ($name, $value) = explode(':', $v, 2);
+ $headers[$name] = $value;
+ } else {
+ $command = $v;
+ }
+ }
+ $frame = new StompFrame($command, $headers, trim($body));
+ if (isset($frame->headers['transformation']) && $frame->headers['transformation'] == 'jms-map-json') {
+ require_once 'Stomp/Message/Map.php';
+ return new StompMessageMap($frame);
+ } else {
+ return $frame;
+ }
+ return $frame;
+ }
+}
+
diff --git a/lib/mail.php b/lib/mail.php
index dffac3262..c724764cc 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -251,11 +251,11 @@ function mail_subscribe_notify_profile($listenee, $other)
common_config('site', 'name'),
$other->profileurl,
($other->location) ?
- sprintf(_("Location: %s\n"), $other->location) : '',
+ sprintf(_("Location: %s"), $other->location) . "\n" : '',
($other->homepage) ?
- sprintf(_("Homepage: %s\n"), $other->homepage) : '',
+ sprintf(_("Homepage: %s"), $other->homepage) . "\n" : '',
($other->bio) ?
- sprintf(_("Bio: %s\n\n"), $other->bio) : '',
+ sprintf(_("Bio: %s"), $other->bio) . "\n\n" : '',
common_config('site', 'name'),
common_local_url('emailsettings'));
@@ -599,6 +599,10 @@ function mail_notify_attn($user, $notice)
$sender = $notice->getProfile();
+ if ($sender->id == $user->id) {
+ return;
+ }
+
if (!$sender->hasRight(Right::EMAILONREPLY)) {
return;
}
@@ -648,4 +652,3 @@ function mail_notify_attn($user, $notice)
common_init_locale();
mail_to_user($user, $subject, $body);
}
-
diff --git a/lib/mailhandler.php b/lib/mailhandler.php
new file mode 100644
index 000000000..85be89f18
--- /dev/null
+++ b/lib/mailhandler.php
@@ -0,0 +1,275 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once(INSTALLDIR . '/lib/mail.php');
+require_once(INSTALLDIR . '/lib/mediafile.php');
+require_once('Mail/mimeDecode.php');
+
+# FIXME: we use both Mail_mimeDecode and mailparse
+# Need to move everything to mailparse
+
+class MailHandler
+{
+ function __construct()
+ {
+ }
+
+ function handle_message($rawmessage)
+ {
+ list($from, $to, $msg, $attachments) = $this->parse_message($rawmessage);
+ if (!$from || !$to || !$msg) {
+ $this->error(null, _('Could not parse message.'));
+ }
+ common_log(LOG_INFO, "Mail from $from to $to with ".count($attachments) .' attachment(s): ' .substr($msg, 0, 20));
+ $user = $this->user_from_header($from);
+ if (!$user) {
+ $this->error($from, _('Not a registered user.'));
+ return false;
+ }
+ if (!$this->user_match_to($user, $to)) {
+ $this->error($from, _('Sorry, that is not your incoming email address.'));
+ return false;
+ }
+ if (!$user->emailpost) {
+ $this->error($from, _('Sorry, no incoming email allowed.'));
+ return false;
+ }
+ $response = $this->handle_command($user, $from, $msg);
+ if ($response) {
+ return true;
+ }
+ $msg = $this->cleanup_msg($msg);
+ $msg = common_shorten_links($msg);
+ if (Notice::contentTooLong($msg)) {
+ $this->error($from, sprintf(_('That\'s too long. '.
+ 'Max notice size is %d chars.'),
+ Notice::maxContent()));
+ }
+
+ $mediafiles = array();
+
+ foreach($attachments as $attachment){
+
+ $mf = null;
+
+ try {
+ $mf = MediaFile::fromFileHandle($attachment, $user);
+ } catch(ClientException $ce) {
+ $this->error($from, $ce->getMessage());
+ }
+
+ $msg .= ' ' . $mf->shortUrl();
+
+ array_push($mediafiles, $mf);
+ fclose($attachment);
+ }
+
+ $err = $this->add_notice($user, $msg, $mediafiles);
+
+ if (is_string($err)) {
+ $this->error($from, $err);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ function error($from, $msg)
+ {
+ file_put_contents("php://stderr", $msg . "\n");
+ exit(1);
+ }
+
+ function user_from_header($from_hdr)
+ {
+ $froms = mailparse_rfc822_parse_addresses($from_hdr);
+ if (!$froms) {
+ return null;
+ }
+ $from = $froms[0];
+ $addr = common_canonical_email($from['address']);
+ $user = User::staticGet('email', $addr);
+ if (!$user) {
+ $user = User::staticGet('smsemail', $addr);
+ }
+ return $user;
+ }
+
+ function user_match_to($user, $to_hdr)
+ {
+ $incoming = $user->incomingemail;
+ $tos = mailparse_rfc822_parse_addresses($to_hdr);
+ foreach ($tos as $to) {
+ if (strcasecmp($incoming, $to['address']) == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function handle_command($user, $from, $msg)
+ {
+ $inter = new CommandInterpreter();
+ $cmd = $inter->handle_command($user, $msg);
+ if ($cmd) {
+ $cmd->execute(new MailChannel($from));
+ return true;
+ }
+ return false;
+ }
+
+ function respond($from, $to, $response)
+ {
+
+ $headers['From'] = $to;
+ $headers['To'] = $from;
+ $headers['Subject'] = _('Command complete');
+
+ return mail_send(array($from), $headers, $response);
+ }
+
+ function log($level, $msg)
+ {
+ common_log($level, 'MailDaemon: '.$msg);
+ }
+
+ function add_notice($user, $msg, $mediafiles)
+ {
+ try {
+ $notice = Notice::saveNew($user->id, $msg, 'mail');
+ } catch (Exception $e) {
+ $this->log(LOG_ERR, $e->getMessage());
+ return $e->getMessage();
+ }
+ foreach($mediafiles as $mf){
+ $mf->attachToNotice($notice);
+ }
+ common_broadcast_notice($notice);
+ $this->log(LOG_INFO,
+ 'Added notice ' . $notice->id . ' from user ' . $user->nickname);
+ return true;
+ }
+
+ function parse_message($contents)
+ {
+ $parsed = Mail_mimeDecode::decode(array('input' => $contents,
+ 'include_bodies' => true,
+ 'decode_headers' => true,
+ 'decode_bodies' => true));
+ if (!$parsed) {
+ return null;
+ }
+
+ $from = $parsed->headers['from'];
+
+ $to = $parsed->headers['to'];
+
+ $type = $parsed->ctype_primary . '/' . $parsed->ctype_secondary;
+
+ $attachments = array();
+
+ $this->extract_part($parsed,$msg,$attachments);
+
+ return array($from, $to, $msg, $attachments);
+ }
+
+ function extract_part($parsed,&$msg,&$attachments){
+ if ($parsed->ctype_primary == 'multipart') {
+ if($parsed->ctype_secondary == 'alternative'){
+ $altmsg = $this->extract_msg_from_multipart_alternative_part($parsed);
+ if(!empty($altmsg)) $msg = $altmsg;
+ }else{
+ foreach($parsed->parts as $part){
+ $this->extract_part($part,$msg,$attachments);
+ }
+ }
+ } else if ($parsed->ctype_primary == 'text'
+ && $parsed->ctype_secondary=='plain') {
+ $msg = $parsed->body;
+ if(strtolower($parsed->ctype_parameters['charset']) != "utf-8"){
+ $msg = utf8_encode($msg);
+ }
+ }else if(!empty($parsed->body)){
+ if(common_config('attachments', 'uploads')){
+ //only save attachments if uploads are enabled
+ $attachment = tmpfile();
+ fwrite($attachment, $parsed->body);
+ $attachments[] = $attachment;
+ }
+ }
+ }
+
+ function extract_msg_from_multipart_alternative_part($parsed){
+ foreach ($parsed->parts as $part) {
+ $this->extract_part($part,$msg,$attachments);
+ }
+ //we don't want any attachments that are a result of this parsing
+ return $msg;
+ }
+
+ function unsupported_type($type)
+ {
+ $this->error(null, sprintf(_('Unsupported message type: %s'), $type));
+ }
+
+ function cleanup_msg($msg)
+ {
+ $lines = explode("\n", $msg);
+
+ $output = '';
+
+ foreach ($lines as $line) {
+ // skip quotes
+ if (preg_match('/^\s*>.*$/', $line)) {
+ continue;
+ }
+ // skip start of quote
+ if (preg_match('/^\s*On.*wrote:\s*$/', $line)) {
+ continue;
+ }
+ // probably interesting to someone, not us
+ if (preg_match('/^\s*Sent via/', $line)) {
+ continue;
+ }
+ if (preg_match('/^\s*Sent from my/', $line)) {
+ continue;
+ }
+
+ // skip everything after a sig
+ if (preg_match('/^\s*--+\s*$/', $line) ||
+ preg_match('/^\s*__+\s*$/', $line))
+ {
+ break;
+ }
+ // skip everything after Outlook quote
+ if (preg_match('/^\s*-+\s*Original Message\s*-+\s*$/', $line)) {
+ break;
+ }
+ // skip everything after weird forward
+ if (preg_match('/^\s*Begin\s+forward/', $line)) {
+ break;
+ }
+
+ $output .= ' ' . $line;
+ }
+
+ preg_replace('/\s+/', ' ', $output);
+ return trim($output);
+ }
+}
diff --git a/lib/mediafile.php b/lib/mediafile.php
index 29d752f0c..e3d5b1dbc 100644
--- a/lib/mediafile.php
+++ b/lib/mediafile.php
@@ -176,7 +176,7 @@ class MediaFile
// Should never actually get here
@unlink($_FILES[$param]['tmp_name']);
- throw new ClientException(_('File exceeds user\'s quota!'));
+ throw new ClientException(_('File exceeds user\'s quota.'));
return;
}
@@ -198,7 +198,7 @@ class MediaFile
}
} else {
- throw new ClientException(_('Could not determine file\'s mime-type!'));
+ throw new ClientException(_('Could not determine file\'s MIME type.'));
return;
}
@@ -213,7 +213,7 @@ class MediaFile
// Should never actually get here
- throw new ClientException(_('File exceeds user\'s quota!'));
+ throw new ClientException(_('File exceeds user\'s quota.'));
return;
}
@@ -234,7 +234,7 @@ class MediaFile
$stream['uri'] . ' ' . $filepath));
}
} else {
- throw new ClientException(_('Could not determine file\'s mime-type!'));
+ throw new ClientException(_('Could not determine file\'s MIME type.'));
return;
}
@@ -272,7 +272,7 @@ class MediaFile
$hint = '';
}
throw new ClientException(sprintf(
- _('%s is not a supported filetype on this server.'), $filetype) . $hint);
+ _('%s is not a supported file type on this server.'), $filetype) . $hint);
}
static function respectsQuota($user, $filesize)
@@ -286,4 +286,4 @@ class MediaFile
}
}
-} \ No newline at end of file
+}
diff --git a/lib/noticeform.php b/lib/noticeform.php
index 593a1e932..a00615ce7 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -110,6 +110,8 @@ class NoticeForm extends Form
$this->user = common_current_user();
}
+ $this->profile = $this->user->getProfile();
+
if (common_config('attachments', 'uploads')) {
$this->enctype = 'multipart/form-data';
}
@@ -198,12 +200,25 @@ class NoticeForm extends Form
$this->out->hidden('notice_return-to', $this->action, 'returnto');
}
$this->out->hidden('notice_in-reply-to', $this->inreplyto, 'inreplyto');
- $this->out->hidden('notice_data-lat', empty($this->lat) ? null : $this->lat, 'lat');
- $this->out->hidden('notice_data-lon', empty($this->lon) ? null : $this->lon, 'lon');
- $this->out->hidden('notice_data-location_id', empty($this->location_id) ? null : $this->location_id, 'location_id');
- $this->out->hidden('notice_data-location_ns', empty($this->location_ns) ? null : $this->location_ns, 'location_ns');
- Event::handle('StartShowNoticeFormData', array($this));
+ if ($this->user->shareLocation()) {
+ $this->out->hidden('notice_data-lat', empty($this->lat) ? (empty($this->profile->lat) ? null : $this->profile->lat) : $this->lat, 'lat');
+ $this->out->hidden('notice_data-lon', empty($this->lon) ? (empty($this->profile->lon) ? null : $this->profile->lon) : $this->lon, 'lon');
+ $this->out->hidden('notice_data-location_id', empty($this->location_id) ? (empty($this->profile->location_id) ? null : $this->profile->location_id) : $this->location_id, 'location_id');
+ $this->out->hidden('notice_data-location_ns', empty($this->location_ns) ? (empty($this->profile->location_ns) ? null : $this->profile->location_ns) : $this->location_ns, 'location_ns');
+
+ $this->out->elementStart('div', array('id' => 'notice_data-geo_wrap',
+ 'title' => common_local_url('geocode')));
+ $this->out->checkbox('notice_data-geo', _('Share my location'), true);
+ $this->out->elementEnd('div');
+ $this->out->inlineScript(' var NoticeDataGeo_text = {'.
+ 'ShareDisable: "'._('Do not share my location').'",'.
+ 'InfoMinimize: "'._('Hide this info').'",'.
+ 'ErrorTimeout: "'._('Sorry, retrieving your geo location is taking longer than expected, please try again later').'"'.
+ '}');
+ }
+
+ Event::handle('EndShowNoticeFormData', array($this));
}
}
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 5eb2633ac..78abf34a7 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -379,7 +379,7 @@ class NoticeListItem extends Widget
function showNoticeLink()
{
- if($this->notice->is_local){
+ if($this->notice->is_local == Notice::LOCAL_PUBLIC || $this->notice->is_local == Notice::LOCAL_NONPUBLIC){
$noticeurl = common_local_url('shownotice',
array('notice' => $this->notice->id));
}else{
diff --git a/lib/ombqueuehandler.php b/lib/ombqueuehandler.php
new file mode 100644
index 000000000..3ffc1313b
--- /dev/null
+++ b/lib/ombqueuehandler.php
@@ -0,0 +1,56 @@
+<?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);
+}
+
+/**
+ * Queue handler for pushing new notices to OpenMicroBlogging subscribers.
+ */
+class OmbQueueHandler extends QueueHandler
+{
+
+ function transport()
+ {
+ return 'omb';
+ }
+
+ /**
+ * @fixme doesn't currently report failure back to the queue manager
+ * because omb_broadcast_notice() doesn't report it to us
+ */
+ function handle_notice($notice)
+ {
+ if ($this->is_remote($notice)) {
+ $this->log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id);
+ return true;
+ } else {
+ require_once(INSTALLDIR.'/lib/omb.php');
+ omb_broadcast_notice($notice);
+ return true;
+ }
+ }
+
+ function is_remote($notice)
+ {
+ $user = User::staticGet($notice->profile_id);
+ return is_null($user);
+ }
+}
diff --git a/lib/ping.php b/lib/ping.php
index 5698c4038..735af9ef1 100644
--- a/lib/ping.php
+++ b/lib/ping.php
@@ -21,7 +21,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
function ping_broadcast_notice($notice) {
- if (!$notice->is_local) {
+ if ($notice->is_local != Notice::LOCAL_PUBLIC && $notice->is_local != Notice::LOCAL_NONPUBLIC) {
return true;
}
@@ -115,4 +115,4 @@ function ping_notice_tags($notice) {
return implode('|', $tags);
}
return NULL;
-} \ No newline at end of file
+}
diff --git a/lib/pingqueuehandler.php b/lib/pingqueuehandler.php
new file mode 100644
index 000000000..8bb218078
--- /dev/null
+++ b/lib/pingqueuehandler.php
@@ -0,0 +1,37 @@
+<?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);
+}
+
+/**
+ * Queue handler for pushing new notices to ping servers.
+ */
+class PingQueueHandler extends QueueHandler {
+
+ function transport() {
+ return 'ping';
+ }
+
+ function handle_notice($notice) {
+ require_once INSTALLDIR . '/lib/ping.php';
+ return ping_broadcast_notice($notice);
+ }
+}
diff --git a/lib/plugin.php b/lib/plugin.php
index de7313e59..65ccdafbb 100644
--- a/lib/plugin.php
+++ b/lib/plugin.php
@@ -104,5 +104,16 @@ class Plugin
{
$this->log(LOG_DEBUG, $msg);
}
+
+ function onPluginVersion(&$versions)
+ {
+ $cls = get_class($this);
+ $name = mb_substr($cls, 0, -6);
+
+ $versions[] = array('name' => $name,
+ 'version' => _('Unknown'));
+
+ return true;
+ }
}
diff --git a/lib/pluginqueuehandler.php b/lib/pluginqueuehandler.php
new file mode 100644
index 000000000..24d504699
--- /dev/null
+++ b/lib/pluginqueuehandler.php
@@ -0,0 +1,50 @@
+<?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);
+}
+
+/**
+ * Queue handler for letting plugins handle stuff.
+ *
+ * The plugin queue handler accepts notices over the "plugin" queue
+ * and simply passes them through the "HandleQueuedNotice" event.
+ *
+ * This gives plugins a chance to do background processing without
+ * actually registering their own queue and ensuring that things
+ * are queued into it.
+ *
+ * Fancier plugins may wish to instead hook the 'GetQueueHandlerClass'
+ * event with their own class, in which case they must ensure that
+ * their notices get enqueued when they need them.
+ */
+class PluginQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'plugin';
+ }
+
+ function handle_notice($notice)
+ {
+ Event::handle('HandleQueuedNotice', array(&$notice));
+ return true;
+ }
+}
diff --git a/lib/publicqueuehandler.php b/lib/publicqueuehandler.php
new file mode 100644
index 000000000..9ea9ee73a
--- /dev/null
+++ b/lib/publicqueuehandler.php
@@ -0,0 +1,47 @@
+<?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);
+}
+
+/**
+ * Queue handler for pushing new notices to public XMPP subscribers.
+ * @fixme correct this exception handling
+ */
+class PublicQueueHandler extends QueueHandler
+{
+
+ function transport()
+ {
+ return 'public';
+ }
+
+ function handle_notice($notice)
+ {
+ require_once(INSTALLDIR.'/lib/jabber.php');
+ try {
+ return jabber_public_notice($notice);
+ } catch (XMPPHP_Exception $e) {
+ $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+ die($e->getMessage());
+ }
+ return true;
+ }
+}
diff --git a/lib/queuehandler.php b/lib/queuehandler.php
index cd43b1e09..613be6e33 100644
--- a/lib/queuehandler.php
+++ b/lib/queuehandler.php
@@ -19,14 +19,6 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-require_once(INSTALLDIR.'/lib/daemon.php');
-require_once(INSTALLDIR.'/classes/Queue_item.php');
-require_once(INSTALLDIR.'/classes/Notice.php');
-
-define('CLAIM_TIMEOUT', 1200);
-define('QUEUE_HANDLER_MISS_IDLE', 10);
-define('QUEUE_HANDLER_HIT_IDLE', 0);
-
/**
* Base class for queue handlers.
*
@@ -36,24 +28,20 @@ define('QUEUE_HANDLER_HIT_IDLE', 0);
*
* Subclasses must override at least the following methods:
* - transport
- * - start
- * - finish
* - handle_notice
- *
- * Some subclasses will also want to override the idle handler:
- * - idle
*/
-class QueueHandler extends Daemon
+#class QueueHandler extends Daemon
+class QueueHandler
{
- function __construct($id=null, $daemonize=true)
- {
- parent::__construct($daemonize);
-
- if ($id) {
- $this->set_id($id);
- }
- }
+# function __construct($id=null, $daemonize=true)
+# {
+# parent::__construct($daemonize);
+#
+# if ($id) {
+# $this->set_id($id);
+# }
+# }
/**
* How many seconds a polling-based queue manager should wait between
@@ -61,22 +49,23 @@ class QueueHandler extends Daemon
*
* Defaults to 60 seconds; override to speed up or slow down.
*
+ * @fixme not really compatible with global queue manager
* @return int timeout in seconds
*/
- function timeout()
- {
- return 60;
- }
+# function timeout()
+# {
+# return 60;
+# }
- function class_name()
- {
- return ucfirst($this->transport()) . 'Handler';
- }
+# function class_name()
+# {
+# return ucfirst($this->transport()) . 'Handler';
+# }
- function name()
- {
- return strtolower($this->class_name().'.'.$this->get_id());
- }
+# function name()
+# {
+# return strtolower($this->class_name().'.'.$this->get_id());
+# }
/**
* Return transport keyword which identifies items this queue handler
@@ -93,30 +82,6 @@ class QueueHandler extends Daemon
}
/**
- * Initialization, run when the queue handler starts.
- * If this function indicates failure, the handler run will be aborted.
- *
- * @fixme run() will abort if this doesn't return true,
- * but some subclasses don't bother.
- * @return boolean true on success, false on failure
- */
- function start()
- {
- }
-
- /**
- * Cleanup, run when the queue handler ends.
- * If this function indicates failure, a warning will be logged.
- *
- * @fixme run() will throw warnings if this doesn't return true,
- * but many subclasses don't bother.
- * @return boolean true on success, false on failure
- */
- function finish()
- {
- }
-
- /**
* Here's the meat of your queue handler -- you're handed a Notice
* object, which you may do as you will with.
*
@@ -169,29 +134,10 @@ class QueueHandler extends Daemon
return true;
}
- /**
- * Called by QueueHandler after each handled item or empty polling cycle.
- * This is a good time to e.g. service your XMPP connection.
- *
- * Doesn't need to be overridden if there's no maintenance to do.
- *
- * @param int $timeout seconds to sleep if there's nothing to do
- */
- function idle($timeout=0)
- {
- if ($timeout > 0) {
- sleep($timeout);
- }
- }
function log($level, $msg)
{
common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg);
}
-
- function getSockets()
- {
- return array();
- }
}
diff --git a/lib/queuemanager.php b/lib/queuemanager.php
index 43105b7a8..a98c0efff 100644
--- a/lib/queuemanager.php
+++ b/lib/queuemanager.php
@@ -2,7 +2,7 @@
/**
* StatusNet, the distributed open-source microblogging tool
*
- * Abstract class for queue managers
+ * Abstract class for i/o managers
*
* PHP version 5
*
@@ -23,16 +23,32 @@
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
- * @copyright 2009 StatusNet, Inc.
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2009-2010 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 QueueManager
+/**
+ * Completed child classes must implement the enqueue() method.
+ *
+ * For background processing, classes should implement either socket-based
+ * input (handleInput(), getSockets()) or idle-loop polling (idle()).
+ */
+abstract class QueueManager extends IoManager
{
static $qm = null;
- static function get()
+ /**
+ * Factory function to pull the appropriate QueueManager object
+ * for this site's configuration. It can then be used to queue
+ * events for later processing or to spawn a processing loop.
+ *
+ * Plugins can add to the built-in types by hooking StartNewQueueManager.
+ *
+ * @return QueueManager
+ */
+ public static function get()
{
if (empty(self::$qm)) {
@@ -62,13 +78,130 @@ class QueueManager
return self::$qm;
}
- function enqueue($object, $queue)
+ /**
+ * @fixme wouldn't necessarily work with other class types.
+ * Better to change the interface...?
+ */
+ public static function multiSite()
+ {
+ if (common_config('queue', 'subsystem') == 'stomp') {
+ return IoManager::INSTANCE_PER_PROCESS;
+ } else {
+ return IoManager::SINGLE_ONLY;
+ }
+ }
+
+ function __construct()
{
- throw ServerException("Unimplemented function 'enqueue' called");
+ $this->initialize();
}
- function service($queue, $handler)
+ /**
+ * Store an object (usually/always a Notice) into the given queue
+ * for later processing. No guarantee is made on when it will be
+ * processed; it could be immediately or at some unspecified point
+ * in the future.
+ *
+ * Must be implemented by any queue manager.
+ *
+ * @param Notice $object
+ * @param string $queue
+ */
+ abstract function enqueue($object, $queue);
+
+ /**
+ * Instantiate the appropriate QueueHandler class for the given queue.
+ *
+ * @param string $queue
+ * @return mixed QueueHandler or null
+ */
+ function getHandler($queue)
{
- throw ServerException("Unimplemented function 'service' called");
+ if (isset($this->handlers[$queue])) {
+ $class = $this->handlers[$queue];
+ if (class_exists($class)) {
+ return new $class();
+ } else {
+ common_log(LOG_ERR, "Nonexistent handler class '$class' for queue '$queue'");
+ }
+ } else {
+ common_log(LOG_ERR, "Requested handler for unkown queue '$queue'");
+ }
+ return null;
+ }
+
+ /**
+ * Get a list of all registered queue transport names.
+ *
+ * @return array of strings
+ */
+ function getQueues()
+ {
+ return array_keys($this->handlers);
+ }
+
+ /**
+ * Initialize the list of queue handlers
+ *
+ * @event StartInitializeQueueManager
+ * @event EndInitializeQueueManager
+ */
+ function initialize()
+ {
+ if (Event::handle('StartInitializeQueueManager', array($this))) {
+ $this->connect('plugin', 'PluginQueueHandler');
+ $this->connect('omb', 'OmbQueueHandler');
+ $this->connect('ping', 'PingQueueHandler');
+ if (common_config('sms', 'enabled')) {
+ $this->connect('sms', 'SmsQueueHandler');
+ }
+
+ // XMPP output handlers...
+ if (common_config('xmpp', 'enabled')) {
+ $this->connect('jabber', 'JabberQueueHandler');
+ $this->connect('public', 'PublicQueueHandler');
+
+ // @fixme this should move up a level or should get an actual queue
+ $this->connect('confirm', 'XmppConfirmHandler');
+ }
+
+ // For compat with old plugins not registering their own handlers.
+ $this->connect('plugin', 'PluginQueueHandler');
+ }
+ Event::handle('EndInitializeQueueManager', array($this));
+ }
+
+ /**
+ * Register a queue transport name and handler class for your plugin.
+ * Only registered transports will be reliably picked up!
+ *
+ * @param string $transport
+ * @param string $class
+ */
+ public function connect($transport, $class)
+ {
+ $this->handlers[$transport] = $class;
+ }
+
+ /**
+ * Send a statistic ping to the queue monitoring system,
+ * optionally with a per-queue id.
+ *
+ * @param string $key
+ * @param string $queue
+ */
+ function stats($key, $queue=false)
+ {
+ $owners = array();
+ if ($queue) {
+ $owners[] = "queue:$queue";
+ $owners[] = "site:" . common_config('site', 'server');
+ }
+ if (isset($this->master)) {
+ $this->master->stats($key, $owners);
+ } else {
+ $monitor = new QueueMonitor();
+ $monitor->stats($key, $owners);
+ }
}
}
diff --git a/lib/queuemonitor.php b/lib/queuemonitor.php
new file mode 100644
index 000000000..1c306a629
--- /dev/null
+++ b/lib/queuemonitor.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Monitoring output helper for IoMaster and IoManager/QueueManager
+ *
+ * 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 QueueManager
+ * @package StatusNet
+ * @author Brion Vibber <brion@status.net>
+ * @copyright 2010 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 QueueMonitor
+{
+ protected $monSocket = null;
+
+ /**
+ * Increment monitoring statistics for a given counter, if configured.
+ * Only explicitly listed thread/site/queue owners will be incremented.
+ *
+ * @param string $key counter name
+ * @param array $owners list of owner keys like 'queue:jabber' or 'site:stat01'
+ */
+ public function stats($key, $owners=array())
+ {
+ $this->ping(array('counter' => $key,
+ 'owners' => $owners));
+ }
+
+ /**
+ * Send thread state update to the monitoring server, if configured.
+ *
+ * @param string $thread ID (eg 'generic.1')
+ * @param string $state ('init', 'queue', 'shutdown' etc)
+ * @param string $substate (optional, eg queue name 'omb' 'sms' etc)
+ */
+ public function logState($threadId, $state, $substate='')
+ {
+ $this->ping(array('thread_id' => $threadId,
+ 'state' => $state,
+ 'substate' => $substate,
+ 'ts' => microtime(true)));
+ }
+
+ /**
+ * General call to the monitoring server
+ */
+ protected function ping($data)
+ {
+ $target = common_config('queue', 'monitor');
+ if (empty($target)) {
+ return;
+ }
+
+ $data = $this->prepMonitorData($data);
+
+ if (substr($target, 0, 4) == 'udp:') {
+ $this->pingUdp($target, $data);
+ } else if (substr($target, 0, 5) == 'http:') {
+ $this->pingHttp($target, $data);
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ' unknown monitor target type ' . $target);
+ }
+ }
+
+ protected function pingUdp($target, $data)
+ {
+ if (!$this->monSocket) {
+ $this->monSocket = stream_socket_client($target, $errno, $errstr);
+ }
+ if ($this->monSocket) {
+ $post = http_build_query($data, '', '&');
+ stream_socket_sendto($this->monSocket, $post);
+ } else {
+ common_log(LOG_ERR, __METHOD__ . " UDP logging fail: $errstr");
+ }
+ }
+
+ protected function pingHttp($target, $data)
+ {
+ $client = new HTTPClient();
+ $result = $client->post($target, array(), $data);
+
+ if (!$result->isOk()) {
+ common_log(LOG_ERR, __METHOD__ . ' HTTP ' . $result->getStatus() .
+ ': ' . $result->getBody());
+ }
+ }
+
+ protected function prepMonitorData($data)
+ {
+ #asort($data);
+ #$macdata = http_build_query($data, '', '&');
+ #$key = 'This is a nice old key';
+ #$data['hmac'] = hash_hmac('sha256', $macdata, $key);
+ return $data;
+ }
+
+}
diff --git a/lib/repeatform.php b/lib/repeatform.php
index 50e5d6dbe..4f1c8aa32 100644
--- a/lib/repeatform.php
+++ b/lib/repeatform.php
@@ -104,7 +104,7 @@ class RepeatForm extends Form
*/
function formLegend()
{
- $this->out->element('legend', null, _('Repeat this notice'));
+ $this->out->element('legend', null, _('Repeat this notice?'));
}
/**
@@ -129,7 +129,7 @@ class RepeatForm extends Form
function formActions()
{
$this->out->submit('repeat-submit-' . $this->notice->id,
- _('Repeat'), 'submit', null, _('Repeat this notice'));
+ _('Yes'), 'submit', null, _('Repeat this notice'));
}
/**
diff --git a/lib/router.php b/lib/router.php
index 474e05996..6b87ed27f 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -88,7 +88,10 @@ class Router
$m->connect('doc/:title', array('action' => 'doc'));
- $m->connect('main/login?user_id=:user_id&token=:token', array('action'=>'login'), array('user_id'=> '[0-9]+', 'token'=>'.+'));
+ $m->connect('main/otp/:user_id/:token',
+ array('action' => 'otp'),
+ array('user_id' => '[0-9]+',
+ 'token' => '.+'));
// main stuff is repetitive
@@ -100,7 +103,10 @@ class Router
'sandbox', 'unsandbox',
'silence', 'unsilence',
'repeat',
- 'deleteuser');
+ 'deleteuser',
+ 'geocode',
+ 'version',
+ );
foreach ($main as $a) {
$m->connect('main/'.$a, array('action' => $a));
@@ -439,19 +445,19 @@ class Router
// Social graph
$m->connect('api/friends/ids/:id.:format',
- array('action' => 'apiFriends',
+ array('action' => 'apiuserfriends',
'ids_only' => true));
$m->connect('api/followers/ids/:id.:format',
- array('action' => 'apiFollowers',
+ array('action' => 'apiuserfollowers',
'ids_only' => true));
$m->connect('api/friends/ids.:format',
- array('action' => 'apiFriends',
+ array('action' => 'apiuserfriends',
'ids_only' => true));
$m->connect('api/followers/ids.:format',
- array('action' => 'apiFollowers',
+ array('action' => 'apiuserfollowers',
'ids_only' => true));
// account
diff --git a/lib/schema.php b/lib/schema.php
index a8ba91b87..a7f64ebed 100644
--- a/lib/schema.php
+++ b/lib/schema.php
@@ -523,6 +523,14 @@ class Schema
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
+
+ if (!empty($cd->auto_increment)) {
+ $sql .= " auto_increment ";
+ }
+
+ if (!empty($cd->extra)) {
+ $sql .= "{$cd->extra} ";
+ }
return $sql;
}
diff --git a/lib/smsqueuehandler.php b/lib/smsqueuehandler.php
new file mode 100644
index 000000000..48a96409d
--- /dev/null
+++ b/lib/smsqueuehandler.php
@@ -0,0 +1,39 @@
+<?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);
+}
+
+/**
+ * Queue handler for pushing new notices to local subscribers using SMS.
+ */
+class SmsQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'sms';
+ }
+
+ function handle_notice($notice)
+ {
+ require_once(INSTALLDIR.'/lib/mail.php');
+ return mail_broadcast_notice_sms($notice);
+ }
+}
diff --git a/lib/statusnet.php b/lib/statusnet.php
new file mode 100644
index 000000000..29e903026
--- /dev/null
+++ b/lib/statusnet.php
@@ -0,0 +1,298 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010 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);
+}
+
+global $config, $_server, $_path;
+
+/**
+ * Global configuration setup and management.
+ */
+class StatusNet
+{
+ protected static $have_config;
+
+ /**
+ * Configure and instantiate a plugin into the current configuration.
+ * Class definitions will be loaded from standard paths if necessary.
+ * Note that initialization events won't be fired until later.
+ *
+ * @param string $name class name & plugin file/subdir name
+ * @param array $attrs key/value pairs of public attributes to set on plugin instance
+ *
+ * @throws ServerException if plugin can't be found
+ */
+ public static function addPlugin($name, $attrs = null)
+ {
+ $name = ucfirst($name);
+ $pluginclass = "{$name}Plugin";
+
+ if (!class_exists($pluginclass)) {
+
+ $files = array("local/plugins/{$pluginclass}.php",
+ "local/plugins/{$name}/{$pluginclass}.php",
+ "local/{$pluginclass}.php",
+ "local/{$name}/{$pluginclass}.php",
+ "plugins/{$pluginclass}.php",
+ "plugins/{$name}/{$pluginclass}.php");
+
+ foreach ($files as $file) {
+ $fullpath = INSTALLDIR.'/'.$file;
+ if (@file_exists($fullpath)) {
+ include_once($fullpath);
+ break;
+ }
+ }
+ if (!class_exists($pluginclass)) {
+ throw new ServerException(500, "Plugin $name not found.");
+ }
+ }
+
+ $inst = new $pluginclass();
+ if (!empty($attrs)) {
+ foreach ($attrs as $aname => $avalue) {
+ $inst->$aname = $avalue;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Initialize, or re-initialize, StatusNet global configuration
+ * and plugins.
+ *
+ * If switching site configurations during script execution, be
+ * careful when working with leftover objects -- global settings
+ * affect many things and they may not behave as you expected.
+ *
+ * @param $server optional web server hostname for picking config
+ * @param $path optional URL path for picking config
+ * @param $conffile optional configuration file path
+ *
+ * @throws NoConfigException if config file can't be found
+ */
+ public static function init($server=null, $path=null, $conffile=null)
+ {
+ StatusNet::initDefaults($server, $path);
+ StatusNet::loadConfigFile($conffile);
+
+ // Load settings from database; note we need autoload for this
+ Config::loadSettings();
+
+ self::initPlugins();
+ }
+
+ /**
+ * Fire initialization events for all instantiated plugins.
+ */
+ protected static function initPlugins()
+ {
+ // Load default plugins
+ foreach (common_config('plugins', 'default') as $name => $params) {
+ if (is_null($params)) {
+ addPlugin($name);
+ } else if (is_array($params)) {
+ if (count($params) == 0) {
+ addPlugin($name);
+ } else {
+ $keys = array_keys($params);
+ if (is_string($keys[0])) {
+ addPlugin($name, $params);
+ } else {
+ foreach ($params as $paramset) {
+ addPlugin($name, $paramset);
+ }
+ }
+ }
+ }
+ }
+
+ // XXX: if plugins should check the schema at runtime, do that here.
+ if (common_config('db', 'schemacheck') == 'runtime') {
+ Event::handle('CheckSchema');
+ }
+
+ // Give plugins a chance to initialize in a fully-prepared environment
+ Event::handle('InitializePlugin');
+ }
+
+ /**
+ * Quick-check if configuration has been established.
+ * Useful for functions which may get used partway through
+ * initialization to back off from fancier things.
+ *
+ * @return bool
+ */
+ public function haveConfig()
+ {
+ return self::$have_config;
+ }
+
+ /**
+ * Build default configuration array
+ * @return array
+ */
+ protected static function defaultConfig()
+ {
+ global $_server, $_path;
+ require(INSTALLDIR.'/lib/default.php');
+ return $default;
+ }
+
+ /**
+ * Establish default configuration based on given or default server and path
+ * Sets global $_server, $_path, and $config
+ */
+ protected static function initDefaults($server, $path)
+ {
+ global $_server, $_path, $config;
+
+ Event::clearHandlers();
+
+ // try to figure out where we are. $server and $path
+ // can be set by including module, else we guess based
+ // on HTTP info.
+
+ if (isset($server)) {
+ $_server = $server;
+ } else {
+ $_server = array_key_exists('SERVER_NAME', $_SERVER) ?
+ strtolower($_SERVER['SERVER_NAME']) :
+ null;
+ }
+
+ if (isset($path)) {
+ $_path = $path;
+ } else {
+ $_path = (array_key_exists('SERVER_NAME', $_SERVER) && array_key_exists('SCRIPT_NAME', $_SERVER)) ?
+ self::_sn_to_path($_SERVER['SCRIPT_NAME']) :
+ null;
+ }
+
+ // Set config values initially to default values
+ $default = self::defaultConfig();
+ $config = $default;
+
+ // default configuration, overwritten in config.php
+ // Keep DB_DataObject's db config synced to ours...
+
+ $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
+
+ $config['db'] = $default['db'];
+
+ // Backward compatibility
+
+ $config['site']['design'] =& $config['design'];
+
+ if (function_exists('date_default_timezone_set')) {
+ /* Work internally in UTC */
+ date_default_timezone_set('UTC');
+ }
+ }
+
+ protected function _sn_to_path($sn)
+ {
+ $past_root = substr($sn, 1);
+ $last_slash = strrpos($past_root, '/');
+ if ($last_slash > 0) {
+ $p = substr($past_root, 0, $last_slash);
+ } else {
+ $p = '';
+ }
+ return $p;
+ }
+
+ /**
+ * Load the default or specified configuration file.
+ * Modifies global $config and may establish plugins.
+ *
+ * @throws NoConfigException
+ */
+ protected function loadConfigFile($conffile=null)
+ {
+ global $_server, $_path, $config;
+
+ // From most general to most specific:
+ // server-wide, then vhost-wide, then for a path,
+ // finally for a dir (usually only need one of the last two).
+
+ if (isset($conffile)) {
+ $config_files = array($conffile);
+ } else {
+ $config_files = array('/etc/statusnet/statusnet.php',
+ '/etc/statusnet/laconica.php',
+ '/etc/laconica/laconica.php',
+ '/etc/statusnet/'.$_server.'.php',
+ '/etc/laconica/'.$_server.'.php');
+
+ if (strlen($_path) > 0) {
+ $config_files[] = '/etc/statusnet/'.$_server.'_'.$_path.'.php';
+ $config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
+ }
+
+ $config_files[] = INSTALLDIR.'/config.php';
+ }
+
+ self::$have_config = false;
+
+ foreach ($config_files as $_config_file) {
+ if (@file_exists($_config_file)) {
+ include($_config_file);
+ self::$have_config = true;
+ }
+ }
+
+ if (!self::$have_config) {
+ throw new NoConfigException("No configuration file found.",
+ $config_files);
+ }
+
+ // Fixup for statusnet.ini
+ $_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1);
+
+ if ($_db_name != 'statusnet' && !array_key_exists('ini_'.$_db_name, $config['db'])) {
+ $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/statusnet.ini';
+ }
+
+ // Backwards compatibility
+
+ if (array_key_exists('memcached', $config)) {
+ if ($config['memcached']['enabled']) {
+ addPlugin('Memcache', array('servers' => $config['memcached']['server']));
+ }
+
+ if (!empty($config['memcached']['base'])) {
+ $config['cache']['base'] = $config['memcached']['base'];
+ }
+ }
+ }
+}
+
+class NoConfigException extends Exception
+{
+ public $config_files;
+
+ function __construct($msg, $config_files) {
+ parent::__construct($msg);
+ $this->config_files = $config_files;
+ }
+}
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
index f059b42f0..3090e0bfb 100644
--- a/lib/stompqueuemanager.php
+++ b/lib/stompqueuemanager.php
@@ -30,46 +30,53 @@
require_once 'Stomp.php';
-class LiberalStomp extends Stomp
-{
- function getSocket()
- {
- return $this->_socket;
- }
-}
-class StompQueueManager
+class StompQueueManager extends QueueManager
{
var $server = null;
var $username = null;
var $password = null;
var $base = null;
var $con = null;
+
+ protected $master = null;
+ protected $sites = array();
function __construct()
{
+ parent::__construct();
$this->server = common_config('queue', 'stomp_server');
$this->username = common_config('queue', 'stomp_username');
$this->password = common_config('queue', 'stomp_password');
$this->base = common_config('queue', 'queue_basename');
}
- function _connect()
+ /**
+ * Tell the i/o master we only need a single instance to cover
+ * all sites running in this process.
+ */
+ public static function multiSite()
{
- if (empty($this->con)) {
- $this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'...");
- $this->con = new LiberalStomp($this->server);
+ return IoManager::INSTANCE_PER_PROCESS;
+ }
- if ($this->con->connect($this->username, $this->password)) {
- $this->_log(LOG_INFO, "Connected.");
- } else {
- $this->_log(LOG_ERR, 'Failed to connect to queue server');
- throw new ServerException('Failed to connect to queue server');
- }
- }
+ /**
+ * Record each site we'll be handling input for in this process,
+ * so we can listen to the necessary queues for it.
+ *
+ * @fixme possibly actually do subscription here to save another
+ * loop over all sites later?
+ */
+ public function addSite($server)
+ {
+ $this->sites[] = $server;
}
- function enqueue($object, $queue)
+ /**
+ * Saves a notice object reference into the queue item table.
+ * @return boolean true on success
+ */
+ public function enqueue($object, $queue)
{
$notice = $object;
@@ -77,7 +84,7 @@ class StompQueueManager
// XXX: serialize and send entire notice
- $result = $this->con->send($this->_queueName($queue),
+ $result = $this->con->send($this->queueName($queue),
$notice->id, // BODY of the message
array ('created' => $notice->created));
@@ -88,78 +95,212 @@ class StompQueueManager
common_log(LOG_DEBUG, 'complete remote queueing notice ID = '
. $notice->id . ' for ' . $queue);
+ $this->stats('enqueued', $queue);
}
- function service($queue, $handler)
+ /**
+ * Send any sockets we're listening on to the IO manager
+ * to wait for input.
+ *
+ * @return array of resources
+ */
+ public function getSockets()
{
- $result = null;
-
- $this->_connect();
+ return array($this->con->getSocket());
+ }
- $this->con->setReadTimeout($handler->timeout());
+ /**
+ * We've got input to handle on our socket!
+ * Read any waiting Stomp frame(s) and process them.
+ *
+ * @param resource $socket
+ * @return boolean ok on success
+ */
+ public function handleInput($socket)
+ {
+ assert($socket === $this->con->getSocket());
+ $ok = true;
+ $frames = $this->con->readFrames();
+ foreach ($frames as $frame) {
+ $ok = $ok && $this->_handleNotice($frame);
+ }
+ return $ok;
+ }
- $this->con->subscribe($this->_queueName($queue));
+ /**
+ * Initialize our connection and subscribe to all the queues
+ * we're going to need to handle...
+ *
+ * Side effects: in multi-site mode, may reset site configuration.
+ *
+ * @param IoMaster $master process/event controller
+ * @return bool return false on failure
+ */
+ public function start($master)
+ {
+ parent::start($master);
+ if ($this->sites) {
+ foreach ($this->sites as $server) {
+ StatusNet::init($server);
+ $this->doSubscribe();
+ }
+ } else {
+ $this->doSubscribe();
+ }
+ return true;
+ }
+
+ /**
+ * Subscribe to all the queues we're going to need to handle...
+ *
+ * Side effects: in multi-site mode, may reset site configuration.
+ *
+ * @return bool return false on failure
+ */
+ public function finish()
+ {
+ if ($this->sites) {
+ foreach ($this->sites as $server) {
+ StatusNet::init($server);
+ $this->doUnsubscribe();
+ }
+ } else {
+ $this->doUnsubscribe();
+ }
+ return true;
+ }
+
+ /**
+ * Lazy open connection to Stomp queue server.
+ */
+ protected function _connect()
+ {
+ if (empty($this->con)) {
+ $this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'...");
+ $this->con = new LiberalStomp($this->server);
- while (true) {
+ if ($this->con->connect($this->username, $this->password)) {
+ $this->_log(LOG_INFO, "Connected.");
+ } else {
+ $this->_log(LOG_ERR, 'Failed to connect to queue server');
+ throw new ServerException('Failed to connect to queue server');
+ }
+ }
+ }
- // Wait for something on one of our sockets
+ /**
+ * Subscribe to all enabled notice queues for the current site.
+ */
+ protected function doSubscribe()
+ {
+ $this->_connect();
+ foreach ($this->getQueues() as $queue) {
+ $rawqueue = $this->queueName($queue);
+ $this->_log(LOG_INFO, "Subscribing to $rawqueue");
+ $this->con->subscribe($rawqueue);
+ }
+ }
+
+ /**
+ * Subscribe from all enabled notice queues for the current site.
+ */
+ protected function doUnsubscribe()
+ {
+ $this->_connect();
+ foreach ($this->getQueues() as $queue) {
+ $this->con->unsubscribe($this->queueName($queue));
+ }
+ }
- $stompsock = $this->con->getSocket();
+ /**
+ * Handle and acknowledge a notice event that's come in through a queue.
+ *
+ * If the queue handler reports failure, the message is requeued for later.
+ * Missing notices or handler classes will drop the message.
+ *
+ * Side effects: in multi-site mode, may reset site configuration to
+ * match the site that queued the event.
+ *
+ * @param StompFrame $frame
+ * @return bool
+ */
+ protected function _handleNotice($frame)
+ {
+ list($site, $queue) = $this->parseDestination($frame->headers['destination']);
+ if ($site != common_config('site', 'server')) {
+ $this->stats('switch');
+ StatusNet::init($site);
+ }
- $handsocks = $handler->getSockets();
+ $id = intval($frame->body);
+ $info = "notice $id posted at {$frame->headers['created']} in queue $queue";
- $socks = array_merge(array($stompsock), $handsocks);
+ $notice = Notice::staticGet('id', $id);
+ if (empty($notice)) {
+ $this->_log(LOG_WARNING, "Skipping missing $info");
+ $this->con->ack($frame);
+ $this->stats('badnotice', $queue);
+ return false;
+ }
- $read = $socks;
- $write = array();
- $except = array();
+ $handler = $this->getHandler($queue);
+ if (!$handler) {
+ $this->_log(LOG_ERROR, "Missing handler class; skipping $info");
+ $this->con->ack($frame);
+ $this->stats('badhandler', $queue);
+ return false;
+ }
- $ready = stream_select($read, $write, $except, $handler->timeout(), 0);
+ $ok = $handler->handle_notice($notice);
- if ($ready === false) {
- $this->_log(LOG_ERR, "Error selecting on sockets");
- } else if ($ready > 0) {
- if (in_array($stompsock, $read)) {
- $this->_handleNotice($queue, $handler);
- }
- $handler->idle(QUEUE_HANDLER_HIT_IDLE);
- }
+ if (!$ok) {
+ $this->_log(LOG_WARNING, "Failed handling $info");
+ // FIXME we probably shouldn't have to do
+ // this kind of queue management ourselves;
+ // if we don't ack, it should resend...
+ $this->con->ack($frame);
+ $this->enqueue($notice, $queue);
+ $this->stats('requeued', $queue);
+ return false;
}
- $this->con->unsubscribe($this->_queueName($queue));
+ $this->_log(LOG_INFO, "Successfully handled $info");
+ $this->con->ack($frame);
+ $this->stats('handled', $queue);
+ return true;
}
- function _handleNotice($queue, $handler)
+ /**
+ * Combines the queue_basename from configuration with the
+ * site server name and queue name to give eg:
+ *
+ * /queue/statusnet/identi.ca/sms
+ *
+ * @param string $queue
+ * @return string
+ */
+ protected function queueName($queue)
{
- $frame = $this->con->readFrame();
-
- if (!empty($frame)) {
- $notice = Notice::staticGet('id', $frame->body);
-
- if (empty($notice)) {
- $this->_log(LOG_WARNING, 'Got ID '. $frame->body .' for non-existent notice in queue '. $queue);
- $this->con->ack($frame);
- } else {
- if ($handler->handle_notice($notice)) {
- $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
- $this->con->ack($frame);
- } else {
- $this->_log(LOG_WARNING, 'Failed handling notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue);
- // FIXME we probably shouldn't have to do
- // this kind of queue management ourselves
- $this->con->ack($frame);
- $this->enqueue($notice, $queue);
- }
- unset($notice);
- }
-
- unset($frame);
- }
+ return common_config('queue', 'queue_basename') .
+ common_config('site', 'server') . '/' . $queue;
}
- function _queueName($queue)
+ /**
+ * Returns the site and queue name from the server-side queue.
+ *
+ * @param string queue destination (eg '/queue/statusnet/identi.ca/sms')
+ * @return array of site and queue: ('identi.ca','sms') or false if unrecognized
+ */
+ protected function parseDestination($dest)
{
- return common_config('queue', 'queue_basename') . $queue;
+ $prefix = common_config('queue', 'queue_basename');
+ if (substr($dest, 0, strlen($prefix)) == $prefix) {
+ $rest = substr($dest, strlen($prefix));
+ return explode("/", $rest, 2);
+ } else {
+ common_log(LOG_ERR, "Got a message from unrecognized stomp queue: $dest");
+ return array(false, false);
+ }
}
function _log($level, $msg)
@@ -167,3 +308,4 @@ class StompQueueManager
common_log($level, 'StompQueueManager: '.$msg);
}
}
+
diff --git a/lib/subs.php b/lib/subs.php
index 4b6b03967..5ac1a75a5 100644
--- a/lib/subs.php
+++ b/lib/subs.php
@@ -56,35 +56,44 @@ function subs_subscribe_to($user, $other)
return _('User has blocked you.');
}
- if (!$user->subscribeTo($other)) {
- return _('Could not subscribe.');
- return;
- }
+ try {
+ if (Event::handle('StartSubscribe', array($user, $other))) {
- subs_notify($other, $user);
+ if (!$user->subscribeTo($other)) {
+ return _('Could not subscribe.');
+ return;
+ }
- $cache = common_memcache();
+ subs_notify($other, $user);
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
- }
+ $cache = common_memcache();
- $profile = $user->getProfile();
+ if ($cache) {
+ $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
+ }
- $profile->blowSubscriptionsCount();
- $other->blowSubscribersCount();
+ $profile = $user->getProfile();
- if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) {
- if (!$other->subscribeTo($user)) {
- return _('Could not subscribe other to you.');
- }
- $cache = common_memcache();
+ $profile->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
+
+ if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) {
+ if (!$other->subscribeTo($user)) {
+ return _('Could not subscribe other to you.');
+ }
+ $cache = common_memcache();
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $other->id));
- }
+ if ($cache) {
+ $cache->delete(common_cache_key('user:notices_with_friends:' . $other->id));
+ }
- subs_notify($user, $other);
+ subs_notify($user, $other);
+ }
+
+ Event::handle('EndSubscribe', array($user, $other));
+ }
+ } catch (Exception $e) {
+ return $e->getMessage();
}
return true;
@@ -133,28 +142,37 @@ function subs_unsubscribe_to($user, $other)
return _('Couldn\'t delete self-subscription.');
}
- $sub = DB_DataObject::factory('subscription');
+ try {
+ if (Event::handle('StartUnsubscribe', array($user, $other))) {
- $sub->subscriber = $user->id;
- $sub->subscribed = $other->id;
+ $sub = DB_DataObject::factory('subscription');
- $sub->find(true);
+ $sub->subscriber = $user->id;
+ $sub->subscribed = $other->id;
- // note we checked for existence above
+ $sub->find(true);
- if (!$sub->delete())
- return _('Couldn\'t delete subscription.');
+ // note we checked for existence above
- $cache = common_memcache();
+ if (!$sub->delete())
+ return _('Couldn\'t delete subscription.');
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
- }
+ $cache = common_memcache();
- $profile = $user->getProfile();
+ if ($cache) {
+ $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
+ }
- $profile->blowSubscriptionsCount();
- $other->blowSubscribersCount();
+ $profile = $user->getProfile();
+
+ $profile->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
+
+ Event::handle('EndUnsubscribe', array($user, $other));
+ }
+ } catch (Exception $e) {
+ return $e->getMessage();
+ }
return true;
}
diff --git a/lib/subscriptionlist.php b/lib/subscriptionlist.php
index 89f63e321..e1207774f 100644
--- a/lib/subscriptionlist.php
+++ b/lib/subscriptionlist.php
@@ -123,7 +123,7 @@ class SubscriptionListItem extends ProfileListItem
}
$this->out->elementEnd('ul');
} else {
- $this->out->text(_('(none)'));
+ $this->out->text(_('(None)'));
}
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php
index 72dbc4eed..5595eac05 100644
--- a/lib/unqueuemanager.php
+++ b/lib/unqueuemanager.php
@@ -23,57 +23,35 @@
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
+ * @author Brion Vibber <brion@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
-class UnQueueManager
+class UnQueueManager extends QueueManager
{
+
+ /**
+ * Dummy queue storage manager: instead of saving events for later,
+ * we just process them immediately. This is only suitable for events
+ * that can be processed quickly and don't need polling or long-running
+ * connections to another server such as XMPP.
+ *
+ * @param Notice $object
+ * @param string $queue
+ */
function enqueue($object, $queue)
{
$notice = $object;
-
- switch ($queue)
- {
- case 'omb':
- if ($this->_isLocal($notice)) {
- require_once(INSTALLDIR.'/lib/omb.php');
- omb_broadcast_notice($notice);
- }
- break;
- case 'public':
- if ($this->_isLocal($notice)) {
- require_once(INSTALLDIR.'/lib/jabber.php');
- jabber_public_notice($notice);
- }
- break;
- case 'ping':
- if ($this->_isLocal($notice)) {
- require_once INSTALLDIR . '/lib/ping.php';
- return ping_broadcast_notice($notice);
- }
- case 'sms':
- require_once(INSTALLDIR.'/lib/mail.php');
- mail_broadcast_notice_sms($notice);
- break;
- case 'jabber':
- require_once(INSTALLDIR.'/lib/jabber.php');
- jabber_broadcast_notice($notice);
- break;
- case 'plugin':
- Event::handle('HandleQueuedNotice', array(&$notice));
- break;
- default:
+
+ $handler = $this->getHandler($queue);
+ if ($handler) {
+ $handler->handle_notice($notice);
+ } else {
if (Event::handle('UnqueueHandleNotice', array(&$notice, $queue))) {
throw new ServerException("UnQueueManager: Unknown queue: $queue");
}
}
}
-
- function _isLocal($notice)
- {
- return ($notice->is_local == Notice::LOCAL_PUBLIC ||
- $notice->is_local == Notice::LOCAL_NONPUBLIC);
- }
-} \ No newline at end of file
+}
diff --git a/lib/util.php b/lib/util.php
index 00d1ab557..ef8a5d1f0 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -171,10 +171,19 @@ function common_ensure_session()
if (common_config('sessions', 'handle')) {
Session::setSaveHandler();
}
+ if (array_key_exists(session_name(), $_GET)) {
+ $id = $_GET[session_name()];
+ } else if (array_key_exists(session_name(), $_COOKIE)) {
+ $id = $_COOKIE[session_name()];
+ }
+ if (isset($id)) {
+ session_id($id);
+ setcookie(session_name(), $id);
+ }
@session_start();
if (!isset($_SESSION['started'])) {
$_SESSION['started'] = time();
- if (!empty($c)) {
+ if (!empty($id)) {
common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' .
' is set but started value is null');
}
@@ -246,7 +255,6 @@ function common_rememberme($user=null)
if (!$user) {
$user = common_current_user();
if (!$user) {
- common_debug('No current user to remember', __FILE__);
return false;
}
}
@@ -264,14 +272,11 @@ function common_rememberme($user=null)
if (!$result) {
common_log_db_error($rm, 'INSERT', __FILE__);
- common_debug('Error adding rememberme record for ' . $user->nickname, __FILE__);
return false;
}
$rm->query('COMMIT');
- common_debug('Inserted rememberme record (' . $rm->code . ', ' . $rm->user_id . '); result = ' . $result . '.', __FILE__);
-
$cookieval = $rm->user_id . ':' . $rm->code;
common_log(LOG_INFO, 'adding rememberme cookie "' . $cookieval . '" for ' . $user->nickname);
@@ -379,8 +384,6 @@ function common_current_user()
$_cur = common_remembered_user();
if ($_cur) {
- common_debug("Got User " . $_cur->nickname);
- common_debug("Faking session on remembered user");
// XXX: Is this necessary?
$_SESSION['userid'] = $_cur->id;
}
@@ -814,20 +817,50 @@ function common_path($relative, $ssl=false)
} else if (common_config('site', 'server')) {
$serverpart = common_config('site', 'server');
} else {
- common_log(LOG_ERR, 'Site Sever not configured, unable to determine site name.');
+ common_log(LOG_ERR, 'Site server not configured, unable to determine site name.');
}
} else {
$proto = 'http';
if (common_config('site', 'server')) {
$serverpart = common_config('site', 'server');
} else {
- common_log(LOG_ERR, 'Site Sever not configured, unable to determine site name.');
+ common_log(LOG_ERR, 'Site server not configured, unable to determine site name.');
}
}
+ $relative = common_inject_session($relative, $serverpart);
+
return $proto.'://'.$serverpart.'/'.$pathpart.$relative;
}
+function common_inject_session($url, $serverpart = null)
+{
+ if (common_have_session()) {
+
+ if (empty($serverpart)) {
+ $serverpart = parse_url($url, PHP_URL_HOST);
+ }
+
+ $currentServer = $_SERVER['HTTP_HOST'];
+
+ // Are we pointing to another server (like an SSL server?)
+
+ if (!empty($currentServer) &&
+ 0 != strcasecmp($currentServer, $serverpart)) {
+ // Pass the session ID as a GET parameter
+ $sesspart = session_name() . '=' . session_id();
+ $i = strpos($url, '?');
+ if ($i === false) { // no GET params, just append
+ $url .= '?' . $sesspart;
+ } else {
+ $url = substr($url, 0, $i + 1).$sesspart.'&'.substr($url, $i + 1);
+ }
+ }
+ }
+
+ return $url;
+}
+
function common_date_string($dt)
{
// XXX: do some sexy date formatting
@@ -1015,7 +1048,12 @@ function common_profile_url($nickname)
function common_root_url($ssl=false)
{
- return common_path('', $ssl);
+ $url = common_path('', $ssl);
+ $i = strpos($url, '?');
+ if ($i !== false) {
+ $url = substr($url, 0, $i);
+ }
+ return $url;
}
// returns $bytes bytes of random data as a hexadecimal string
@@ -1090,8 +1128,9 @@ function common_log_line($priority, $msg)
function common_request_id()
{
$pid = getmypid();
+ $server = common_config('site', 'server');
if (php_sapi_name() == 'cli') {
- return $pid;
+ return "$server:$pid";
} else {
static $req_id = null;
if (!isset($req_id)) {
@@ -1101,7 +1140,7 @@ function common_request_id()
$url = $_SERVER['REQUEST_URI'];
}
$method = $_SERVER['REQUEST_METHOD'];
- return "$pid.$req_id $method $url";
+ return "$server:$pid.$req_id $method $url";
}
}
@@ -1409,41 +1448,17 @@ function common_session_token()
function common_cache_key($extra)
{
- $base_key = common_config('memcached', 'base');
-
- if (empty($base_key)) {
- $base_key = common_keyize(common_config('site', 'name'));
- }
-
- return 'statusnet:' . $base_key . ':' . $extra;
+ return Cache::key($extra);
}
function common_keyize($str)
{
- $str = strtolower($str);
- $str = preg_replace('/\s/', '_', $str);
- return $str;
+ return Cache::keyize($str);
}
function common_memcache()
{
- static $cache = null;
- if (!common_config('memcached', 'enabled')) {
- return null;
- } else {
- if (!$cache) {
- $cache = new Memcache();
- $servers = common_config('memcached', 'server');
- if (is_array($servers)) {
- foreach($servers as $server) {
- $cache->addServer($server);
- }
- } else {
- $cache->addServer($servers);
- }
- }
- return $cache;
- }
+ return Cache::instance();
}
function common_license_terms($uri)
diff --git a/lib/xmppconfirmmanager.php b/lib/xmppconfirmmanager.php
new file mode 100644
index 000000000..ee4e294fd
--- /dev/null
+++ b/lib/xmppconfirmmanager.php
@@ -0,0 +1,168 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008-2010 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);
+}
+
+/**
+ * Event handler for pushing new confirmations to Jabber users.
+ * @fixme recommend redoing this on a queue-trigger model
+ * @fixme expiration of old items got dropped in the past, put it back?
+ */
+class XmppConfirmManager extends IoManager
+{
+
+ /**
+ * @return mixed XmppConfirmManager, or false if unneeded
+ */
+ public static function get()
+ {
+ if (common_config('xmpp', 'enabled')) {
+ $site = common_config('site', 'server');
+ return new XmppConfirmManager();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tell the i/o master we need one instance for each supporting site
+ * being handled in this process.
+ */
+ public static function multiSite()
+ {
+ return IoManager::INSTANCE_PER_SITE;
+ }
+
+ function __construct()
+ {
+ $this->site = common_config('site', 'server');
+ }
+
+ /**
+ * 10 seconds? Really? That seems a bit frequent.
+ */
+ function pollInterval()
+ {
+ return 10;
+ }
+
+ /**
+ * Ping!
+ * @return boolean true if we found something
+ */
+ function poll()
+ {
+ $this->switchSite();
+ $confirm = $this->next_confirm();
+ if ($confirm) {
+ $this->handle_confirm($confirm);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function handle_confirm($confirm)
+ {
+ require_once INSTALLDIR . '/lib/jabber.php';
+
+ common_log(LOG_INFO, 'Sending confirmation for ' . $confirm->address);
+ $user = User::staticGet($confirm->user_id);
+ if (!$user) {
+ common_log(LOG_WARNING, 'Confirmation for unknown user ' . $confirm->user_id);
+ return;
+ }
+ $success = jabber_confirm_address($confirm->code,
+ $user->nickname,
+ $confirm->address);
+ if (!$success) {
+ common_log(LOG_ERR, 'Confirmation failed for ' . $confirm->address);
+ # Just let the claim age out; hopefully things work then
+ return;
+ } else {
+ common_log(LOG_INFO, 'Confirmation sent for ' . $confirm->address);
+ # Mark confirmation sent; need a dupe so we don't have the WHERE clause
+ $dupe = Confirm_address::staticGet('code', $confirm->code);
+ if (!$dupe) {
+ common_log(LOG_WARNING, 'Could not refetch confirm', __FILE__);
+ return;
+ }
+ $orig = clone($dupe);
+ $dupe->sent = $dupe->claimed;
+ $result = $dupe->update($orig);
+ if (!$result) {
+ common_log_db_error($dupe, 'UPDATE', __FILE__);
+ # Just let the claim age out; hopefully things work then
+ return;
+ }
+ }
+ return true;
+ }
+
+ protected function next_confirm()
+ {
+ $confirm = new Confirm_address();
+ $confirm->whereAdd('claimed IS null');
+ $confirm->whereAdd('sent IS null');
+ # XXX: eventually we could do other confirmations in the queue, too
+ $confirm->address_type = 'jabber';
+ $confirm->orderBy('modified DESC');
+ $confirm->limit(1);
+ if ($confirm->find(true)) {
+ common_log(LOG_INFO, 'Claiming confirmation for ' . $confirm->address);
+ # working around some weird DB_DataObject behaviour
+ $confirm->whereAdd(''); # clears where stuff
+ $original = clone($confirm);
+ $confirm->claimed = common_sql_now();
+ $result = $confirm->update($original);
+ if ($result) {
+ common_log(LOG_INFO, 'Succeeded in claim! '. $result);
+ return $confirm;
+ } else {
+ common_log(LOG_INFO, 'Failed in claim!');
+ return false;
+ }
+ }
+ return null;
+ }
+
+ protected function clear_old_confirm_claims()
+ {
+ $confirm = new Confirm();
+ $confirm->claimed = null;
+ $confirm->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
+ $confirm->update(DB_DATAOBJECT_WHEREADD_ONLY);
+ $confirm->free();
+ unset($confirm);
+ }
+
+ /**
+ * Make sure we're on the right site configuration
+ */
+ protected function switchSite()
+ {
+ if ($this->site != common_config('site', 'server')) {
+ common_log(LOG_DEBUG, __METHOD__ . ": switching to site $this->site");
+ $this->stats('switch');
+ StatusNet::init($this->site);
+ }
+ }
+}
diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php
new file mode 100644
index 000000000..9662e97d1
--- /dev/null
+++ b/lib/xmppmanager.php
@@ -0,0 +1,273 @@
+<?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); }
+
+/**
+ * XMPP background connection manager for XMPP-using queue handlers,
+ * allowing them to send outgoing messages on the right connection.
+ *
+ * Input is handled during socket select loop, keepalive pings during idle.
+ * Any incoming messages will be forwarded to the main XmppDaemon process,
+ * which handles direct user interaction.
+ *
+ * In a multi-site queuedaemon.php run, one connection will be instantiated
+ * for each site being handled by the current process that has XMPP enabled.
+ */
+
+class XmppManager extends IoManager
+{
+ protected $site = null;
+ protected $pingid = 0;
+ protected $lastping = null;
+
+ static protected $singletons = array();
+
+ const PING_INTERVAL = 120;
+
+ /**
+ * Fetch the singleton XmppManager for the current site.
+ * @return mixed XmppManager, or false if unneeded
+ */
+ public static function get()
+ {
+ if (common_config('xmpp', 'enabled')) {
+ $site = common_config('site', 'server');
+ if (empty(self::$singletons[$site])) {
+ self::$singletons[$site] = new XmppManager();
+ }
+ return self::$singletons[$site];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tell the i/o master we need one instance for each supporting site
+ * being handled in this process.
+ */
+ public static function multiSite()
+ {
+ return IoManager::INSTANCE_PER_SITE;
+ }
+
+ function __construct()
+ {
+ $this->site = common_config('site', 'server');
+ }
+
+ /**
+ * Initialize connection to server.
+ * @return boolean true on success
+ */
+ public function start($master)
+ {
+ parent::start($master);
+ $this->switchSite();
+
+ require_once "lib/jabber.php";
+
+ # Low priority; we don't want to receive messages
+
+ common_log(LOG_INFO, "INITIALIZE");
+ $this->conn = jabber_connect($this->resource());
+
+ if (empty($this->conn)) {
+ common_log(LOG_ERR, "Couldn't connect to server.");
+ return false;
+ }
+
+ $this->conn->addEventHandler('message', 'forward_message', $this);
+ $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this);
+ $this->conn->setReconnectTimeout(600);
+ jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', -1);
+
+ return !is_null($this->conn);
+ }
+
+ /**
+ * Message pump is triggered on socket input, so we only need an idle()
+ * call often enough to trigger our outgoing pings.
+ */
+ function timeout()
+ {
+ return self::PING_INTERVAL;
+ }
+
+ /**
+ * Lists the XMPP connection socket to allow i/o master to wake
+ * when input comes in here as well as from the queue source.
+ *
+ * @return array of resources
+ */
+ public function getSockets()
+ {
+ return array($this->conn->getSocket());
+ }
+
+ /**
+ * Process XMPP events that have come in over the wire.
+ * Side effects: may switch site configuration
+ * @fixme may kill process on XMPP error
+ * @param resource $socket
+ */
+ public function handleInput($socket)
+ {
+ $this->switchSite();
+
+ # Process the queue for as long as needed
+ try {
+ if ($this->conn) {
+ assert($socket === $this->conn->getSocket());
+
+ common_log(LOG_DEBUG, "Servicing the XMPP queue.");
+ $this->stats('xmpp_process');
+ $this->conn->processTime(0);
+ }
+ } catch (XMPPHP_Exception $e) {
+ common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+ die($e->getMessage());
+ }
+ }
+
+ /**
+ * Idle processing for io manager's execution loop.
+ * Send keepalive pings to server.
+ *
+ * Side effect: kills process on exception from XMPP library.
+ *
+ * @fixme non-dying error handling
+ */
+ public function idle($timeout=0)
+ {
+ if ($this->conn) {
+ $now = time();
+ if (empty($this->lastping) || $now - $this->lastping > self::PING_INTERVAL) {
+ $this->switchSite();
+ try {
+ $this->sendPing();
+ $this->lastping = $now;
+ } catch (XMPPHP_Exception $e) {
+ common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
+ die($e->getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Send a keepalive ping to the XMPP server.
+ */
+ protected function sendPing()
+ {
+ $jid = jabber_daemon_address().'/'.$this->resource();
+ $server = common_config('xmpp', 'server');
+
+ if (!isset($this->pingid)) {
+ $this->pingid = 0;
+ } else {
+ $this->pingid++;
+ }
+
+ common_log(LOG_DEBUG, "Sending ping #{$this->pingid}");
+
+ $this->conn->send("<iq from='{$jid}' to='{$server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
+ }
+
+ /**
+ * Callback for Jabber reconnect event
+ * @param $pl
+ */
+ function handle_reconnect(&$pl)
+ {
+ common_log(LOG_NOTICE, 'XMPP reconnected');
+
+ $this->conn->processUntil('session_start');
+ $this->conn->presence(null, 'available', null, 'available', -1);
+ }
+
+ /**
+ * Callback for Jabber message event.
+ *
+ * This connection handles output; if we get a message straight to us,
+ * forward it on to our XmppDaemon listener for processing.
+ *
+ * @param $pl
+ */
+ function forward_message(&$pl)
+ {
+ if ($pl['type'] != 'chat') {
+ common_log(LOG_DEBUG, 'Ignoring message of type ' . $pl['type'] . ' from ' . $pl['from']);
+ return;
+ }
+ $listener = $this->listener();
+ if (strtolower($listener) == strtolower($pl['from'])) {
+ common_log(LOG_WARNING, 'Ignoring loop message.');
+ return;
+ }
+ common_log(LOG_INFO, 'Forwarding message from ' . $pl['from'] . ' to ' . $listener);
+ $this->conn->message($this->listener(), $pl['body'], 'chat', null, $this->ofrom($pl['from']));
+ }
+
+ /**
+ * Build an <addresses> block with an ofrom entry for forwarded messages
+ *
+ * @param string $from Jabber ID of original sender
+ * @return string XML fragment
+ */
+ protected function ofrom($from)
+ {
+ $address = "<addresses xmlns='http://jabber.org/protocol/address'>\n";
+ $address .= "<address type='ofrom' jid='$from' />\n";
+ $address .= "</addresses>\n";
+ return $address;
+ }
+
+ /**
+ * Build the complete JID of the XmppDaemon process which
+ * handles primary XMPP input for this site.
+ *
+ * @return string Jabber ID
+ */
+ protected function listener()
+ {
+ if (common_config('xmpp', 'listener')) {
+ return common_config('xmpp', 'listener');
+ } else {
+ return jabber_daemon_address() . '/' . common_config('xmpp','resource') . 'daemon';
+ }
+ }
+
+ protected function resource()
+ {
+ return 'queue' . posix_getpid(); // @fixme PIDs won't be host-unique
+ }
+
+ /**
+ * Make sure we're on the right site configuration
+ */
+ protected function switchSite()
+ {
+ if ($this->site != common_config('site', 'server')) {
+ common_log(LOG_DEBUG, __METHOD__ . ": switching to site $this->site");
+ $this->stats('switch');
+ StatusNet::init($this->site);
+ }
+ }
+}
diff --git a/lib/xmppqueuehandler.php b/lib/xmppqueuehandler.php
deleted file mode 100644
index f28fc9088..000000000
--- a/lib/xmppqueuehandler.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2008, 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-require_once(INSTALLDIR.'/lib/queuehandler.php');
-
-define('PING_INTERVAL', 120);
-
-/**
- * Common superclass for all XMPP-using queue handlers. They all need to
- * service their message queues on idle, and forward any incoming messages
- * to the XMPP listener connection. So, we abstract out common code to a
- * superclass.
- */
-
-class XmppQueueHandler extends QueueHandler
-{
- var $pingid = 0;
- var $lastping = null;
-
- function start()
- {
- # Low priority; we don't want to receive messages
-
- $this->log(LOG_INFO, "INITIALIZE");
- $this->conn = jabber_connect($this->_id.$this->transport());
-
- if (empty($this->conn)) {
- $this->log(LOG_ERR, "Couldn't connect to server.");
- return false;
- }
-
- $this->conn->addEventHandler('message', 'forward_message', $this);
- $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this);
- $this->conn->setReconnectTimeout(600);
- jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', -1);
-
- return !is_null($this->conn);
- }
-
- function timeout()
- {
- return 10;
- }
-
- function handle_reconnect(&$pl)
- {
- $this->log(LOG_NOTICE, 'reconnected');
-
- $this->conn->processUntil('session_start');
- $this->conn->presence(null, 'available', null, 'available', -1);
- }
-
- function idle($timeout=0)
- {
- # Process the queue for as long as needed
- try {
- if ($this->conn) {
- $this->log(LOG_DEBUG, "Servicing the XMPP queue.");
- $this->conn->processTime($timeout);
- $now = time();
- if (empty($this->lastping) || $now - $this->lastping > PING_INTERVAL) {
- $this->sendPing();
- $this->lastping = $now;
- }
- }
- } catch (XMPPHP_Exception $e) {
- $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage());
- die($e->getMessage());
- }
- }
-
- function sendPing()
- {
- $jid = jabber_daemon_address().'/'.$this->_id.$this->transport();
- $server = common_config('xmpp', 'server');
-
- if (!isset($this->pingid)) {
- $this->pingid = 0;
- } else {
- $this->pingid++;
- }
-
- $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}");
-
- $this->conn->send("<iq from='{$jid}' to='{$server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
- }
-
- function forward_message(&$pl)
- {
- if ($pl['type'] != 'chat') {
- $this->log(LOG_DEBUG, 'Ignoring message of type ' . $pl['type'] . ' from ' . $pl['from']);
- return;
- }
- $listener = $this->listener();
- if (strtolower($listener) == strtolower($pl['from'])) {
- $this->log(LOG_WARNING, 'Ignoring loop message.');
- return;
- }
- $this->log(LOG_INFO, 'Forwarding message from ' . $pl['from'] . ' to ' . $listener);
- $this->conn->message($this->listener(), $pl['body'], 'chat', null, $this->ofrom($pl['from']));
- }
-
- function ofrom($from)
- {
- $address = "<addresses xmlns='http://jabber.org/protocol/address'>\n";
- $address .= "<address type='ofrom' jid='$from' />\n";
- $address .= "</addresses>\n";
- return $address;
- }
-
- function listener()
- {
- if (common_config('xmpp', 'listener')) {
- return common_config('xmpp', 'listener');
- } else {
- return jabber_daemon_address() . '/' . common_config('xmpp','resource') . 'daemon';
- }
- }
-
- function getSockets()
- {
- return array($this->conn->getSocket());
- }
-}