diff options
279 files changed, 38942 insertions, 4615 deletions
diff --git a/.gitignore b/.gitignore index d84e5e742..bf491ec3c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ lac08.log php.log .DS_Store nbproject +*.mo diff --git a/EVENTS.txt b/EVENTS.txt index 41f67dd6e..9f98637bc 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -762,6 +762,24 @@ StartShowContentLicense: Showing the default license for content EndShowContentLicense: Showing the default license for content - $action: the current action +GetImTransports: Get IM transports that are available +- &$transports: append your transport to this array like so: $transports[transportName]=array('display'=>display) + +NormalizeImScreenname: Normalize an IM screenname +- $transport: transport the screenname is on +- &$screenname: screenname to be normalized + +ValidateImScreenname: Validate an IM screenname +- $transport: transport the screenname is on +- $screenname: screenname to be validated +- $valid: is the screenname valid? + +SendImConfirmationCode: Send a confirmation code to confirm a user owns an IM screenname +- $transport: transport the screenname exists on +- $screenname: screenname being confirmed +- $code: confirmation code for confirmation URL +- $user: user requesting the confirmation + StartUserRegister: When a new user is being registered - &$profile: new profile data (no ID) - &$user: new user account (no ID or URI) @@ -873,9 +873,7 @@ sslserver: use an alternate server name for SSL URLs, like parameters correctly so that both the SSL server and the "normal" server can access the session cookie and preferably other cookies as well. -shorturllength: Length of URL at which URLs in a message exceeding 140 - characters will be sent to the user's chosen - shortening service. +shorturllength: ignored. See 'url' section below. dupelimit: minimum time allowed for one person to say the same thing twice. Default 60s. Anything lower is considered a user or UI error. @@ -1538,6 +1536,22 @@ external: external links in notices. One of three values: 'sometimes', nofollowed on profile, notice, and favorites page. Default is 'sometimes'. +url +--- + +Everybody loves URL shorteners. These are some options for fine-tuning +how and when the server shortens URLs. + +shortener: URL shortening service to use by default. Users can override + individually. 'ur1.ca' by default. +maxlength: If an URL is strictly longer than this limit, it will be + shortened. Note that the URL shortener service may return an + URL longer than this limit. Defaults to 25. Users can + override. If set to 0, all URLs will be shortened. +maxnoticelength: If a notice is strictly longer than this limit, all + URLs in the notice will be shortened. Users can override. + -1 means the text limit for notices. + router ------ @@ -1550,6 +1564,7 @@ cache: whether to cache the router in memcache (or another caching router cached) or others who see strange behavior. You're unlikely to need this unless you're a developer. + Plugins ======= diff --git a/actions/apiaccountupdatedeliverydevice.php b/actions/apiaccountupdatedeliverydevice.php index ec87c4c95..e7d44dc44 100644 --- a/actions/apiaccountupdatedeliverydevice.php +++ b/actions/apiaccountupdatedeliverydevice.php @@ -116,10 +116,16 @@ class ApiAccountUpdateDeliveryDeviceAction extends ApiAuthAction if (strtolower($this->device) == 'sms') { $this->user->smsnotify = true; } elseif (strtolower($this->device) == 'im') { - $this->user->jabbernotify = true; + //TODO IM is pluginized now, so what should we do? + //Enable notifications for all IM plugins? + //For now, don't do anything + //$this->user->jabbernotify = true; } elseif (strtolower($this->device == 'none')) { $this->user->smsnotify = false; - $this->user->jabbernotify = false; + //TODO IM is pluginized now, so what should we do? + //Disable notifications for all IM plugins? + //For now, don't do anything + //$this->user->jabbernotify = false; } $result = $this->user->update($original); diff --git a/actions/apigroupprofileupdate.php b/actions/apigroupprofileupdate.php new file mode 100644 index 000000000..6ac4b5a4b --- /dev/null +++ b/actions/apigroupprofileupdate.php @@ -0,0 +1,367 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Update a group's profile + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category API + * @package StatusNet + * @author Zach Copley <zach@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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/apiauth.php'; + +/** + * API analog to the group edit page + * + * @category API + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiGroupProfileUpdateAction extends ApiAuthAction +{ + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + + $this->nickname = common_canonical_nickname($this->trimmed('nickname')); + + $this->fullname = $this->trimmed('fullname'); + $this->homepage = $this->trimmed('homepage'); + $this->description = $this->trimmed('description'); + $this->location = $this->trimmed('location'); + $this->aliasstring = $this->trimmed('aliases'); + + $this->user = $this->auth_user; + $this->group = $this->getTargetGroup($this->arg('id')); + + return true; + } + + /** + * Handle the request + * + * See which request params have been set, and update the profile + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError( + _('This method requires a POST.'), + 400, $this->format + ); + return; + } + + if (!in_array($this->format, array('xml', 'json'))) { + $this->clientError( + _('API method not found.'), + 404, + $this->format + ); + return; + } + + if (empty($this->user)) { + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + if (empty($this->group)) { + $this->clientError(_('Group not found.'), 404, $this->format); + return false; + } + + if (!$this->user->isAdmin($this->group)) { + $this->clientError(_('You must be an admin to edit the group.'), 403); + return false; + } + + $this->group->query('BEGIN'); + + $orig = clone($this->group); + + try { + + if (!empty($this->nickname)) { + if ($this->validateNickname()) { + $this->group->nickname = $this->nickname; + $this->group->mainpage = common_local_url( + 'showgroup', + array('nickname' => $this->nickname) + ); + } + } + + if (!empty($this->fullname)) { + $this->validateFullname(); + $this->group->fullname = $this->fullname; + } + + if (!empty($this->homepage)) { + $this->validateHomepage(); + $this->group->homepage = $this->hompage; + } + + if (!empty($this->description)) { + $this->validateDescription(); + $this->group->description = $this->decription; + } + + if (!empty($this->location)) { + $this->validateLocation(); + $this->group->location = $this->location; + } + + } catch (ApiValidationException $ave) { + $this->clientError( + $ave->getMessage(), + 403, + $this->format + ); + return; + } + + $result = $this->group->update($orig); + + if (!$result) { + common_log_db_error($this->group, 'UPDATE', __FILE__); + $this->serverError(_('Could not update group.')); + } + + $aliases = array(); + + try { + + if (!empty($this->aliasstring)) { + $aliases = $this->validateAliases(); + } + + } catch (ApiValidationException $ave) { + $this->clientError( + $ave->getMessage(), + 403, + $this->format + ); + return; + } + + $result = $this->group->setAliases($aliases); + + if (!$result) { + $this->serverError(_('Could not create aliases.')); + } + + if (!empty($this->nickname) && ($this->nickname != $orig->nickname)) { + common_log(LOG_INFO, "Saving local group info."); + $local = Local_group::staticGet('group_id', $this->group->id); + $local->setNickname($this->nickname); + } + + $this->group->query('COMMIT'); + + switch($this->format) { + case 'xml': + $this->showSingleXmlGroup($this->group); + break; + case 'json': + $this->showSingleJsonGroup($this->group); + break; + default: + $this->clientError(_('API method not found.'), 404, $this->format); + break; + } + } + + function nicknameExists($nickname) + { + $group = Local_group::staticGet('nickname', $nickname); + + if (!empty($group) && + $group->group_id != $this->group->id) { + return true; + } + + $alias = Group_alias::staticGet('alias', $nickname); + + if (!empty($alias) && + $alias->group_id != $this->group->id) { + return true; + } + + return false; + } + + function validateNickname() + { + if (!Validate::string( + $this->nickname, array( + 'min_length' => 1, + 'max_length' => 64, + 'format' => NICKNAME_FMT + ) + ) + ) { + throw new ApiValidationException( + _( + 'Nickname must have only lowercase letters ' . + 'and numbers and no spaces.' + ) + ); + } else if ($this->nicknameExists($this->nickname)) { + throw new ApiValidationException( + _('Nickname already in use. Try another one.') + ); + } else if (!User_group::allowedNickname($this->nickname)) { + throw new ApiValidationException( + _('Not a valid nickname.') + ); + } + + return true; + } + + function validateHomepage() + { + if (!is_null($this->homepage) + && (strlen($this->homepage) > 0) + && !Validate::uri( + $this->homepage, + array('allowed_schemes' => array('http', 'https') + ) + ) + ) { + throw new ApiValidationException( + _('Homepage is not a valid URL.') + ); + } + } + + function validateFullname() + { + if (!is_null($this->fullname) && mb_strlen($this->fullname) > 255) { + throw new ApiValidationException( + _('Full name is too long (max 255 chars).') + ); + } + } + + function validateDescription() + { + if (User_group::descriptionTooLong($this->description)) { + throw new ApiValidationException( + sprintf( + _('description is too long (max %d chars).'), + User_group::maxDescription() + ) + ); + } + } + + function validateLocation() + { + if (!is_null($this->location) && mb_strlen($this->location) > 255) { + throw new ApiValidationException( + _('Location is too long (max 255 chars).') + ); + } + } + + function validateAliases() + { + $aliases = array_map( + 'common_canonical_nickname', + array_unique( + preg_split('/[\s,]+/', + $this->aliasstring + ) + ) + ); + + if (count($aliases) > common_config('group', 'maxaliases')) { + throw new ApiValidationException( + sprintf( + _('Too many aliases! Maximum %d.'), + common_config('group', 'maxaliases') + ) + ); + } + + foreach ($aliases as $alias) { + if (!Validate::string( + $alias, array( + 'min_length' => 1, + 'max_length' => 64, + 'format' => NICKNAME_FMT) + ) + ) { + throw new ApiValidationException( + sprintf( + _('Invalid alias: "%s"'), + $alias + ) + ); + } + + if ($this->nicknameExists($alias)) { + throw new ApiValidationException( + sprintf( + _('Alias "%s" already in use. Try another one.'), + $alias) + ); + } + + // XXX assumes alphanum nicknames + if (strcmp($alias, $this->nickname) == 0) { + throw new ApiValidationException( + _('Alias can\'t be the same as nickname.') + ); + } + } + + return $aliases; + } + +}
\ No newline at end of file diff --git a/actions/confirmaddress.php b/actions/confirmaddress.php index 5617c5339..fd04c3130 100644 --- a/actions/confirmaddress.php +++ b/actions/confirmaddress.php @@ -48,7 +48,7 @@ class ConfirmaddressAction extends Action { /** type of confirmation. */ - var $type = null; + var $address; /** * Accept a confirmation code @@ -87,40 +87,77 @@ class ConfirmaddressAction extends Action return; } $type = $confirm->address_type; - if (!in_array($type, array('email', 'jabber', 'sms'))) { - // TRANS: Server error for a unknow address type %s, which can be 'email', 'jabber', or 'sms'. - $this->serverError(sprintf(_('Unrecognized address type %s.'), $type)); + $transports = array(); + Event::handle('GetImTransports', array(&$transports)); + if (!in_array($type, array('email', 'sms')) && !in_array($type, array_keys($transports))) { + // TRANS: Server error for an unknown address type, which can be 'email', 'sms', or the name of an IM network (such as 'xmpp' or 'aim') + $this->serverError(sprintf(_('Unrecognized address type %s'), $type)); return; } - if ($cur->$type == $confirm->address) { - // TRANS: Client error for an already confirmed email/jabber/sms address. - $this->clientError(_('That address has already been confirmed.')); - return; - } - + $this->address = $confirm->address; $cur->query('BEGIN'); + if (in_array($type, array('email', 'sms'))) + { + if ($cur->$type == $confirm->address) { + // TRANS: Client error for an already confirmed email/jabber/sms address. + $this->clientError(_('That address has already been confirmed.')); + return; + } + + $orig_user = clone($cur); + + $cur->$type = $confirm->address; + + if ($type == 'sms') { + $cur->carrier = ($confirm->address_extra)+0; + $carrier = Sms_carrier::staticGet($cur->carrier); + $cur->smsemail = $carrier->toEmailAddress($cur->sms); + } + + $result = $cur->updateKeys($orig_user); + + if (!$result) { + common_log_db_error($cur, 'UPDATE', __FILE__); + $this->serverError(_('Couldn\'t update user.')); + return; + } + + if ($type == 'email') { + $cur->emailChanged(); + } + + } else { + + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->transport = $confirm->address_type; + $user_im_prefs->user_id = $cur->id; + if ($user_im_prefs->find() && $user_im_prefs->fetch()) { + if($user_im_prefs->screenname == $confirm->address){ + $this->clientError(_('That address has already been confirmed.')); + return; + } + $user_im_prefs->screenname = $confirm->address; + $result = $user_im_prefs->update(); + + if (!$result) { + common_log_db_error($user_im_prefs, 'UPDATE', __FILE__); + $this->serverError(_('Couldn\'t update user im preferences.')); + return; + } + }else{ + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->screenname = $confirm->address; + $user_im_prefs->transport = $confirm->address_type; + $user_im_prefs->user_id = $cur->id; + $result = $user_im_prefs->insert(); + + if (!$result) { + common_log_db_error($user_im_prefs, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t insert user im preferences.')); + return; + } + } - $orig_user = clone($cur); - - $cur->$type = $confirm->address; - - if ($type == 'sms') { - $cur->carrier = ($confirm->address_extra)+0; - $carrier = Sms_carrier::staticGet($cur->carrier); - $cur->smsemail = $carrier->toEmailAddress($cur->sms); - } - - $result = $cur->updateKeys($orig_user); - - if (!$result) { - common_log_db_error($cur, 'UPDATE', __FILE__); - // TRANS: Server error displayed when a user update to the database fails in the contact address confirmation action. - $this->serverError(_('Couldn\'t update user.')); - return; - } - - if ($type == 'email') { - $cur->emailChanged(); } $result = $confirm->delete(); @@ -134,8 +171,6 @@ class ConfirmaddressAction extends Action } $cur->query('COMMIT'); - - $this->type = $type; $this->showPage(); } @@ -158,13 +193,12 @@ class ConfirmaddressAction extends Action function showContent() { $cur = common_current_user(); - $type = $this->type; $this->element('p', null, // TRANS: Success message for the contact address confirmation action. // TRANS: %s can be 'email', 'jabber', or 'sms'. sprintf(_('The address "%s" has been '. 'confirmed for your account.'), - $cur->$type)); + $this->address)); } } diff --git a/actions/getfile.php b/actions/getfile.php index 9cbe8e1d9..a45506ce2 100644 --- a/actions/getfile.php +++ b/actions/getfile.php @@ -129,9 +129,9 @@ class GetfileAction extends Action return null; } - $cache = common_memcache(); + $cache = Cache::instance(); if($cache) { - $key = common_cache_key('attachments:etag:' . $this->path); + $key = Cache::key('attachments:etag:' . $this->path); $etag = $cache->get($key); if($etag === false) { $etag = crc32(file_get_contents($this->path)); diff --git a/actions/hostmeta.php b/actions/hostmeta.php index 2587bb6b9..331fc8a99 100644 --- a/actions/hostmeta.php +++ b/actions/hostmeta.php @@ -51,11 +51,11 @@ class HostMetaAction extends Action $xrd->host = $domain; if(Event::handle('StartHostMetaLinks', array(&$xrd->links))) { - $url = common_local_url('userxrd'); - $url.= '?uri={uri}'; - $xrd->links[] = array('rel' => Discovery::LRDD_REL, - 'template' => $url, - 'title' => array('Resource Descriptor')); + $url = common_local_url('userxrd'); + $url.= '?uri={uri}'; + $xrd->links[] = array('rel' => Discovery::LRDD_REL, + 'template' => $url, + 'title' => array('Resource Descriptor')); Event::handle('EndHostMetaLinks', array(&$xrd->links)); } diff --git a/actions/imsettings.php b/actions/imsettings.php index 29cbeb82e..1b1bc0dc0 100644 --- a/actions/imsettings.php +++ b/actions/imsettings.php @@ -31,9 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/connectsettingsaction.php'; -require_once INSTALLDIR.'/lib/jabber.php'; - /** * Settings for Jabber/XMPP integration * @@ -72,8 +69,8 @@ class ImsettingsAction extends ConnectSettingsAction // TRANS: [instant messages] is link text, "(%%doc.im%%)" is the link. // TRANS: the order and formatting of link text and link should remain unchanged. return _('You can send and receive notices through '. - 'Jabber/GTalk [instant messages](%%doc.im%%). '. - 'Configure your address and settings below.'); + 'instant messaging [instant messages](%%doc.im%%). '. + 'Configure your addresses and settings below.'); } /** @@ -88,105 +85,124 @@ class ImsettingsAction extends ConnectSettingsAction function showContent() { - if (!common_config('xmpp', 'enabled')) { + $transports = array(); + Event::handle('GetImTransports', array(&$transports)); + if (! $transports) { $this->element('div', array('class' => 'error'), - // TRANS: Message given in the IM settings if XMPP is not enabled on the site. + // TRANS: Message given in the IM settings if IM is not enabled on the site. _('IM is not available.')); return; } $user = common_current_user(); - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_im', - 'class' => 'form_settings', - 'action' => - common_local_url('imsettings'))); - $this->elementStart('fieldset', array('id' => 'settings_im_address')); - // TRANS: Form legend for IM settings form. - $this->element('legend', null, _('IM address')); - $this->hidden('token', common_session_token()); - - if ($user->jabber) { - $this->element('p', 'form_confirmed', $user->jabber); - // TRANS: Form note in IM settings form. - $this->element('p', 'form_note', - _('Current confirmed Jabber/GTalk address.')); - $this->hidden('jabber', $user->jabber); - // TRANS: Button label to remove a confirmed IM address. - $this->submit('remove', _m('BUTTON','Remove')); - } else { - $confirm = $this->getConfirmation(); - if ($confirm) { - $this->element('p', 'form_unconfirmed', $confirm->address); + + $user_im_prefs_by_transport = array(); + + foreach($transports as $transport=>$transport_info) + { + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_im', + 'class' => 'form_settings', + 'action' => + common_local_url('imsettings'))); + $this->elementStart('fieldset', array('id' => 'settings_im_address')); + // TRANS: Form legend for IM settings form. + $this->element('legend', null, $transport_info['display']); + $this->hidden('token', common_session_token()); + $this->hidden('transport', $transport); + + if ($user_im_prefs = User_im_prefs::pkeyGet( array('transport' => $transport, 'user_id' => $user->id) )) { + $user_im_prefs_by_transport[$transport] = $user_im_prefs; + $this->element('p', 'form_confirmed', $user_im_prefs->screenname); + // TRANS: Form note in IM settings form. $this->element('p', 'form_note', - // TRANS: Form note in IM settings form. - // TRANS: %s is the IM address set for the site. - sprintf(_('Awaiting confirmation on this address. '. - 'Check your Jabber/GTalk account for a '. - 'message with further instructions. '. - '(Did you add %s to your buddy list?)'), - jabber_daemon_address())); - $this->hidden('jabber', $confirm->address); - // TRANS: Button label to cancel an IM address confirmation procedure. - $this->submit('cancel', _m('BUTTON','Cancel')); + sprintf(_('Current confirmed %s address.'),$transport_info['display'])); + $this->hidden('screenname', $user_im_prefs->screenname); + // TRANS: Button label to remove a confirmed IM address. + $this->submit('remove', _m('BUTTON','Remove')); } else { - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - // TRANS: Field label for IM address input in IM settings form. - $this->input('jabber', _('IM address'), - ($this->arg('jabber')) ? $this->arg('jabber') : null, - // TRANS: IM address input field instructions in IM settings form. - // TRANS: %s is the IM address set for the site. - // TRANS: Do not translate "example.org". It is one of the domain names reserved for use in examples by - // TRANS: http://www.rfc-editor.org/rfc/rfc2606.txt. Any other domain may be owned by a legitimate - // TRANS: person or organization. - sprintf(_('Jabber or GTalk address, '. - 'like "UserName@example.org". '. - 'First, make sure to add %s to your '. - 'buddy list in your IM client or on GTalk.'), - jabber_daemon_address())); - $this->elementEnd('li'); - $this->elementEnd('ul'); - // TRANS: Button label for adding an IM address in IM settings form. - $this->submit('add', _m('BUTTON','Add')); + $confirm = $this->getConfirmation($transport); + if ($confirm) { + $this->element('p', 'form_unconfirmed', $confirm->address); + // TRANS: Form note in IM settings form. + $this->element('p', 'form_note', + // TRANS: Form note in IM settings form. + // TRANS: %s is the IM address set for the site. + sprintf(_('Awaiting confirmation on this address. '. + 'Check your %s account for a '. + 'message with further instructions. '. + '(Did you add %s to your buddy list?)'), + $transport_info['display'], + $transport_info['daemonScreenname'])); + $this->hidden('screenname', $confirm->address); + // TRANS: Button label to cancel an IM address confirmation procedure. + $this->submit('cancel', _m('BUTTON','Cancel')); + } else { + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->input('screenname', _('IM address'), + ($this->arg('screenname')) ? $this->arg('screenname') : null, + sprintf(_('%s screenname.'), + $transport_info['display'])); + $this->elementEnd('li'); + $this->elementEnd('ul'); + // TRANS: Button label for adding an IM address in IM settings form. + $this->submit('add', _m('BUTTON','Add')); + } } + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + if($user_im_prefs_by_transport) + { + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_im', + 'class' => 'form_settings', + 'action' => + common_local_url('imsettings'))); + $this->elementStart('fieldset', array('id' => 'settings_im_preferences')); + // TRANS: Header for IM preferences form. + $this->element('legend', null, _('IM Preferences')); + $this->hidden('token', common_session_token()); + $this->elementStart('table'); + $this->elementStart('tr'); + foreach($user_im_prefs_by_transport as $transport=>$user_im_prefs) + { + $this->element('th', null, $transports[$transport]['display']); + } + $this->elementEnd('tr'); + $preferences = array( + // TRANS: Checkbox label in IM preferences form. + array('name'=>'notify', 'description'=>_('Send me notices')), + // TRANS: Checkbox label in IM preferences form. + array('name'=>'updatefrompresence', 'description'=>_('Post a notice when my status changes.')), + // TRANS: Checkbox label in IM preferences form. + array('name'=>'replies', 'description'=>_('Send me replies '. + 'from people I\'m not subscribed to.')), + // TRANS: Checkbox label in IM preferences form. + array('name'=>'microid', 'description'=>_('Publish a MicroID')) + ); + foreach($preferences as $preference) + { + $this->elementStart('tr'); + foreach($user_im_prefs_by_transport as $transport=>$user_im_prefs) + { + $preference_name = $preference['name']; + $this->elementStart('td'); + $this->checkbox($transport . '_' . $preference['name'], + $preference['description'], + $user_im_prefs->$preference_name); + $this->elementEnd('td'); + } + $this->elementEnd('tr'); + } + $this->elementEnd('table'); + // TRANS: Button label to save IM preferences. + $this->submit('save', _m('BUTTON','Save')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); } - $this->elementEnd('fieldset'); - - $this->elementStart('fieldset', array('id' => 'settings_im_preferences')); - // TRANS: Form legend for IM preferences form. - $this->element('legend', null, _('IM preferences')); - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->checkbox('jabbernotify', - // TRANS: Checkbox label in IM preferences form. - _('Send me notices through Jabber/GTalk.'), - $user->jabbernotify); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('updatefrompresence', - // TRANS: Checkbox label in IM preferences form. - _('Post a notice when my Jabber/GTalk status changes.'), - $user->updatefrompresence); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('jabberreplies', - // TRANS: Checkbox label in IM preferences form. - _('Send me replies through Jabber/GTalk '. - 'from people I\'m not subscribed to.'), - $user->jabberreplies); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('jabbermicroid', - // TRANS: Checkbox label in IM preferences form. - _('Publish a MicroID for my Jabber/GTalk address.'), - $user->jabbermicroid); - $this->elementEnd('li'); - $this->elementEnd('ul'); - // TRANS: Button label to save IM preferences. - $this->submit('save', _m('BUTTON','Save')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); } /** @@ -195,14 +211,14 @@ class ImsettingsAction extends ConnectSettingsAction * @return Confirm_address address object for this user */ - function getConfirmation() + function getConfirmation($transport) { $user = common_current_user(); $confirm = new Confirm_address(); $confirm->user_id = $user->id; - $confirm->address_type = 'jabber'; + $confirm->address_type = $transport; if ($confirm->find(true)) { return $confirm; @@ -257,35 +273,33 @@ class ImsettingsAction extends ConnectSettingsAction function savePreferences() { - $jabbernotify = $this->boolean('jabbernotify'); - $updatefrompresence = $this->boolean('updatefrompresence'); - $jabberreplies = $this->boolean('jabberreplies'); - $jabbermicroid = $this->boolean('jabbermicroid'); - $user = common_current_user(); - assert(!is_null($user)); // should already be checked - - $user->query('BEGIN'); - - $original = clone($user); - - $user->jabbernotify = $jabbernotify; - $user->updatefrompresence = $updatefrompresence; - $user->jabberreplies = $jabberreplies; - $user->jabbermicroid = $jabbermicroid; - - $result = $user->update($original); - - if ($result === false) { - common_log_db_error($user, 'UPDATE', __FILE__); - // TRANS: Server error thrown on database error updating IM preferences. - $this->serverError(_('Couldn\'t update user.')); - return; + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->query('BEGIN'); + $user_im_prefs->user_id = $user->id; + if($user_im_prefs->find() && $user_im_prefs->fetch()) + { + $preferences = array('notify', 'updatefrompresence', 'replies', 'microid'); + do + { + $original = clone($user_im_prefs); + $new = clone($user_im_prefs); + foreach($preferences as $preference) + { + $new->$preference = $this->boolean($new->transport . '_' . $preference); + } + $result = $new->update($original); + + if ($result === false) { + common_log_db_error($user, 'UPDATE', __FILE__); + // TRANS: Server error thrown on database error updating IM preferences. + $this->serverError(_('Couldn\'t update IM preferences.')); + return; + } + }while($user_im_prefs->fetch()); } - - $user->query('COMMIT'); - + $user_im_prefs->query('COMMIT'); // TRANS: Confirmation message for successful IM preferences save. $this->showForm(_('Preferences saved.'), true); } @@ -294,7 +308,7 @@ class ImsettingsAction extends ConnectSettingsAction * Sends a confirmation to the address given * * Stores a confirmation record and sends out a - * Jabber message with the confirmation info. + * message with the confirmation info. * * @return void */ @@ -303,41 +317,45 @@ class ImsettingsAction extends ConnectSettingsAction { $user = common_current_user(); - $jabber = $this->trimmed('jabber'); + $screenname = $this->trimmed('screenname'); + $transport = $this->trimmed('transport'); // Some validation - if (!$jabber) { + if (!$screenname) { // TRANS: Message given saving IM address without having provided one. - $this->showForm(_('No Jabber ID.')); + $this->showForm(_('No screenname.')); return; } - $jabber = jabber_normalize_jid($jabber); + if (!$transport) { + $this->showForm(_('No transport.')); + return; + } - if (!$jabber) { + Event::handle('NormalizeImScreenname', array($transport, &$screenname)); + + if (!$screenname) { // TRANS: Message given saving IM address that cannot be normalised. - $this->showForm(_('Cannot normalize that Jabber ID')); + $this->showForm(_('Cannot normalize that screenname')); return; } - if (!jabber_valid_base_jid($jabber, common_config('email', 'domain_check'))) { + $valid = false; + Event::handle('ValidateImScreenname', array($transport, $screenname, &$valid)); + if (!$valid) { // TRANS: Message given saving IM address that not valid. - $this->showForm(_('Not a valid Jabber ID')); - return; - } else if ($user->jabber == $jabber) { - // TRANS: Message given saving IM address that is already set. - $this->showForm(_('That is already your Jabber ID.')); + $this->showForm(_('Not a valid screenname')); return; - } else if ($this->jabberExists($jabber)) { + } else if ($this->screennameExists($transport, $screenname)) { // TRANS: Message given saving IM address that is already set for another user. - $this->showForm(_('Jabber ID already belongs to another user.')); + $this->showForm(_('Screenname already belongs to another user.')); return; } $confirm = new Confirm_address(); - $confirm->address = $jabber; - $confirm->address_type = 'jabber'; + $confirm->address = $screenname; + $confirm->address_type = $transport; $confirm->user_id = $user->id; $confirm->code = common_confirmation_code(64); $confirm->sent = common_sql_now(); @@ -352,17 +370,11 @@ class ImsettingsAction extends ConnectSettingsAction return; } - jabber_confirm_address($confirm->code, - $user->nickname, - $jabber); + Event::handle('SendImConfirmationCode', array($transport, $screenname, $confirm->code, $user)); // TRANS: Message given saving valid IM address that is to be confirmed. - // TRANS: %s is the IM address set for the site. - $msg = sprintf(_('A confirmation code was sent '. - 'to the IM address you added. '. - 'You must approve %s for '. - 'sending messages to you.'), - jabber_daemon_address()); + $msg = _('A confirmation code was sent '. + 'to the IM address you added.'); $this->showForm($msg, true); } @@ -377,16 +389,17 @@ class ImsettingsAction extends ConnectSettingsAction function cancelConfirmation() { - $jabber = $this->arg('jabber'); + $screenname = $this->trimmed('screenname'); + $transport = $this->trimmed('transport'); - $confirm = $this->getConfirmation(); + $confirm = $this->getConfirmation($transport); if (!$confirm) { // TRANS: Message given canceling IM address confirmation that is not pending. $this->showForm(_('No pending confirmation to cancel.')); return; } - if ($confirm->address != $jabber) { + if ($confirm->address != $screenname) { // TRANS: Message given canceling IM address confirmation for the wrong IM address. $this->showForm(_('That is the wrong IM address.')); return; @@ -397,7 +410,7 @@ class ImsettingsAction extends ConnectSettingsAction if (!$result) { common_log_db_error($confirm, 'DELETE', __FILE__); // TRANS: Server error thrown on database error canceling IM address confirmation. - $this->serverError(_('Couldn\'t delete IM confirmation.')); + $this->serverError(_('Couldn\'t delete confirmation.')); return; } @@ -417,32 +430,29 @@ class ImsettingsAction extends ConnectSettingsAction { $user = common_current_user(); - $jabber = $this->arg('jabber'); + $screenname = $this->trimmed('screenname'); + $transport = $this->trimmed('transport'); // Maybe an old tab open...? - if ($user->jabber != $jabber) { + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->user_id = $user->id; + if(! ($user_im_prefs->find() && $user_im_prefs->fetch())) { // TRANS: Message given trying to remove an IM address that is not // TRANS: registered for the active user. - $this->showForm(_('That is not your Jabber ID.')); + $this->showForm(_('That is not your screenname.')); return; } - $user->query('BEGIN'); - - $original = clone($user); - - $user->jabber = null; - - $result = $user->updateKeys($original); + $result = $user_im_prefs->delete(); if (!$result) { common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown on database error removing a registered IM address. + $this->serverError(_('Couldn\'t update user im prefs.')); $this->serverError(_('Couldn\'t update user.')); return; } - $user->query('COMMIT'); // XXX: unsubscribe to the old address @@ -451,25 +461,27 @@ class ImsettingsAction extends ConnectSettingsAction } /** - * Does this Jabber ID exist? + * Does this screenname exist? * * Checks if we already have another user with this address. * - * @param string $jabber Address to check + * @param string $transport Transport to check + * @param string $screenname Screenname to check * - * @return boolean whether the Jabber ID exists + * @return boolean whether the screenname exists */ - function jabberExists($jabber) + function screennameExists($transport, $screenname) { $user = common_current_user(); - $other = User::staticGet('jabber', $jabber); - - if (!$other) { + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->transport = $transport; + $user_im_prefs->screenname = $screenname; + if($user_im_prefs->find() && $user_im_prefs->fetch()){ + return true; + }else{ return false; - } else { - return $other->id != $user->id; } } } diff --git a/actions/login.php b/actions/login.php index d3e4312f7..103df7ee5 100644 --- a/actions/login.php +++ b/actions/login.php @@ -63,28 +63,6 @@ class LoginAction extends Action } /** - * Prepare page to run - * - * - * @param $args - * @return string title - */ - - function prepare($args) - { - parent::prepare($args); - - // @todo this check should really be in index.php for all sensitive actions - $ssl = common_config('site', 'ssl'); - if (empty($_SERVER['HTTPS']) && ($ssl == 'always' || $ssl == 'sometimes')) { - common_redirect(common_local_url('login')); - // exit - } - - return true; - } - - /** * Handle input, produce output * * Switches on request method; either shows the form or handles its input. @@ -118,27 +96,10 @@ class LoginAction extends Action * @return void */ - function checkLogin($user_id=null, $token=null) + function checkLogin($user_id=null) { // XXX: login throttle - // CSRF protection - token set in NoticeForm - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - $st = common_session_token(); - if (empty($token)) { - common_log(LOG_WARNING, 'No token provided by client.'); - } else if (empty($st)) { - common_log(LOG_WARNING, 'No session token stored.'); - } else { - common_log(LOG_WARNING, 'Token = ' . $token . ' and session token = ' . $st); - } - - $this->clientError(_('There was a problem with your session token. '. - 'Try again, please.')); - return; - } - $nickname = $this->trimmed('nickname'); $password = $this->arg('password'); @@ -261,7 +222,6 @@ class LoginAction extends Action $this->elementEnd('li'); $this->elementEnd('ul'); $this->submit('submit', _('Login')); - $this->hidden('token', common_session_token()); $this->elementEnd('fieldset'); $this->elementEnd('form'); $this->elementStart('p'); diff --git a/actions/plugindisable.php b/actions/plugindisable.php new file mode 100644 index 000000000..7f107b333 --- /dev/null +++ b/actions/plugindisable.php @@ -0,0 +1,78 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Plugin enable action. + * + * 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/>. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin enable action. + * + * (Re)-enables a plugin from the default plugins list. + * + * Takes parameters: + * + * - plugin: plugin name + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class PluginDisableAction extends PluginEnableAction +{ + /** + * Value to save into $config['plugins']['disable-<name>'] + */ + protected function overrideValue() + { + return 1; + } + + protected function successShortTitle() + { + // TRANS: Page title for AJAX form return when a disabling a plugin. + return _m('plugin', 'Disabled'); + } + + protected function successNextForm() + { + return new EnablePluginForm($this, $this->plugin); + } +} + + diff --git a/actions/pluginenable.php b/actions/pluginenable.php new file mode 100644 index 000000000..2dbb3e395 --- /dev/null +++ b/actions/pluginenable.php @@ -0,0 +1,166 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Plugin enable action. + * + * 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/>. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin enable action. + * + * (Re)-enables a plugin from the default plugins list. + * + * Takes parameters: + * + * - plugin: plugin name + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class PluginEnableAction extends Action +{ + var $user; + var $plugin; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + // @fixme these are pretty common, should a parent class factor these out? + + // Only allow POST requests + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This action only accepts POST requests.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + // Only for logged-in users + + $this->user = common_current_user(); + + if (empty($this->user)) { + $this->clientError(_('Not logged in.')); + return false; + } + + if (!AdminPanelAction::canAdmin('plugins')) { + $this->clientError(_('You cannot administer plugins.')); + return false; + } + + $this->plugin = $this->arg('plugin'); + $defaultPlugins = common_config('plugins', 'default'); + if (!array_key_exists($this->plugin, $defaultPlugins)) { + $this->clientError(_('No such plugin.')); + return false; + } + + return true; + } + + /** + * Handle request + * + * Does the subscription and returns results. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + $key = 'disable-' . $this->plugin; + Config::save('plugins', $key, $this->overrideValue()); + + // @fixme this is a pretty common pattern and should be refactored down + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, $this->successShortTitle()); + $this->elementEnd('head'); + $this->elementStart('body'); + $form = $this->successNextForm(); + $form->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('pluginsadminpanel'); + common_redirect($url, 303); + } + } + + /** + * Value to save into $config['plugins']['disable-<name>'] + */ + protected function overrideValue() + { + return 0; + } + + protected function successShortTitle() + { + // TRANS: Page title for AJAX form return when enabling a plugin. + return _m('plugin', 'Enabled'); + } + + protected function successNextForm() + { + return new DisablePluginForm($this, $this->plugin); + } +} diff --git a/actions/pluginsadminpanel.php b/actions/pluginsadminpanel.php new file mode 100644 index 000000000..bc400bd51 --- /dev/null +++ b/actions/pluginsadminpanel.php @@ -0,0 +1,110 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugins administration panel + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Settings + * @package StatusNet + * @author 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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugins settings + * + * @category Admin + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class PluginsadminpanelAction extends AdminPanelAction +{ + + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + // TRANS: Tab and title for plugins admin panel. + return _('Plugins'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + // TRANS: Instructions at top of plugin admin page. + return _('Additional plugins can be enabled and configured manually. ' . + 'See the <a href="http://status.net/wiki/Plugins">online plugin ' . + 'documentation</a> for more details.'); + } + + /** + * Show the plugins admin panel form + * + * @return void + */ + + function showForm() + { + $this->elementStart('fieldset', array('id' => 'settings_plugins_default')); + + // TRANS: Admin form section header + $this->element('legend', null, _('Default plugins'), 'default'); + + $this->showDefaultPlugins(); + + $this->elementEnd('fieldset'); + } + + /** + * Until we have a general plugin metadata infrastructure, for now + * we'll just list up the ones we know from the global default + * plugins list. + */ + protected function showDefaultPlugins() + { + $plugins = array_keys(common_config('plugins', 'default')); + natsort($plugins); + + if ($plugins) { + $list = new PluginList($plugins, $this); + $list->show(); + } else { + $this->element('p', null, + _('All default plugins have been disabled from the ' . + 'site\'s configuration file.')); + } + } +} diff --git a/actions/redirect.php b/actions/redirect.php new file mode 100644 index 000000000..9cb03708d --- /dev/null +++ b/actions/redirect.php @@ -0,0 +1,71 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Site access administration panel + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Action + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer site access settings + * + * @category Action + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class RedirectAction extends Action +{ + /** + * These pages are read-only. + * + * @param array $args unused. + * + * @return boolean read-only flag (false) + */ + + function isReadOnly($args) + { + return true; + } + + /** + * Handle a request + * + * @param array $args array of arguments + * + * @return nothing + */ + function handle($args) + { + common_redirect(common_local_url($this->arg('nextAction'), $this->arg('args'))); + } +} + diff --git a/actions/register.php b/actions/register.php index 5d91aef70..075b1af99 100644 --- a/actions/register.php +++ b/actions/register.php @@ -74,13 +74,6 @@ class RegisterAction extends Action parent::prepare($args); $this->code = $this->trimmed('code'); - // @todo this check should really be in index.php for all sensitive actions - $ssl = common_config('site', 'ssl'); - if (empty($_SERVER['HTTPS']) && ($ssl == 'always' || $ssl == 'sometimes')) { - common_redirect(common_local_url('register')); - // exit - } - if (empty($this->code)) { common_ensure_session(); if (array_key_exists('invitecode', $_SESSION)) { diff --git a/actions/shownotice.php b/actions/shownotice.php index b4af7dbaa..91b0901bf 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -274,12 +274,6 @@ class ShownoticeAction extends OwnerDesignAction 'content' => $id->toString())); } - if ($user->jabbermicroid && $user->jabber && $this->notice->uri) { - $id = new Microid('xmpp:', $user->jabber, - $this->notice->uri); - $this->element('meta', array('name' => 'microid', - 'content' => $id->toString())); - } $this->element('link',array('rel'=>'alternate', 'type'=>'application/json+oembed', 'href'=>common_local_url( diff --git a/actions/showstream.php b/actions/showstream.php index 5a22bdf28..702edd732 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -177,12 +177,6 @@ class ShowstreamAction extends ProfileAction $this->element('meta', array('name' => 'microid', 'content' => $id->toString())); } - if ($this->user->jabbermicroid && $this->user->jabber && $this->profile->profileurl) { - $id = new Microid('xmpp:'.$this->user->jabber, - $this->selfUrl()); - $this->element('meta', array('name' => 'microid', - 'content' => $id->toString())); - } // See https://wiki.mozilla.org/Microsummaries diff --git a/actions/subscriptions.php b/actions/subscriptions.php index a814a4f35..10e8f2860 100644 --- a/actions/subscriptions.php +++ b/actions/subscriptions.php @@ -218,7 +218,9 @@ class SubscriptionsListItem extends SubscriptionListItem return; } - if (!common_config('xmpp', 'enabled') && !common_config('sms', 'enabled')) { + $transports = array(); + Event::handle('GetImTransports', array(&$transports)); + if (!$transports && !common_config('sms', 'enabled')) { return; } @@ -228,7 +230,7 @@ class SubscriptionsListItem extends SubscriptionListItem 'action' => common_local_url('subedit'))); $this->out->hidden('token', common_session_token()); $this->out->hidden('profile', $this->profile->id); - if (common_config('xmpp', 'enabled')) { + if ($transports) { $attrs = array('name' => 'jabber', 'type' => 'checkbox', 'class' => 'checkbox', @@ -239,7 +241,7 @@ class SubscriptionsListItem extends SubscriptionListItem $this->out->element('input', $attrs); // TRANS: Checkbox label for enabling Jabber messages for a profile in a subscriptions list. - $this->out->element('label', array('for' => 'jabber-'.$this->profile->id), _('Jabber')); + $this->out->element('label', array('for' => 'jabber-'.$this->profile->id), _('IM')); } else { $this->out->hidden('jabber', $sub->jabber); } diff --git a/actions/othersettings.php b/actions/urlsettings.php index 13460a4bf..7b2fc3f55 100644 --- a/actions/othersettings.php +++ b/actions/urlsettings.php @@ -46,17 +46,18 @@ require_once INSTALLDIR.'/lib/accountsettingsaction.php'; * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ -class OthersettingsAction extends AccountSettingsAction + +class UrlsettingsAction extends AccountSettingsAction { /** * Title of the page * * @return string Title of the page */ + function title() { - // Page title for a tab in user profile settings. - return _('Other settings'); + return _('URL settings'); } /** @@ -93,13 +94,15 @@ class OthersettingsAction extends AccountSettingsAction 'id' => 'form_settings_other', 'class' => 'form_settings', 'action' => - common_local_url('othersettings'))); + common_local_url('urlsettings'))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); - $shorteners = array(); + $shorteners = array(_('[none]') => array('freeService' => false)); + Event::handle('GetUrlShorteners', array(&$shorteners)); + $services = array(); foreach($shorteners as $name=>$value) { @@ -124,10 +127,18 @@ class OthersettingsAction extends AccountSettingsAction $this->elementEnd('li'); } $this->elementStart('li'); - // TRANS: Label for checkbox. - $this->checkbox('viewdesigns', _('View profile designs'), - // TRANS: Tooltip for checkbox. - $user->viewdesigns, _('Show or hide profile designs.')); + $this->input('maxurllength', + _('URL longer than'), + (!is_null($this->arg('maxurllength'))) ? + $this->arg('maxurllength') : User_urlshortener_prefs::maxUrlLength($user), + _('URLs longer than this will be shortened, 0 means always shorten.')); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->input('maxnoticelength', + _('Text longer than'), + (!is_null($this->arg('maxnoticelength'))) ? + $this->arg('maxnoticelength') : User_urlshortener_prefs::maxNoticeLength($user), + _('URLs in notices longer than this will be shortened, 0 means always shorten.')); $this->elementEnd('li'); $this->elementEnd('ul'); // TRANS: Button text for saving "Other settings" in profile. @@ -163,7 +174,17 @@ class OthersettingsAction extends AccountSettingsAction return; } - $viewdesigns = $this->boolean('viewdesigns'); + $maxurllength = $this->trimmed('maxurllength'); + + if (!Validate::number($maxurllength, array('min' => 0))) { + throw new ClientException(_('Invalid number for max url length.')); + } + + $maxnoticelength = $this->trimmed('maxnoticelength'); + + if (!Validate::number($maxnoticelength, array('min' => 0))) { + throw new ClientException(_('Invalid number for max notice length.')); + } $user = common_current_user(); @@ -174,7 +195,6 @@ class OthersettingsAction extends AccountSettingsAction $original = clone($user); $user->urlshorteningservice = $urlshorteningservice; - $user->viewdesigns = $viewdesigns; $result = $user->update($original); @@ -185,6 +205,32 @@ class OthersettingsAction extends AccountSettingsAction return; } + $prefs = User_urlshortener_prefs::getPrefs($user); + $orig = null; + + if (empty($prefs)) { + $prefs = new User_urlshortener_prefs(); + + $prefs->user_id = $user->id; + $prefs->created = common_sql_now(); + } else { + $orig = clone($prefs); + } + + $prefs->urlshorteningservice = $urlshorteningservice; + $prefs->maxurllength = $maxurllength; + $prefs->maxnoticelength = $maxnoticelength; + + if (!empty($orig)) { + $result = $prefs->update($orig); + } else { + $result = $prefs->insert(); + } + + if (!$result) { + throw new ServerException(_('Error saving user URL shortening preferences.')); + } + $user->query('COMMIT'); $this->showForm(_('Preferences saved.'), true); diff --git a/actions/userdesignsettings.php b/actions/userdesignsettings.php index 1cf878000..e6caea3a1 100644 --- a/actions/userdesignsettings.php +++ b/actions/userdesignsettings.php @@ -121,6 +121,20 @@ class UserDesignSettingsAction extends DesignSettingsAction } /** + * Shows the design settings form + * + * @param Design $design a working design to show + * + * @return nothing + */ + + function showDesignForm($design) + { + $form = new UserDesignForm($this, $design, $this->submitaction); + $form->show(); + } + + /** * Save or update the user's design settings * * @return void @@ -128,6 +142,8 @@ class UserDesignSettingsAction extends DesignSettingsAction function saveDesign() { + $this->saveDesignPreferences(); + foreach ($this->args as $key => $val) { if (preg_match('/(#ho|ho)Td.*g/i', $val)) { $this->sethd(); @@ -164,7 +180,8 @@ class UserDesignSettingsAction extends DesignSettingsAction $tile = true; } - $user = common_current_user(); + $user = common_current_user(); + $design = $user->getDesign(); if (!empty($design)) { @@ -282,4 +299,64 @@ class UserDesignSettingsAction extends DesignSettingsAction $this->showForm(_('Enjoy your hotdog!'), true); } + function saveDesignPreferences() + { + $viewdesigns = $this->boolean('viewdesigns'); + + $user = common_current_user(); + + $original = clone($user); + + $user->viewdesigns = $viewdesigns; + + $result = $user->update($original); + + if ($result === false) { + common_log_db_error($user, 'UPDATE', __FILE__); + throw new ServerException(_('Couldn\'t update user.')); + } + } +} + +class UserDesignForm extends DesignForm +{ + function __construct($out, $design, $actionurl) + { + parent::__construct($out, $design, $actionurl); + } + + /** + * Legend of the Form + * + * @return void + */ + function formLegend() + { + $this->out->element('legend', null, _('Design settings')); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $user = common_current_user(); + + $this->out->elementStart('ul', 'form_data'); + $this->out->elementStart('li'); + $this->out->checkbox('viewdesigns', _('View profile designs'), + - $user->viewdesigns, _('Show or hide profile designs.')); + $this->out->elementEnd('li'); + $this->out->elementEnd('ul'); + + $this->out->elementEnd('fieldset'); + + $this->out->elementStart('fieldset'); + $this->out->element('legend', null, _('Background file')); + + parent::formData(); + } } diff --git a/classes/Config.php b/classes/Config.php index 43b99587f..e14730438 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -58,7 +58,7 @@ class Config extends Memcached_DataObject $c = self::memcache(); if (!empty($c)) { - $settings = $c->get(common_cache_key(self::settingsKey)); + $settings = $c->get(Cache::key(self::settingsKey)); if ($settings !== false) { return $settings; } @@ -77,7 +77,7 @@ class Config extends Memcached_DataObject $config->free(); if (!empty($c)) { - $c->set(common_cache_key(self::settingsKey), $settings); + $c->set(Cache::key(self::settingsKey), $settings); } return $settings; @@ -154,7 +154,7 @@ class Config extends Memcached_DataObject $c = self::memcache(); if (!empty($c)) { - $c->delete(common_cache_key(self::settingsKey)); + $c->delete(Cache::key(self::settingsKey)); } } } diff --git a/classes/File_redirection.php b/classes/File_redirection.php index 53c15bf8b..1096f500b 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -190,22 +190,51 @@ class File_redirection extends Memcached_DataObject * @param User $user whose shortening options to use; defaults to the current web session user * @return string */ - function makeShort($long_url, $user=null) { - + function makeShort($long_url, $user=null) + { $canon = File_redirection::_canonUrl($long_url); $short_url = File_redirection::_userMakeShort($canon, $user); // Did we get one? Is it shorter? - if (!empty($short_url) && mb_strlen($short_url) < mb_strlen($long_url)) { + + if (!empty($short_url)) { + return $short_url; + } else { + return $long_url; + } + } + + /** + * Shorten a URL with the current user's configured shortening + * options, if applicable. + * + * If it cannot be shortened or the "short" URL is longer than the + * original, the original is returned. + * + * If the referenced item has not been seen before, embedding data + * may be saved. + * + * @param string $long_url + * @return string + */ + + function forceShort($long_url, $user) + { + $canon = File_redirection::_canonUrl($long_url); + + $short_url = File_redirection::_userMakeShort($canon, $user, true); + + // Did we get one? Is it shorter? + if (!empty($short_url)) { return $short_url; } else { return $long_url; } } - function _userMakeShort($long_url, User $user=null) { - $short_url = common_shorten_url($long_url, $user); + function _userMakeShort($long_url, User $user=null, $force = false) { + $short_url = common_shorten_url($long_url, $user, $force); if (!empty($short_url) && $short_url != $long_url) { $short_url = (string)$short_url; // store it diff --git a/classes/Managed_DataObject.php b/classes/Managed_DataObject.php new file mode 100644 index 000000000..7990d7f40 --- /dev/null +++ b/classes/Managed_DataObject.php @@ -0,0 +1,154 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 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/>. + */ + +/** + * Wrapper for Memcached_DataObject which knows its own schema definition. + * Builds its own damn settings from a schema definition. + * + * @author Brion Vibber <brion@status.net> + */ +abstract class Managed_DataObject extends Memcached_DataObject +{ + /** + * The One True Thingy that must be defined and declared. + */ + public static abstract function schemaDef(); + + /** + * get/set an associative array of table columns + * + * @access public + * @return array (associative) + */ + function table() + { + // Hack for PHP 5.2 not supporting late static binding + //$table = static::schemaDef(); + $table = call_user_func(array(get_class($this), 'schemaDef')); + return array_map(array($this, 'columnBitmap'), $table['fields']); + } + + /** + * get/set an array of table primary keys + * + * Key info is pulled from the table definition array. + * + * @access private + * @return array + */ + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * Get a sequence key + * + * Returns the first serial column defined in the table, if any. + * + * @access private + * @return array (column,use_native,sequence_name) + */ + + function sequenceKey() + { + $table = call_user_func(array(get_class($this), 'schemaDef')); + foreach ($table['fields'] as $name => $column) { + if ($column['type'] == 'serial') { + // We have a serial/autoincrement column. + // Declare it to be a native sequence! + return array($name, true, false); + } + } + + // No sequence key on this table. + return array(false, false, false); + } + + /** + * Return key definitions for DB_DataObject and Memcache_DataObject. + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keyTypes() + { + $table = call_user_func(array(get_class($this), 'schemaDef')); + + if (!empty($table['unique keys'])) { + foreach ($table['unique keys'] as $idx => $fields) { + foreach ($fields as $name) { + $keys[$name] = 'U'; + } + } + } + + if (!empty($table['primary key'])) { + foreach ($table['primary key'] as $name) { + $keys[$name] = 'K'; + } + } + return $keys; + } + + /** + * Build the appropriate DB_DataObject bitfield map for this field. + * + * @param array $column + * @return int + */ + function columnBitmap($column) + { + $type = $column['type']; + + // For quoting style... + $intTypes = array('int', + 'integer', + 'float', + 'serial', + 'numeric'); + if (in_array($type, $intTypes)) { + $style = DB_DATAOBJECT_INT; + } else { + $style = DB_DATAOBJECT_STR; + } + + // Data type formatting style... + $formatStyles = array('blob' => DB_DATAOBJECT_BLOB, + 'text' => DB_DATAOBJECT_TXT, + 'date' => DB_DATAOBJECT_DATE, + 'time' => DB_DATAOBJECT_TIME, + 'datetime' => DB_DATAOBJECT_DATE | DB_DATAOBJECT_TIME, + 'timestamp' => DB_DATAOBJECT_MYSQLTIMESTAMP); + + if (isset($formatStyles[$type])) { + $style |= $formatStyles[$type]; + } + + // Nullable? + if (!empty($column['not null'])) { + $style |= DB_DATAOBJECT_NOTNULL; + } + + return $style; + } +}
\ No newline at end of file diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index ccfd886a1..27bb5d3c9 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -124,7 +124,7 @@ class Memcached_DataObject extends Safe_DataObject } static function memcache() { - return common_memcache(); + return Cache::instance(); } static function cacheKey($cls, $k, $v) { @@ -134,7 +134,7 @@ class Memcached_DataObject extends Safe_DataObject str_replace("\n", " ", $e->getTraceAsString())); } $vstr = self::valueString($v); - return common_cache_key(strtolower($cls).':'.$k.':'.$vstr); + return Cache::key(strtolower($cls).':'.$k.':'.$vstr); } static function getcached($cls, $k, $v) { @@ -302,8 +302,8 @@ class Memcached_DataObject extends Safe_DataObject $inst->query($qry); return $inst; } - $key_part = common_keyize($cls).':'.md5($qry); - $ckey = common_cache_key($key_part); + $key_part = Cache::keyize($cls).':'.md5($qry); + $ckey = Cache::key($key_part); $stored = $c->get($ckey); if ($stored !== false) { @@ -338,13 +338,27 @@ class Memcached_DataObject extends Safe_DataObject } $start = microtime(true); - $result = parent::_query($string); + $fail = false; + try { + $result = parent::_query($string); + } catch (Exception $e) { + $fail = $e; + } $delta = microtime(true) - $start; $limit = common_config('db', 'log_slow_queries'); if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) { $clean = $this->sanitizeQuery($string); - common_log(LOG_DEBUG, sprintf("DB query (%0.3fs): %s", $delta, $clean)); + if ($fail) { + $msg = sprintf("FAILED DB query (%0.3fs): %s - %s", $delta, $fail->getMessage(), $clean); + } else { + $msg = sprintf("DB query (%0.3fs): %s", $delta, $clean); + } + common_log(LOG_DEBUG, $msg); + } + + if ($fail) { + throw $fail; } return $result; } @@ -550,7 +564,7 @@ class Memcached_DataObject extends Safe_DataObject $keyPart = vsprintf($format, $args); - $cacheKey = common_cache_key($keyPart); + $cacheKey = Cache::key($keyPart); return $c->delete($cacheKey); } @@ -592,7 +606,7 @@ class Memcached_DataObject extends Safe_DataObject return false; } - $cacheKey = common_cache_key($keyPart); + $cacheKey = Cache::key($keyPart); return $c->get($cacheKey); } @@ -605,7 +619,7 @@ class Memcached_DataObject extends Safe_DataObject return false; } - $cacheKey = common_cache_key($keyPart); + $cacheKey = Cache::key($keyPart); return $c->set($cacheKey, $value, $flag, $expiry); } diff --git a/classes/Notice.php b/classes/Notice.php index a067cd374..c1150626b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -599,7 +599,7 @@ class Notice extends Memcached_DataObject function getStreamByIds($ids) { - $cache = common_memcache(); + $cache = Cache::instance(); if (!empty($cache)) { $notices = array(); @@ -775,7 +775,7 @@ class Notice extends Memcached_DataObject $c = self::memcache(); if (!empty($c)) { - $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id)); + $ni = $c->get(Cache::key('notice:who_gets:'.$this->id)); if ($ni !== false) { return $ni; } @@ -827,7 +827,7 @@ class Notice extends Memcached_DataObject if (!empty($c)) { // XXX: pack this data better - $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni); + $c->set(Cache::key('notice:who_gets:'.$this->id), $ni); } return $ni; @@ -1473,7 +1473,7 @@ class Notice extends Memcached_DataObject function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) { - $cache = common_memcache(); + $cache = Cache::instance(); if (empty($cache) || $since_id != 0 || $max_id != 0 || @@ -1483,7 +1483,7 @@ class Notice extends Memcached_DataObject $max_id))); } - $idkey = common_cache_key($cachekey); + $idkey = Cache::key($cachekey); $idstr = $cache->get($idkey); @@ -1665,17 +1665,17 @@ class Notice extends Memcached_DataObject function repeatStream($limit=100) { - $cache = common_memcache(); + $cache = Cache::instance(); if (empty($cache)) { $ids = $this->_repeatStreamDirect($limit); } else { - $idstr = $cache->get(common_cache_key('notice:repeats:'.$this->id)); + $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id)); if ($idstr !== false) { $ids = explode(',', $idstr); } else { $ids = $this->_repeatStreamDirect(100); - $cache->set(common_cache_key('notice:repeats:'.$this->id), implode(',', $ids)); + $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids)); } if ($limit < 100) { // We do a max of 100, so slice down to limit @@ -1826,10 +1826,10 @@ class Notice extends Memcached_DataObject if ($tag->find()) { while ($tag->fetch()) { - self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, common_keyize($tag->tag)); - self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, common_keyize($tag->tag)); - self::blow('notice_tag:notice_ids:%s', common_keyize($tag->tag)); - self::blow('notice_tag:notice_ids:%s;last', common_keyize($tag->tag)); + self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s', Cache::keyize($tag->tag)); + self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($tag->tag)); $tag->delete(); } } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 6eada7022..ea38bb350 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -40,7 +40,7 @@ class Notice_tag extends Memcached_DataObject $ids = Notice::stream(array('Notice_tag', '_streamDirect'), array($tag), - 'notice_tag:notice_ids:' . common_keyize($tag), + 'notice_tag:notice_ids:' . Cache::keyize($tag), $offset, $limit); return Notice::getStreamByIds($ids); @@ -82,9 +82,9 @@ class Notice_tag extends Memcached_DataObject function blowCache($blowLast=false) { - self::blow('notice_tag:notice_ids:%s', common_keyize($this->tag)); + self::blow('notice_tag:notice_ids:%s', Cache::keyize($this->tag)); if ($blowLast) { - self::blow('notice_tag:notice_ids:%s;last', common_keyize($this->tag)); + self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($this->tag)); } } diff --git a/classes/Profile.php b/classes/Profile.php index 332d51e20..1f959eb0d 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -410,10 +410,10 @@ class Profile extends Memcached_DataObject function subscriptionCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cnt = $c->get(common_cache_key('profile:subscription_count:'.$this->id)); + $cnt = $c->get(Cache::key('profile:subscription_count:'.$this->id)); if (is_integer($cnt)) { return (int) $cnt; } @@ -427,7 +427,7 @@ class Profile extends Memcached_DataObject $cnt = ($cnt > 0) ? $cnt - 1 : $cnt; if (!empty($c)) { - $c->set(common_cache_key('profile:subscription_count:'.$this->id), $cnt); + $c->set(Cache::key('profile:subscription_count:'.$this->id), $cnt); } return $cnt; @@ -435,9 +435,9 @@ class Profile extends Memcached_DataObject function subscriberCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cnt = $c->get(common_cache_key('profile:subscriber_count:'.$this->id)); + $cnt = $c->get(Cache::key('profile:subscriber_count:'.$this->id)); if (is_integer($cnt)) { return (int) $cnt; } @@ -449,7 +449,7 @@ class Profile extends Memcached_DataObject $cnt = (int) $sub->count('distinct subscriber'); if (!empty($c)) { - $c->set(common_cache_key('profile:subscriber_count:'.$this->id), $cnt); + $c->set(Cache::key('profile:subscriber_count:'.$this->id), $cnt); } return $cnt; @@ -480,7 +480,7 @@ class Profile extends Memcached_DataObject function hasFave($notice) { - $cache = common_memcache(); + $cache = Cache::instance(); // XXX: Kind of a hack. @@ -515,9 +515,9 @@ class Profile extends Memcached_DataObject function faveCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cnt = $c->get(common_cache_key('profile:fave_count:'.$this->id)); + $cnt = $c->get(Cache::key('profile:fave_count:'.$this->id)); if (is_integer($cnt)) { return (int) $cnt; } @@ -528,7 +528,7 @@ class Profile extends Memcached_DataObject $cnt = (int) $faves->count('distinct notice_id'); if (!empty($c)) { - $c->set(common_cache_key('profile:fave_count:'.$this->id), $cnt); + $c->set(Cache::key('profile:fave_count:'.$this->id), $cnt); } return $cnt; @@ -536,10 +536,10 @@ class Profile extends Memcached_DataObject function noticeCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cnt = $c->get(common_cache_key('profile:notice_count:'.$this->id)); + $cnt = $c->get(Cache::key('profile:notice_count:'.$this->id)); if (is_integer($cnt)) { return (int) $cnt; } @@ -550,7 +550,7 @@ class Profile extends Memcached_DataObject $cnt = (int) $notices->count('distinct id'); if (!empty($c)) { - $c->set(common_cache_key('profile:notice_count:'.$this->id), $cnt); + $c->set(Cache::key('profile:notice_count:'.$this->id), $cnt); } return $cnt; @@ -558,47 +558,47 @@ class Profile extends Memcached_DataObject function blowFavesCache() { - $cache = common_memcache(); + $cache = Cache::instance(); if ($cache) { // Faves don't happen chronologically, so we need to blow // ;last cache, too - $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id)); - $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last')); - $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id)); - $cache->delete(common_cache_key('fave:ids_by_user_own:'.$this->id.';last')); + $cache->delete(Cache::key('fave:ids_by_user:'.$this->id)); + $cache->delete(Cache::key('fave:ids_by_user:'.$this->id.';last')); + $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id)); + $cache->delete(Cache::key('fave:ids_by_user_own:'.$this->id.';last')); } $this->blowFaveCount(); } function blowSubscriberCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $c->delete(common_cache_key('profile:subscriber_count:'.$this->id)); + $c->delete(Cache::key('profile:subscriber_count:'.$this->id)); } } function blowSubscriptionCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $c->delete(common_cache_key('profile:subscription_count:'.$this->id)); + $c->delete(Cache::key('profile:subscription_count:'.$this->id)); } } function blowFaveCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $c->delete(common_cache_key('profile:fave_count:'.$this->id)); + $c->delete(Cache::key('profile:fave_count:'.$this->id)); } } function blowNoticeCount() { - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $c->delete(common_cache_key('profile:notice_count:'.$this->id)); + $c->delete(Cache::key('profile:notice_count:'.$this->id)); } } diff --git a/classes/Schema_version.php b/classes/Schema_version.php new file mode 100644 index 000000000..6b464c6d1 --- /dev/null +++ b/classes/Schema_version.php @@ -0,0 +1,22 @@ +<?php +/** + * Table Definition for schema_version + */ + +class Schema_version extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'schema_version'; // table name + public $table_name; // varchar(64) primary_key not_null + public $checksum; // varchar(64) not_null + public $modified; // datetime() not_null + + /* Static get */ + function staticGet($k,$v=null) + { return Memcached_DataObject::staticGet('Schema_version',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE +} diff --git a/classes/User.php b/classes/User.php index 1b1b971ec..4baa6d1b3 100644 --- a/classes/User.php +++ b/classes/User.php @@ -48,11 +48,6 @@ class User extends Memcached_DataObject public $language; // varchar(50) public $timezone; // varchar(50) public $emailpost; // tinyint(1) default_1 - public $jabber; // varchar(255) unique_key - public $jabbernotify; // tinyint(1) - public $jabberreplies; // tinyint(1) - public $jabbermicroid; // tinyint(1) default_1 - public $updatefrompresence; // tinyint(1) public $sms; // varchar(64) unique_key public $carrier; // int(4) public $smsnotify; // tinyint(1) @@ -94,7 +89,7 @@ class User extends Memcached_DataObject { $this->_connect(); $parts = array(); - foreach (array('nickname', 'email', 'jabber', 'incomingemail', 'sms', 'carrier', 'smsemail', 'language', 'timezone') as $k) { + foreach (array('nickname', 'email', 'incomingemail', 'sms', 'carrier', 'smsemail', 'language', 'timezone') as $k) { if (strcmp($this->$k, $orig->$k) != 0) { $parts[] = $k . ' = ' . $this->_quote($this->$k); } diff --git a/classes/User_im_prefs.php b/classes/User_im_prefs.php new file mode 100644 index 000000000..75be8969e --- /dev/null +++ b/classes/User_im_prefs.php @@ -0,0 +1,94 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Data class for user IM preferences + * + * 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 Data + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @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/ + */ + +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; + +class User_im_prefs extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'user_im_prefs'; // table name + public $user_id; // int(4) primary_key not_null + public $screenname; // varchar(255) not_null + public $transport; // varchar(255) not_null + public $notify; // tinyint(1) + public $replies; // tinyint(1) + public $microid; // tinyint(1) + public $updatefrompresence; // tinyint(1) + public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + + /* Static get */ + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_im_prefs',$k,$v); } + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('User_im_prefs', $kv); + } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + /* + DB_DataObject calculates the sequence key(s) by taking the first key returned by the keys() function. + In this case, the keys() function returns user_id as the first key. user_id is not a sequence, but + DB_DataObject's sequenceKey() will incorrectly think it is. Then, since the sequenceKey() is a numeric + type, but is not set to autoincrement in the database, DB_DataObject will create a _seq table and + manage the sequence itself. This is not the correct behavior for the user_id in this class. + So we override that incorrect behavior, and simply say there is no sequence key. + */ + function sequenceKey() + { + return array(false,false); + } + + /** + * We have two compound keys with unique constraints: + * (transport, user_id) which is our primary key, and + * (transport, screenname) which is an additional constraint. + * + * Currently there's not a way to represent that second key + * in the general keys list, so we're adding it here to the + * list of keys to use for caching, ensuring that it gets + * cleared as well when we change. + * + * @return array of cache keys + */ + function _allCacheKeys() + { + $ukeys = 'transport,screenname'; + $uvals = $this->transport . ',' . $this->screenname; + + $ckeys = parent::_allCacheKeys(); + $ckeys[] = $this->cacheKey($this->tableName(), $ukeys, $uvals); + return $ckeys; + } + +} diff --git a/classes/User_urlshortener_prefs.php b/classes/User_urlshortener_prefs.php new file mode 100755 index 000000000..e0f85af01 --- /dev/null +++ b/classes/User_urlshortener_prefs.php @@ -0,0 +1,105 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 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); +} + +class User_urlshortener_prefs extends Memcached_DataObject +{ + ###START_AUTOCODE + /* the code below is auto generated do not remove the above tag */ + + public $__table = 'user_urlshortener_prefs'; // table name + public $user_id; // int(4) primary_key not_null + public $urlshorteningservice; // varchar(50) default_ur1.ca + public $maxurllength; // int(4) not_null + public $maxnoticelength; // int(4) not_null + public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + + /* Static get */ + function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_urlshortener_prefs',$k,$v); } + + /* the code above is auto generated do not remove the tag below */ + ###END_AUTOCODE + + function sequenceKey() + { + return array(false, false, false); + } + + static function maxUrlLength($user) + { + $def = common_config('url', 'maxlength'); + + $prefs = self::getPrefs($user); + + if (empty($prefs)) { + return $def; + } else { + return $prefs->maxurllength; + } + } + + static function maxNoticeLength($user) + { + $def = common_config('url', 'maxnoticelength'); + + if ($def == -1) { + $def = Notice::maxContent(); + } + + $prefs = self::getPrefs($user); + + if (empty($prefs)) { + return $def; + } else { + return $prefs->maxnoticelength; + } + } + + static function urlShorteningService($user) + { + $def = common_config('url', 'shortener'); + + $prefs = self::getPrefs($user); + + if (empty($prefs)) { + if (!empty($user)) { + return $user->urlshorteningservice; + } else { + return $def; + } + } else { + return $prefs->urlshorteningservice; + } + } + + static function getPrefs($user) + { + if (empty($user)) { + return null; + } + + $prefs = User_urlshortener_prefs::staticGet('user_id', $user->id); + + return $prefs; + } +} diff --git a/classes/statusnet.ini b/classes/statusnet.ini index ef631e28d..d1d2980fd 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -509,6 +509,14 @@ replied_id = 1 notice_id = K profile_id = K +[schema_version] +table_name = 130 +checksum = 130 +modified = 384 + +[schema_version__keys] +table_name = K + [session] id = 130 session_data = 34 @@ -573,11 +581,6 @@ emailmicroid = 17 language = 2 timezone = 2 emailpost = 17 -jabber = 2 -jabbernotify = 17 -jabberreplies = 17 -jabbermicroid = 17 -updatefrompresence = 17 sms = 2 carrier = 1 smsnotify = 17 @@ -597,7 +600,6 @@ id = K nickname = U email = U incomingemail = U -jabber = U sms = U uri = U @@ -650,3 +652,33 @@ modified = 384 [user_location_prefs__keys] user_id = K + +[user_im_prefs] +user_id = 129 +screenname = 130 +transport = 130 +notify = 17 +replies = 17 +microid = 17 +updatefrompresence = 17 +created = 142 +modified = 384 + +[user_im_prefs__keys] +user_id = K +transport = K +; There's another unique index on (transport, screenname) +; but we have no way to represent a compound index other than +; the primary key in here. To ensure proper cache purging, +; we need to tweak the class. + +[user_urlshortener_prefs] +user_id = 129 +urlshorteningservice = 2 +maxurllength = 129 +maxnoticelength = 129 +created = 142 +modified = 384 + +[user_urlshortener_prefs__keys] +user_id = K diff --git a/db/core.php b/db/core.php new file mode 100644 index 000000000..29aa917c1 --- /dev/null +++ b/db/core.php @@ -0,0 +1,1026 @@ +<?php + +/** + * + * Some notes... + * + * Drupal docs don't list a bool type, but it might be nice to use rather than 'tinyint' + * Note however that we use bitfields and things as well in tinyints, and PG's + * "bool" type isn't 100% compatible with 0/1 checks. Just keeping tinyints. :) + * + * decimal <-> numeric + * + * MySQL 'timestamp' columns were formerly used for 'modified' files for their + * auto-updating properties. This didn't play well with changes to cache usage + * in 0.9.x, as we don't know the timestamp value at INSERT time and never + * have a chance to load it up again before caching. For now I'm leaving them + * in, but we may want to clean them up later. + * + * Current code should be setting 'created' and 'modified' fields explicitly; + * this also avoids mismatches between server and client timezone settings. + * + * + * fulltext indexes? + * got one or two things wanting a custom charset setting on a field? + * + * foreign keys are kinda funky... + * those specified in inline syntax (as all in the original .sql) are NEVER ENFORCED on mysql + * those made with an explicit 'foreign key' WITHIN INNODB and IF there's a proper index, do get enforced + * double-check what we've been doing on postgres? + */ + +$schema['profile'] = array( + 'description' => 'local and remote users have profiles', + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'nickname' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'), + 'fullname' => array('type' => 'varchar', 'length' => 255, 'description' => 'display name'), + 'profileurl' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL, cached so we dont regenerate'), + 'homepage' => array('type' => 'varchar', 'length' => 255, 'description' => 'identifying URL'), + 'bio' => array('type' => 'text', 'description' => 'descriptive biography'), + 'location' => array('type' => 'varchar', 'length' => 255, 'description' => 'physical location'), + 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'), + 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), + 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), + 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'profile_nickname_idx' => array('nickname'), + ), + 'fulltext indexes' => array( + 'nickname' => array('nickname', 'fullname', 'location', 'bio', 'homepage') + ), +); + +$schema['avatar'] = array( + 'fields' => array( + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'), + 'original' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'uploaded by user or generated?'), + 'width' => array('type' => 'int', 'not null' => true, 'description' => 'image width'), + 'height' => array('type' => 'int', 'not null' => true, 'description' => 'image height'), + 'mediatype' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'file type'), + 'filename' => array('type' => 'varchar', 'length' => 255, 'description' => 'local filename, if local'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'avatar location'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('profile_id', 'width', 'height'), + 'unique keys' => array( + 'avatar_url_key' => array('url'), + ), + 'foreign keys' => array( + 'avatar_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + 'indexes' => array( + 'avatar_profile_id_idx' => array('profile_id'), + ), +); + +$schema['sms_carrier'] = array( + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'primary key for SMS carrier'), + 'name' => array('type' => 'varchar', 'length' => 64, 'description' => 'name of the carrier'), + 'email_pattern' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'sprintf pattern for making an email address from a phone number'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'sms_carrier_name_key' => array('name'), + ), +); + +$schema['user'] = array( + 'description' => 'local users', + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'), + 'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname or username, duped in profile'), + 'password' => array('type' => 'varchar', 'length' => 255, 'description' => 'salted password, can be null for OpenID users'), + 'email' => array('type' => 'varchar', 'length' => 255, 'description' => 'email address for password recovery etc.'), + 'incomingemail' => array('type' => 'varchar', 'length' => 255, 'description' => 'email address for post-by-email'), + 'emailnotifysub' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of subscriptions'), + 'emailnotifyfav' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of favorites'), + 'emailnotifynudge' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of nudges'), + 'emailnotifymsg' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of direct messages'), + 'emailnotifyattn' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Notify by email of @-replies'), + 'emailmicroid' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'whether to publish email microid'), + 'language' => array('type' => 'varchar', 'length' => 50, 'description' => 'preferred language'), + 'timezone' => array('type' => 'varchar', 'length' => 50, 'description' => 'timezone'), + 'emailpost' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Post by email'), + 'sms' => array('type' => 'varchar', 'length' => 64, 'description' => 'sms phone number'), + 'carrier' => array('type' => 'int', 'description' => 'foreign key to sms_carrier'), + 'smsnotify' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to send notices to SMS'), + 'smsreplies' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'whether to send notices to SMS on replies'), + 'smsemail' => array('type' => 'varchar', 'length' => 255, 'description' => 'built from sms and carrier'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'autosubscribe' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'automatically subscribe to users who subscribe to us'), + 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'ur1.ca', 'description' => 'service to use for auto-shortening URLs'), + 'inboxed' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'has an inbox been created for this user?'), + 'design_id' => array('type' => 'int', 'description' => 'id of a design'), + 'viewdesigns' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'whether to view user-provided designs'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'user_nickname_key' => array('nickname'), + 'user_email_key' => array('email'), + 'user_incomingemail_key' => array('incomingemail'), + 'user_sms_key' => array('sms'), + 'user_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'user_id_fkey' => array('profile', array('id' => 'id')), + 'user_carrier_fkey' => array('sms_carrier', array('carrier' => 'id')), + 'user_design_id_fkey' => array('design', array('design_id' => 'id')), + ), + 'indexes' => array( + 'user_smsemail_idx' => array('smsemail'), + ), +); + +$schema['remote_profile'] = array( + 'description' => 'remote people (OMB)', + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'postnoticeurl' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL we use for posting notices'), + 'updateprofileurl' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL we use for updates to this profile'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'remote_profile_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'remote_profile_id_fkey' => array('profile', array('id' => 'id')), + ), +); + +$schema['subscription'] = array( + 'fields' => array( + 'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'profile listening'), + 'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'profile being listened to'), + 'jabber' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'deliver jabber messages'), + 'sms' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'deliver sms messages'), + 'token' => array('type' => 'varchar', 'length' => 255, 'description' => 'authorization token'), + 'secret' => array('type' => 'varchar', 'length' => 255, 'description' => 'token secret'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('subscriber', 'subscribed'), + 'indexes' => array( + 'subscription_subscriber_idx' => array('subscriber', 'created'), + 'subscription_subscribed_idx' => array('subscribed', 'created'), + 'subscription_token_idx' => array('token'), + ), +); + +$schema['notice'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'content' => array('type' => 'text', 'description' => 'update content'), + 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'), + 'is_local' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'notice was generated by a user'), + 'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'), + 'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'), + 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'), + 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'), + 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), + 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), + 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'notice_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'notice_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + 'notice_reply_to_fkey' => array('notice', array('reply_to' => 'id')), + 'notice_conversation_fkey' => array('conversation', array('conversation' => 'id')), # note... used to refer to notice.id + 'notice_repeat_of_fkey' => array('notice', array('repeat_of' => 'id')), # @fixme: what about repeats of deleted notices? + ), + 'indexes' => array( + 'notice_profile_id_idx' => array('profile_id', 'created', 'id'), + 'notice_conversation_idx' => array('conversation'), + 'notice_created_idx' => array('created'), + 'notice_replyto_idx' => array('reply_to'), + 'notice_repeatof_idx' => array('repeat_of'), + ), + 'fulltext indexes' => array( + 'content' => array('content'), + ) +); + +$schema['notice_source'] = array( + 'fields' => array( + 'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'source code'), + 'name' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'name of the source'), + 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'url to link to'), + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'date this record was created'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('code'), +); + +$schema['reply'] = array( + 'fields' => array( + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice that is the reply'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'profile replied to'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + 'replied_id' => array('type' => 'int', 'description' => 'notice replied to (not used, see notice.reply_to)'), + ), + 'primary key' => array('notice_id', 'profile_id'), + 'foreign keys' => array( + 'reply_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + 'reply_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + 'indexes' => array( + 'reply_notice_id_idx' => array('notice_id'), + 'reply_profile_id_idx' => array('profile_id'), + 'reply_replied_id_idx' => array('replied_id'), + ), +); + +$schema['fave'] = array( + 'fields' => array( + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice that is the favorite'), + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who likes this notice'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('notice_id', 'user_id'), + 'foreign keys' => array( + 'fave_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + 'fave_user_id_fkey' => array('profile', array('user_id' => 'id')), // note: formerly referenced notice.id, but we can now record remote users' favorites + ), + 'indexes' => array( + 'fave_notice_id_idx' => array('notice_id'), + 'fave_user_id_idx' => array('user_id', 'modified'), + 'fave_modified_idx' => array('modified'), + ), +); + +/* tables for OAuth */ + +$schema['consumer'] = array( + 'description' => 'OAuth consumer record', + 'fields' => array( + 'consumer_key' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'unique identifier, root URL'), + 'consumer_secret' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'secret value'), + 'seed' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'seed for new tokens by this consumer'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('consumer_key'), +); + +$schema['token'] = array( + 'description' => 'OAuth token record', + 'fields' => array( + 'consumer_key' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'unique identifier, root URL'), + 'tok' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'identifying value'), + 'secret' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'secret value'), + 'type' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'request or access'), + 'state' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'for requests, 0 = initial, 1 = authorized, 2 = used'), + 'verifier' => array('type' => 'varchar', 'length' => 255, 'description' => 'verifier string for OAuth 1.0a'), + 'verified_callback' => array('type' => 'varchar', 'length' => 255, 'description' => 'verified callback URL for OAuth 1.0a'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('consumer_key', 'tok'), + 'foreign keys' => array( + 'token_consumer_key_fkey' => array('consumer', array('consumer_key'=> 'consumer_key')), + ), +); + +$schema['nonce'] = array( + 'description' => 'OAuth nonce record', + 'fields' => array( + 'consumer_key' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'unique identifier, root URL'), + 'tok' => array('type' => 'char', 'length' => 32, 'description' => 'buggy old value, ignored'), + 'nonce' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'nonce'), + 'ts' => array('type' => 'datetime', 'not null' => true, 'description' => 'timestamp sent'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('consumer_key', 'ts', 'nonce'), +); + +$schema['oauth_application'] = array( + 'description' => 'OAuth application registration record', + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'owner' => array('type' => 'int', 'not null' => true, 'description' => 'owner of the application'), + 'consumer_key' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'application consumer key'), + 'name' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'name of the application'), + 'description' => array('type' => 'varchar', 'length' => 255, 'description' => 'description of the application'), + 'icon' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'application icon'), + 'source_url' => array('type' => 'varchar', 'length' => 255, 'description' => 'application homepage - used for source link'), + 'organization' => array('type' => 'varchar', 'length' => 255, 'description' => 'name of the organization running the application'), + 'homepage' => array('type' => 'varchar', 'length' => 255, 'description' => 'homepage for the organization'), + 'callback_url' => array('type' => 'varchar', 'length' => 255, 'description' => 'url to redirect to after authentication'), + 'type' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'type of app, 1 = browser, 2 = desktop'), + 'access_type' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'default access type, bit 1 = read, bit 2 = write'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'oauth_application_name_key' => array('name'), // in the long run, we should perhaps not force these unique, and use another source id + ), + 'foreign keys' => array( + 'oauth_application_owner_fkey' => array('profile', array('owner' => 'id')), // Are remote users allowed to create oauth application records? + 'oauth_application_consumer_key_fkey' => array('consumer', array('consumer_key' => 'consumer_key')), + ), +); + +$schema['oauth_application_user'] = array( + 'fields' => array( + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'user of the application'), + 'application_id' => array('type' => 'int', 'not null' => true, 'description' => 'id of the application'), + 'access_type' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'access type, bit 1 = read, bit 2 = write'), + 'token' => array('type' => 'varchar', 'length' => 255, 'description' => 'request or access token'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('profile_id', 'application_id'), + 'foreign keys' => array( + 'oauth_application_user_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + 'oauth_application_user_application_id_fkey' => array('oauth_application', array('application_id' => 'id')), + ), +); + +/* These are used by JanRain OpenID library */ + +$schema['oid_associations'] = array( + 'fields' => array( + 'server_url' => array('type' => 'blob', 'not null' => true), + 'handle' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'default' => ''), // character set latin1, + 'secret' => array('type' => 'blob'), + 'issued' => array('type' => 'int'), + 'lifetime' => array('type' => 'int'), + 'assoc_type' => array('type' => 'varchar', 'length' => 64), + ), + 'primary key' => array(array('server_url', 255), 'handle'), +); + +$schema['oid_nonces'] = array( + 'fields' => array( + 'server_url' => array('type' => 'varchar', 'length' => 2047), + 'timestamp' => array('type' => 'int'), + 'salt' => array('type' => 'char', 'length' => 40), + ), + 'unique keys' => array( + 'oid_nonces_server_url_timestamp_salt_key' => array(array('server_url', 255), 'timestamp', 'salt'), + ), +); + +$schema['confirm_address'] = array( + 'fields' => array( + 'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'good random code'), + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who requested confirmation'), + 'address' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'address (email, xmpp, SMS, etc.)'), + 'address_extra' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'carrier ID, for SMS'), + 'address_type' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'address type ("email", "xmpp", "sms")'), + 'claimed' => array('type' => 'datetime', 'description' => 'date this was claimed for queueing'), + 'sent' => array('type' => 'datetime', 'description' => 'date this was sent for queueing'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('code'), + 'foreign keys' => array( + 'confirm_address_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['remember_me'] = array( + 'fields' => array( + 'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'good random code'), + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who is logged in'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('code'), + 'foreign keys' => array( + 'remember_me_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['queue_item'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'frame' => array('type' => 'blob', 'not null' => true, 'description' => 'data: object reference or opaque string'), + 'transport' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'queue for what? "email", "xmpp", "sms", "irc", ...'), // @fixme 8 chars is too short; bump up. + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'claimed' => array('type' => 'datetime', 'description' => 'date this item was claimed'), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'queue_item_created_idx' => array('created'), + ), +); + +$schema['notice_tag'] = array( + 'description' => 'Hash tags', + 'fields' => array( + 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash tag associated with this notice'), + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice tagged'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('tag', 'notice_id'), + 'foreign keys' => array( + 'notice_tag_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + ), + 'indexes' => array( + 'notice_tag_created_idx' => array('created'), + 'notice_tag_notice_id_idx' => array('notice_id'), + ), +); + +/* Synching with foreign services */ + +$schema['foreign_service'] = array( + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'numeric key for service'), + 'name' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'name of the service'), + 'description' => array('type' => 'varchar', 'length' => 255, 'description' => 'description'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'foreign_service_name_key' => array('name'), + ), +); + +$schema['foreign_user'] = array( + 'fields' => array( + 'id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'unique numeric key on foreign service'), + 'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'identifying URI'), + 'nickname' => array('type' => 'varchar', 'length' => 255, 'description' => 'nickname on foreign service'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id', 'service'), + 'foreign keys' => array( + 'foreign_user_service_fkey' => array('foreign_service', array('service' => 'id')), + ), + 'unique keys' => array( + 'foreign_user_uri_key' => array('uri'), + ), +); + +$schema['foreign_link'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'link to user on this system, if exists'), + 'foreign_id' => array('type' => 'int', 'size' => 'big', 'unsigned' => true, 'not null' => true, 'description' => 'link to user on foreign service, if exists'), + 'service' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to service'), + 'credentials' => array('type' => 'varchar', 'length' => 255, 'description' => 'authc credentials, typically a password'), + 'noticesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies'), + 'friendsync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 2, 'description' => 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming'), + 'profilesync' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming'), + 'last_noticesync' => array('type' => 'datetime', 'description' => 'last time notices were imported'), + 'last_friendsync' => array('type' => 'datetime', 'description' => 'last time friends were imported'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('user_id', 'foreign_id', 'service'), + 'foreign keys' => array( + 'foreign_link_user_id_fkey' => array('user', array('user_id' => 'id')), + 'foreign_link_foreign_id_fkey' => array('foreign_user', array('foreign_id' => 'id', 'service' => 'service')), + 'foreign_link_service_fkey' => array('foreign_service', array('service' => 'id')), + ), + 'indexes' => array( + 'foreign_user_user_id_idx' => array('user_id'), + ), +); + +$schema['foreign_subscription'] = array( + 'fields' => array( + 'service' => array('type' => 'int', 'not null' => true, 'description' => 'service where relationship happens'), + 'subscriber' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'subscriber on foreign service'), + 'subscribed' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'subscribed user'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('service', 'subscriber', 'subscribed'), + 'foreign keys' => array( + 'foreign_subscription_service_fkey' => array('foreign_service', array('service' => 'id')), + 'foreign_subscription_subscriber_fkey' => array('foreign_user', array('subscriber' => 'id', 'service' => 'service')), + 'foreign_subscription_subscribed_fkey' => array('foreign_user', array('subscribed' => 'id', 'service' => 'service')), + ), + 'indexes' => array( + 'foreign_subscription_subscriber_idx' => array('service', 'subscriber'), + 'foreign_subscription_subscribed_idx' => array('service', 'subscribed'), + ), +); + +$schema['invitation'] = array( + 'fields' => array( + 'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'random code for an invitation'), + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'who sent the invitation'), + 'address' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'invitation sent to'), + 'address_type' => array('type' => 'varchar', 'length' => 8, 'not null' => true, 'description' => 'address type ("email", "xmpp", "sms")'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('code'), + 'foreign keys' => array( + 'invitation_user_id_fkey' => array('user', array('user_id' => 'id')), + ), + 'indexes' => array( + 'invitation_address_idx' => array('address', 'address_type'), + 'invitation_user_id_idx' => array('user_id'), + ), +); + +$schema['message'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier'), + 'from_profile' => array('type' => 'int', 'not null' => true, 'description' => 'who the message is from'), + 'to_profile' => array('type' => 'int', 'not null' => true, 'description' => 'who the message is to'), + 'content' => array('type' => 'text', 'description' => 'message content'), + 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + 'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'message_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'message_from_profile_fkey' => array('profile', array('from_profile' => 'id')), + 'message_to_profile_fkey' => array('profile', array('to_profile' => 'id')), + ), + 'indexes' => array( + // @fixme these are really terrible indexes, since you can only sort on one of them at a time. + // looks like we really need a (to_profile, created) for inbox and a (from_profile, created) for outbox + 'message_from_idx' => array('from_profile'), + 'message_to_idx' => array('to_profile'), + 'message_created_idx' => array('created'), + ), +); + +$schema['notice_inbox'] = array( + 'description' => 'Obsolete; old entries here are converted to packed entries in the inbox table since 0.9', + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user receiving the message'), + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice received'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice was created'), + 'source' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'reason it is in the inbox, 1=subscription'), + ), + 'primary key' => array('user_id', 'notice_id'), + 'foreign keys' => array( + 'notice_inbox_user_id_fkey' => array('user', array('user_id' => 'id')), + 'notice_inbox_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + ), + 'indexes' => array( + 'notice_inbox_notice_id_idx' => array('notice_id'), + ), +); + +$schema['profile_tag'] = array( + 'fields' => array( + 'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'), + 'tagged' => array('type' => 'int', 'not null' => true, 'description' => 'profile tagged'), + 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash tag associated with this notice'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was added'), + ), + 'primary key' => array('tagger', 'tagged', 'tag'), + 'foreign keys' => array( + 'profile_tag_tagger_fkey' => array('user', array('tagger' => 'id')), + 'profile_tag_tagged_fkey' => array('profile', array('tagged' => 'id')), + ), + 'indexes' => array( + 'profile_tag_modified_idx' => array('modified'), + 'profile_tag_tagger_tag_idx' => array('tagger', 'tag'), + 'profile_tag_tagged_idx' => array('tagged'), + ), +); + +$schema['profile_block'] = array( + 'fields' => array( + 'blocker' => array('type' => 'int', 'not null' => true, 'description' => 'user making the block'), + 'blocked' => array('type' => 'int', 'not null' => true, 'description' => 'profile that is blocked'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date of blocking'), + ), + 'foreign keys' => array( + 'profile_block_blocker_fkey' => array('user', array('blocker' => 'id')), + 'profile_block_blocked_fkey' => array('profile', array('blocked' => 'id')), + ), + 'primary key' => array('blocker', 'blocked'), +); + +$schema['user_group'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + + 'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'nickname for addressing'), + 'fullname' => array('type' => 'varchar', 'length' => 255, 'description' => 'display name'), + 'homepage' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL, cached so we dont regenerate'), + 'description' => array('type' => 'text', 'description' => 'group description'), + 'location' => array('type' => 'varchar', 'length' => 255, 'description' => 'related physical location, if any'), + + 'original_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'original size logo'), + 'homepage_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'homepage (profile) size logo'), + 'stream_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'stream-sized logo'), + 'mini_logo' => array('type' => 'varchar', 'length' => 255, 'description' => 'mini logo'), + 'design_id' => array('type' => 'int', 'description' => 'id of a design'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universal identifier'), + 'mainpage' => array('type' => 'varchar', 'length' => 255, 'description' => 'page for group info to link to'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'user_group_uri_key' => array('uri'), + ), + 'foreign keys' => array( + 'user_group_design_id_fkey' => array('design', array('design_id' => 'id')), + ), + 'indexes' => array( + 'user_group_nickname_idx' => array('nickname'), + ), +); + +$schema['group_member'] = array( + 'fields' => array( + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'), + 'is_admin' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this user an admin?'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('group_id', 'profile_id'), + 'foreign keys' => array( + 'group_member_group_id_fkey' => array('user_group', array('group_id' => 'id')), + 'group_member_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), + 'indexes' => array( + // @fixme probably we want a (profile_id, created) index here? + 'group_member_profile_id_idx' => array('profile_id'), + 'group_member_created_idx' => array('created'), + ), +); + +$schema['related_group'] = array( + // @fixme description for related_group? + 'fields' => array( + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'), + 'related_group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('group_id', 'related_group_id'), + 'foreign keys' => array( + 'related_group_group_id_fkey' => array('user_group', array('group_id' => 'id')), + 'related_group_related_group_id_fkey' => array('user_group', array('related_group_id' => 'id')), + ), +); + +$schema['group_inbox'] = array( + 'description' => 'Many-many table listing notices posted to a given group, or which groups a given notice was posted to.', + 'fields' => array( + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group receiving the message'), + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice received'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice was created'), + ), + 'primary key' => array('group_id', 'notice_id'), + 'foreign keys' => array( + 'group_inbox_group_id_fkey' => array('user_group', array('group_id' => 'id')), + 'group_inbox_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + ), + 'indexes' => array( + 'group_inbox_created_idx' => array('created'), + 'group_inbox_notice_id_idx' => array('notice_id'), + ), +); + +$schema['file'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'destination URL after following redirections'), + 'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'), + 'size' => array('type' => 'int', 'description' => 'size of resource when available'), + 'title' => array('type' => 'varchar', 'length' => 255, 'description' => 'title of resource when available'), + 'date' => array('type' => 'int', 'description' => 'date of resource according to http query'), + 'protected' => array('type' => 'int', 'description' => 'true when URL is private (needs login)'), + 'filename' => array('type' => 'varchar', 'length' => 255, 'description' => 'if a local file, name of the file'), + + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'file_url_key' => array('url'), + ), +); + +$schema['file_oembed'] = array( + 'fields' => array( + 'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'oEmbed for that URL/file'), + 'version' => array('type' => 'varchar', 'length' => 20, 'description' => 'oEmbed spec. version'), + 'type' => array('type' => 'varchar', 'length' => 20, 'description' => 'oEmbed type: photo, video, link, rich'), + 'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'), + 'provider' => array('type' => 'varchar', 'length' => 50, 'description' => 'name of this oEmbed provider'), + 'provider_url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of this oEmbed provider'), + 'width' => array('type' => 'int', 'description' => 'width of oEmbed resource when available'), + 'height' => array('type' => 'int', 'description' => 'height of oEmbed resource when available'), + 'html' => array('type' => 'text', 'description' => 'html representation of this oEmbed resource when applicable'), + 'title' => array('type' => 'varchar', 'length' => 255, 'description' => 'title of oEmbed resource when available'), + 'author_name' => array('type' => 'varchar', 'length' => 50, 'description' => 'author name for this oEmbed resource'), + 'author_url' => array('type' => 'varchar', 'length' => 255, 'description' => 'author URL for this oEmbed resource'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL for this oEmbed resource when applicable (photo, link)'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('file_id'), + 'foreign keys' => array( + 'file_oembed_file_id_fkey' => array('file', array('file_id' => 'id')), + ), +); + +$schema['file_redirection'] = array( + 'fields' => array( + 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'short URL (or any other kind of redirect) for file (id)'), + 'file_id' => array('type' => 'int', 'description' => 'short URL for what URL/file'), + 'redirections' => array('type' => 'int', 'description' => 'redirect count'), + 'httpcode' => array('type' => 'int', 'description' => 'HTTP status code (20x, 30x, etc.)'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('url'), + 'foreign keys' => array( + 'file_redirection_file_id_fkey' => array('file' => array('file_id' => 'id')), + ), +); + +$schema['file_thumbnail'] = array( + 'fields' => array( + 'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'thumbnail for what URL/file'), + 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of thumbnail'), + 'width' => array('type' => 'int', 'description' => 'width of thumbnail'), + 'height' => array('type' => 'int', 'description' => 'height of thumbnail'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('file_id'), + 'foreign keys' => array( + 'file_thumbnail_file_id_fkey' => array('file', array('file_id' => 'id')), + ), + 'unique keys' => array( + 'file_thumbnail_url_key' => array('url'), + ), +); + +$schema['file_to_post'] = array( + 'fields' => array( + 'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'id of URL/file'), + 'post_id' => array('type' => 'int', 'not null' => true, 'description' => 'id of the notice it belongs to'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('file_id', 'post_id'), + 'foreign keys' => array( + 'file_to_post_file_id_fkey' => array('file', array('file_id' => 'id')), + 'file_to_post_post_id_fkey' => array('notice', array('post_id' => 'id')), + ), + 'indexes' => array( + 'post_id_idx' => array('post_id'), + ), +); + +$schema['design'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'design ID'), + 'backgroundcolor' => array('type' => 'int', 'description' => 'main background color'), + 'contentcolor' => array('type' => 'int', 'description' => 'content area background color'), + 'sidebarcolor' => array('type' => 'int', 'description' => 'sidebar background color'), + 'textcolor' => array('type' => 'int', 'description' => 'text color'), + 'linkcolor' => array('type' => 'int', 'description' => 'link color'), + 'backgroundimage' => array('type' => 'varchar', 'length' => 255, 'description' => 'background image, if any'), + 'disposition' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'bit 1 = hide background image, bit 2 = display background image, bit 4 = tile background image'), + ), + 'primary key' => array('id'), +); + +$schema['group_block'] = array( + 'fields' => array( + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group profile is blocked from'), + 'blocked' => array('type' => 'int', 'not null' => true, 'description' => 'profile that is blocked'), + 'blocker' => array('type' => 'int', 'not null' => true, 'description' => 'user making the block'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date of blocking'), + ), + 'primary key' => array('group_id', 'blocked'), + 'foreign keys' => array( + 'group_block_group_id_fkey' => array('user_group', array('group_id' => 'id')), + 'group_block_blocked_fkey' => array('profile', array('blocked' => 'id')), + 'group_block_blocker_fkey' => array('user', array('blocker' => 'id')), + ), +); + +$schema['group_alias'] = array( + 'fields' => array( + 'alias' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'additional nickname for the group'), + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group profile is blocked from'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date alias was created'), + ), + 'primary key' => array('alias'), + 'foreign keys' => array( + 'group_alias_group_id_fkey' => array('user_group', array('group_id' => 'id')), + ), + 'indexes' => array( + 'group_alias_group_id_idx' => array('group_id'), + ), +); + +$schema['session'] = array( + 'fields' => array( + 'id' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'session ID'), + 'session_data' => array('type' => 'text', 'description' => 'session data'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'session_modified_idx' => array('modified'), + ), +); + +$schema['deleted_notice'] = array( + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity of notice'), + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'author of the notice'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'), + 'deleted' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'deleted_notice_uri_key' => array('uri'), + ), + 'indexes' => array( + 'deleted_notice_profile_id_idx' => array('profile_id'), + ), +); + +$schema['config'] = array( + 'fields' => array( + 'section' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'default' => '', 'description' => 'configuration section'), + 'setting' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'default' => '', 'description' => 'configuration setting'), + 'value' => array('type' => 'varchar', 'length' => 255, 'description' => 'configuration value'), + ), + 'primary key' => array('section', 'setting'), +); + +$schema['profile_role'] = array( + 'fields' => array( + 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'account having the role'), + 'role' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'string representing the role'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the role was granted'), + ), + 'primary key' => array('profile_id', 'role'), + 'foreign keys' => array( + 'profile_role_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + ), +); + +$schema['location_namespace'] = array( + 'fields' => array( + 'id' => array('type' => 'int', 'not null' => true, 'description' => 'identity for this namespace'), + 'description' => array('type' => 'varchar', 'length' => 255, 'description' => 'description of the namespace'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), +); + +$schema['login_token'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user owning this token'), + 'token' => array('type' => 'char', 'length' => 32, 'not null' => true, 'description' => 'token useable for logging in'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + 'login_token_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['user_location_prefs'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who has the preference'), + 'share_location' => array('type' => 'int', 'size' => 'tiny', 'default' => 1, 'description' => 'Whether to share location data'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + 'user_location_prefs_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['inbox'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user receiving the notice'), + 'notice_ids' => array('type' => 'blob', 'description' => 'packed list of notice ids'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + 'inbox_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +// @fixme possibly swap this for a more general prefs table? +$schema['user_im_prefs'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user'), + 'screenname' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'screenname on this service'), + 'transport' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'transport (ex xmpp, aim)'), + 'notify' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'Notify when a new notice is sent'), + 'replies' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'Send replies from people not subscribed to'), + 'microid' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 1, 'description' => 'Publish a MicroID'), + 'updatefrompresence' => array('type' => 'int', 'size' => 'tiny', 'not null' => true, 'default' => 0, 'description' => 'Send replies from people not subscribed to.'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('user_id', 'transport'), + 'unique keys' => array( + 'transport_screenname_key' => array('transport', 'screenname'), + ), + 'foreign keys' => array( + 'user_im_prefs_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['conversation'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), + 'uri' => array('type' => 'varchar', 'length' => 225, 'description' => 'URI of the conversation'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'conversation_uri_key' => array('uri'), + ), +); + +$schema['local_group'] = array( + 'description' => 'Record for a user group on the local site, with some additional info not in user_group', + 'fields' => array( + 'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'group represented'), + 'nickname' => array('type' => 'varchar', 'length' => 64, 'description' => 'group represented'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('group_id'), + 'foreign keys' => array( + 'local_group_group_id_fkey' => array('user_group', array('group_id' => 'id')), + ), + 'unique keys' => array( + 'local_group_nickname_key' => array('nickname'), + ), +); + +$schema['user_urlshortener_prefs'] = array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user'), + 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'ur1.ca', 'description' => 'service to use for auto-shortening URLs'), + 'maxurllength' => array('type' => 'int', 'not null' => true, 'description' => 'urls greater than this length will be shortened, 0 = always, null = never'), + 'maxnoticelength' => array('type' => 'int', 'not null' => true, 'description' => 'notices with content greater than this value will have all urls shortened, 0 = always, null = never'), + + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + 'user_urlshortener_prefs_user_id_fkey' => array('user', array('user_id' => 'id')), + ), +); + +$schema['schema_version'] = array( + 'description' => 'To avoid checking database structure all the time, we store a checksum of the expected schema info for each table here. If it has not changed since the last time we checked the table, we can leave it as is.', + 'fields' => array( + 'table_name' => array('type' => 'varchar', 'length' => '64', 'not null' => true, 'description' => 'Table name'), + 'checksum' => array('type' => 'varchar', 'length' => '64', 'not null' => true, 'description' => 'Checksum of schema array; a mismatch indicates we should check the table more thoroughly.'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('table_name'), +); diff --git a/db/statusnet.sql b/db/statusnet.sql index ac48e6253..5426ace92 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -62,11 +62,6 @@ create table user ( language varchar(50) comment 'preferred language', timezone varchar(50) comment 'timezone', emailpost tinyint default 1 comment 'Post by email', - jabber varchar(255) unique key comment 'jabber ID for notices', - jabbernotify tinyint default 0 comment 'whether to send notices to jabber', - jabberreplies tinyint default 0 comment 'whether to send notices to jabber on replies', - jabbermicroid tinyint default 1 comment 'whether to publish xmpp microid', - updatefrompresence tinyint default 0 comment 'whether to record updates from Jabber presence notices', sms varchar(64) unique key comment 'sms phone number', carrier integer comment 'foreign key to sms_carrier' references sms_carrier (id), smsnotify tinyint default 0 comment 'whether to send notices to SMS', @@ -268,9 +263,9 @@ create table oid_nonces ( create table confirm_address ( code varchar(32) not null primary key comment 'good random code', user_id integer not null comment 'user who requested confirmation' references user (id), - address varchar(255) not null comment 'address (email, Jabber, SMS, etc.)', + address varchar(255) not null comment 'address (email, xmpp, SMS, etc.)', address_extra varchar(255) not null comment 'carrier ID, for SMS', - address_type varchar(8) not null comment 'address type ("email", "jabber", "sms")', + address_type varchar(8) not null comment 'address type ("email", "xmpp", "sms")', claimed datetime comment 'date this was claimed for queueing', sent datetime comment 'date this was sent for queueing', modified timestamp comment 'date this record was modified' @@ -285,7 +280,7 @@ create table remember_me ( create table queue_item ( id integer auto_increment primary key comment 'unique identifier', frame blob not null comment 'data: object reference or opaque string', - transport varchar(8) not null comment 'queue for what? "email", "jabber", "sms", "irc", ...', + transport varchar(8) not null comment 'queue for what? "email", "xmpp", "sms", "irc", ...', created datetime not null comment 'date this record was created', claimed datetime comment 'date this item was claimed', @@ -357,7 +352,7 @@ create table invitation ( code varchar(32) not null primary key comment 'random code for an invitation', user_id int not null comment 'who sent the invitation' references user (id), address varchar(255) not null comment 'invitation sent to', - address_type varchar(8) not null comment 'address type ("email", "jabber", "sms")', + address_type varchar(8) not null comment 'address type ("email", "xmpp", "sms")', created datetime not null comment 'date this record was created', index invitation_address_idx (address, address_type), @@ -648,6 +643,21 @@ create table inbox ( ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +create table user_im_prefs ( + user_id integer not null comment 'user' references user (id), + screenname varchar(255) not null comment 'screenname on this service', + transport varchar(255) not null comment 'transport (ex xmpp, aim)', + notify tinyint(1) not null default 0 comment 'Notify when a new notice is sent', + replies tinyint(1) not null default 0 comment 'Send replies from people not subscribed to', + microid tinyint(1) not null default 1 comment 'Publish a MicroID', + updatefrompresence tinyint(1) not null default 0 comment 'Send replies from people not subscribed to.', + created timestamp not null DEFAULT CURRENT_TIMESTAMP comment 'date this record was created', + modified timestamp comment 'date this record was modified', + + constraint primary key (user_id, transport), + constraint unique key `transport_screenname_key` ( `transport` , `screenname` ) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + create table conversation ( id integer auto_increment primary key comment 'unique identifier', uri varchar(225) unique comment 'URI of the conversation', @@ -665,3 +675,15 @@ create table local_group ( ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +create table user_urlshortener_prefs ( + + user_id integer not null comment 'user' references user (id), + urlshorteningservice varchar(50) default 'ur1.ca' comment 'service to use for auto-shortening URLs', + maxurllength integer not null comment 'urls greater than this length will be shortened, 0 = always, null = never', + maxnoticelength integer not null comment 'notices with content greater than this value will have all urls shortened, 0 = always, null = never', + + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified', + + constraint primary key (user_id) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; diff --git a/doc-src/api b/doc-src/api new file mode 100644 index 000000000..5c8815bdd --- /dev/null +++ b/doc-src/api @@ -0,0 +1,6 @@ +<!-- Copyright 2008-2010 StatusNet Inc. and contributors. --> +<!-- Document licensed under Creative Commons Attribution 3.0 Unported. See --> +<!-- http://creativecommons.org/licenses/by/3.0/ for details. --> + +%%site.name%% provides an API that applications can use to interact with it. +More information about this API can be found on the [StatusNet Wiki](http://status.net/wiki/API). diff --git a/doc-src/help b/doc-src/help index 024d47378..6d12b4cd7 100644 --- a/doc-src/help +++ b/doc-src/help @@ -35,3 +35,4 @@ Here are some documents that you might find helpful in understanding * [Source](%%doc.source%%) - How to get the StatusNet source code * [Badge](%%doc.badge%%) - How to put a StatusNet badge on your blog or homepage * [Bookmarklet](%%doc.bookmarklet%%) - Bookmarklet for posting Web pages +* [API](%%doc.api%%) - API for use by external clients @@ -41,8 +41,6 @@ define('INSTALLDIR', dirname(__FILE__)); define('STATUSNET', true); define('LACONICA', true); // compatibility -require_once INSTALLDIR . '/lib/common.php'; - $user = null; $action = null; @@ -72,52 +70,69 @@ function getPath($req) */ function handleError($error) { - if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) { - return; - } + try { - $logmsg = "PEAR error: " . $error->getMessage(); - if (common_config('site', 'logdebug')) { - $logmsg .= " : ". $error->getDebugInfo(); - } - // DB queries often end up with a lot of newlines; merge to a single line - // for easier grepability... - $logmsg = str_replace("\n", " ", $logmsg); - common_log(LOG_ERR, $logmsg); - - // @fixme backtrace output should be consistent with exception handling - if (common_config('site', 'logdebug')) { - $bt = $error->getBacktrace(); - foreach ($bt as $n => $line) { - common_log(LOG_ERR, formatBacktraceLine($n, $line)); + if ($error->getCode() == DB_DATAOBJECT_ERROR_NODATA) { + return; + } + + $logmsg = "PEAR error: " . $error->getMessage(); + if ($error instanceof PEAR_Exception && common_config('site', 'logdebug')) { + $logmsg .= " : ". $error->toText(); + } + // DB queries often end up with a lot of newlines; merge to a single line + // for easier grepability... + $logmsg = str_replace("\n", " ", $logmsg); + common_log(LOG_ERR, $logmsg); + + // @fixme backtrace output should be consistent with exception handling + if (common_config('site', 'logdebug')) { + $bt = $error->getTrace(); + foreach ($bt as $n => $line) { + common_log(LOG_ERR, formatBacktraceLine($n, $line)); + } + } + if ($error instanceof DB_DataObject_Error + || $error instanceof DB_Error + || ($error instanceof PEAR_Exception && $error->getCode() == -24) + ) { + //If we run into a DB error, assume we can't connect to the DB at all + //so set the current user to null, so we don't try to access the DB + //while rendering the error page. + global $_cur; + $_cur = null; + + $msg = sprintf( + _( + 'The database for %s isn\'t responding correctly, '. + 'so the site won\'t work properly. '. + 'The site admins probably know about the problem, '. + 'but you can contact them at %s to make sure. '. + 'Otherwise, wait a few minutes and try again.' + ), + common_config('site', 'name'), + common_config('site', 'email') + ); + } else { + $msg = _( + 'An important error occured, probably related to email setup. '. + 'Check logfiles for more info..' + ); } - } - if ($error instanceof DB_DataObject_Error - || $error instanceof DB_Error - ) { - $msg = sprintf( - _( - 'The database for %s isn\'t responding correctly, '. - 'so the site won\'t work properly. '. - 'The site admins probably know about the problem, '. - 'but you can contact them at %s to make sure. '. - 'Otherwise, wait a few minutes and try again.' - ), - common_config('site', 'name'), - common_config('site', 'email') - ); - } else { - $msg = _( - 'An important error occured, probably related to email setup. '. - 'Check logfiles for more info..' - ); - } - $dac = new DBErrorAction($msg, 500); - $dac->showPage(); + $dac = new DBErrorAction($msg, 500); + $dac->showPage(); + + } catch (Exception $e) { + echo _('An error occurred.'); + } exit(-1); } +set_exception_handler('handleError'); + +require_once INSTALLDIR . '/lib/common.php'; + /** * Format a backtrace line for debug output roughly like debug_print_backtrace() does. * Exceptions already have this built in, but PEAR error objects just give us the array. @@ -242,10 +257,6 @@ function main() return; } - // For database errors - - PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); - // Make sure RW database is setup setupRW(); @@ -272,6 +283,14 @@ function main() return; } + $site_ssl = common_config('site', 'ssl'); + + // If the request is HTTP and it should be HTTPS... + if ($site_ssl != 'never' && !StatusNet::isHTTPS() && common_is_sensitive($args['action'])) { + common_redirect(common_local_url($args['action'], $args)); + return; + } + $args = array_merge($args, $_REQUEST); Event::handle('ArgsInitialize', array(&$args)); diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php index 7991c9002..2c519e4ab 100644 --- a/lib/accountsettingsaction.php +++ b/lib/accountsettingsaction.php @@ -134,12 +134,12 @@ class AccountSettingsNav extends Widget $this->showMenuItem('userdesignsettings',_('Design'),$title); Event::handle('EndAccountSettingsDesignMenuItem', array($this, &$menu)); } - if(Event::handle('StartAccountSettingsOtherMenuItem', array($this, &$menu))){ + if(Event::handle('StartAccountSettingsUrlMenuItem', array($this, &$menu))){ // TRANS: Link title attribute in user account settings menu. - $title = _('Other options'); + $title = _('URL shortener settings'); // TRANS: Link description in user account settings menu. - $this->showMenuItem('othersettings',_('Other'),$title); - Event::handle('EndAccountSettingsOtherMenuItem', array($this, &$menu)); + $this->showMenuItem('urlsettings',_('URL'),$title); + Event::handle('EndAccountSettingsUrlMenuItem', array($this, &$menu)); } Event::handle('EndAccountSettingsNav', array(&$this->action)); diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php index fae9f4fa5..8dd16e9d0 100644 --- a/lib/adminpanelaction.php +++ b/lib/adminpanelaction.php @@ -404,6 +404,14 @@ class AdminPanelNav extends Widget $menu_title, $action_name == 'licenseadminpanel', 'nav_license_admin_panel'); } + if (AdminPanelAction::canAdmin('plugins')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Plugins configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('pluginsadminpanel'), _('Plugins'), + $menu_title, $action_name == 'pluginsadminpanel', 'nav_design_admin_panel'); + } + Event::handle('EndAdminPanelNav', array($this)); } $this->action->elementEnd('ul'); diff --git a/lib/apiaction.php b/lib/apiaction.php index 2de513cbb..7eba1b3b8 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -98,6 +98,8 @@ if (!defined('STATUSNET')) { exit(1); } +class ApiValidationException extends Exception { } + /** * Contains most of the Twitter-compatible API output functions. * diff --git a/lib/cache.php b/lib/cache.php index dc667654a..8dc97a642 100644 --- a/lib/cache.php +++ b/lib/cache.php @@ -80,7 +80,7 @@ class Cache $base_key = common_config('cache', 'base'); if (empty($base_key)) { - $base_key = common_keyize(common_config('site', 'name')); + $base_key = self::keyize(common_config('site', 'name')); } return 'statusnet:' . $base_key . ':' . $extra; diff --git a/lib/channel.php b/lib/channel.php index fbc2e8697..ae9b2d214 100644 --- a/lib/channel.php +++ b/lib/channel.php @@ -69,62 +69,6 @@ class CLIChannel extends Channel } } -class XMPPChannel extends Channel -{ - var $conn = null; - - function source() - { - return 'xmpp'; - } - - function __construct($conn) - { - $this->conn = $conn; - } - - function on($user) - { - return $this->set_notify($user, 1); - } - - function off($user) - { - return $this->set_notify($user, 0); - } - - function output($user, $text) - { - $text = '['.common_config('site', 'name') . '] ' . $text; - jabber_send_message($user->jabber, $text); - } - - function error($user, $text) - { - $text = '['.common_config('site', 'name') . '] ' . $text; - jabber_send_message($user->jabber, $text); - } - - function set_notify(&$user, $notify) - { - $orig = clone($user); - $user->jabbernotify = $notify; - $result = $user->update($orig); - if (!$result) { - $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - common_log(LOG_ERR, - 'Could not set notify flag to ' . $notify . - ' for user ' . common_log_objstring($user) . - ': ' . $last_error->message); - return false; - } else { - common_log(LOG_INFO, - 'User ' . $user->nickname . ' set notify flag to ' . $notify); - return true; - } - } -} - class WebChannel extends Channel { var $out = null; @@ -216,12 +160,12 @@ class MailChannel extends Channel function on($user) { - return $this->set_notify($user, 1); + return $this->setNotify($user, 1); } function off($user) { - return $this->set_notify($user, 0); + return $this->setNotify($user, 0); } function output($user, $text) @@ -246,7 +190,7 @@ class MailChannel extends Channel return mail_send(array($this->addr), $headers, $text); } - function set_notify($user, $value) + function setNotify($user, $value) { $orig = clone($user); $user->smsnotify = $value; diff --git a/lib/command.php b/lib/command.php index 3fb4d76c7..852d0a8f7 100644 --- a/lib/command.php +++ b/lib/command.php @@ -718,7 +718,7 @@ class OffCommand extends Command } function handle($channel) { - if ($other) { + if ($this->other) { // TRANS: Error text shown when issuing the command "off" with a setting which has not yet been implemented. $channel->error($this->user, _("Command not yet implemented.")); } else { @@ -744,7 +744,7 @@ class OnCommand extends Command function handle($channel) { - if ($other) { + if ($this->other) { // TRANS: Error text shown when issuing the command "on" with a setting which has not yet been implemented. $channel->error($this->user, _("Command not yet implemented.")); } else { diff --git a/lib/common.php b/lib/common.php index cf4d6e1e7..ca02a3e7f 100644 --- a/lib/common.php +++ b/lib/common.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 2009, StatusNet, Inc. + * 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 @@ -19,124 +19,13 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @fixme shouldn't this be in index.php instead? //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.6'); -define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility - -define('STATUSNET_CODENAME', 'Man on the Moon'); - -define('AVATAR_PROFILE_SIZE', 96); -define('AVATAR_STREAM_SIZE', 48); -define('AVATAR_MINI_SIZE', 24); - -define('NOTICES_PER_PAGE', 20); -define('PROFILES_PER_PAGE', 20); - -define('FOREIGN_NOTICE_SEND', 1); -define('FOREIGN_NOTICE_RECV', 2); -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... -if (!function_exists('dl')) { - // 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; - } - } -} - -# global configuration object - -require_once('PEAR.php'); -require_once('DB/DataObject.php'); -require_once('DB/DataObject/Cast.php'); # for dates - -require_once(INSTALLDIR.'/lib/language.php'); - -// This gets included before the config file, so that admin code and plugins -// can use it - -require_once(INSTALLDIR.'/lib/event.php'); -require_once(INSTALLDIR.'/lib/plugin.php'); - -function addPlugin($name, $attrs = null) -{ - return StatusNet::addPlugin($name, $attrs); -} - -function _have_config() -{ - return StatusNet::haveConfig(); -} - -function __autoload($cls) -{ - if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) { - require_once(INSTALLDIR.'/classes/' . $cls . '.php'); - } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) { - require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php'); - } else if (mb_substr($cls, -6) == 'Action' && - file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) { - require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); - } else if ($cls == 'OAuthRequest') { - require_once('OAuth.php'); - } else { - Event::handle('Autoload', array(&$cls)); - } -} - -// XXX: how many of these could be auto-loaded on use? -// XXX: note that these files should not use config options -// at compile time since DB config options are not yet loaded. - -require_once 'Validate.php'; -require_once 'markdown.php'; - -// XXX: other formats here - -/** - * Avoid the NICKNAME_FMT constant; use the Nickname class instead. - * - * Nickname::DISPLAY_FMT is more suitable for inserting into regexes; - * note that it includes the [] and repeating bits, so should be wrapped - * directly in a capture paren usually. - * - * For validation, use Nickname::normalize(), Nickname::isValid() etc. - * - * @deprecated - */ -define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER); - -require_once INSTALLDIR.'/lib/util.php'; -require_once INSTALLDIR.'/lib/action.php'; -require_once INSTALLDIR.'/lib/mail.php'; -require_once INSTALLDIR.'/lib/subs.php'; - -require_once INSTALLDIR.'/lib/clientexception.php'; -require_once INSTALLDIR.'/lib/serverexception.php'; +// All the fun stuff to actually initialize StatusNet's framework code, +// without loading up a site configuration. +require_once INSTALLDIR . '/lib/framework.php'; try { StatusNet::init(@$server, @$path, @$conffile); diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index 325276c5f..0f64fee8c 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -100,7 +100,9 @@ class ConnectSettingsNav extends Widget # action => array('prompt', 'title') $menu = array(); - if (common_config('xmpp', 'enabled')) { + $transports = array(); + Event::handle('GetImTransports', array(&$transports)); + if ($transports) { $menu['imsettings'] = // TRANS: Menu item for Instant Messaging settings. array(_m('MENU','IM'), diff --git a/lib/default.php b/lib/default.php index 029dbb390..5c4484121 100644 --- a/lib/default.php +++ b/lib/default.php @@ -76,7 +76,8 @@ $default = 'schemacheck' => 'runtime', // 'runtime' or 'script' 'annotate_queries' => false, // true to add caller comments to queries, eg /* POST Notice::saveNew */ 'log_queries' => false, // true to log all DB queries - 'log_slow_queries' => 0), // if set, log queries taking over N seconds + 'log_slow_queries' => 0, // if set, log queries taking over N seconds + 'mysql_foreign_keys' => false), // if set, enables experimental foreign key support on MySQL 'syslog' => array('appname' => 'statusnet', # for syslog 'priority' => 'debug', # XXX: currently ignored @@ -305,11 +306,14 @@ $default = 'OStatus' => null, 'WikiHashtags' => null, 'RSSCloud' => null, + 'ClientSideShorten' => null, + 'StrictTransportSecurity' => null, 'OpenID' => null), 'locale_path' => false, // Set to a path to use *instead of* each plugin's own locale subdirectories ), + 'pluginlist' => array(), 'admin' => - array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'license')), + array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'license', 'plugins')), 'singleuser' => array('enabled' => false, 'nickname' => null), @@ -324,6 +328,10 @@ $default = 'members' => true, 'peopletag' => true, 'external' => 'sometimes'), // Options: 'sometimes', 'never', default = 'sometimes' + 'url' => + array('shortener' => 'ur1.ca', + 'maxlength' => 25, + 'maxnoticelength' => -1), 'http' => // HTTP client settings when contacting other sites array('ssl_cafile' => false, // To enable SSL cert validation, point to a CA bundle (eg '/usr/lib/ssl/certs/ca-certificates.crt') 'curl' => false, // Use CURL backend for HTTP fetches if available. (If not, PHP's socket streams will be used.) diff --git a/lib/designform.php b/lib/designform.php new file mode 100644 index 000000000..7702b873f --- /dev/null +++ b/lib/designform.php @@ -0,0 +1,324 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for choosing a design + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Form for choosing a design + * + * Used for choosing a site design, user design, or group design. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + */ + +class DesignForm extends Form +{ + /** + * Return-to args + */ + + var $design = null; + var $actionurl = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param Design $design initial design + * @param Design $actionurl url of action (for form posting) + */ + + function __construct($out, $design, $actionurl) + { + parent::__construct($out); + + $this->design = $design; + $this->actionurl = $actionurl; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'design'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_design'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return $this->actionurl; + } + + /** + * Legend of the Form + * + * @return void + */ + function formLegend() + { + $this->out->element('legend', null, _('Change design')); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->backgroundData(); + + $this->out->elementEnd('fieldset'); + + $this->out->elementStart('fieldset', array('id' => 'settings_design_color')); + // TRANS: Fieldset legend on profile design page to change profile page colours. + $this->out->element('legend', null, _('Change colours')); + $this->colourData(); + $this->out->elementEnd('fieldset'); + + $this->out->elementStart('fieldset'); + + // TRANS: Button text on profile design page to immediately reset all colour settings to default. + $this->out->submit('defaults', _('Use defaults'), 'submit form_action-default', + // TRANS: Title for button on profile design page to reset all colour settings to default. + 'defaults', _('Restore default designs')); + + $this->out->element('input', array('id' => 'settings_design_reset', + 'type' => 'reset', + // TRANS: Button text on profile design page to reset all colour settings to default without saving. + 'value' => _m('BUTTON', 'Reset'), + 'class' => 'submit form_action-primary', + // TRANS: Title for button on profile design page to reset all colour settings to default without saving. + 'title' => _('Reset back to default'))); + } + + function backgroundData() + { + $this->out->elementStart('ul', 'form_data'); + $this->out->elementStart('li'); + $this->out->element('label', array('for' => 'design_background-image_file'), + // TRANS: Label in form on profile design page. + // TRANS: Field contains file name on user's computer that could be that user's custom profile background image. + _('Upload file')); + $this->out->element('input', array('name' => 'design_background-image_file', + 'type' => 'file', + 'id' => 'design_background-image_file')); + // TRANS: Instructions for form on profile design page. + $this->out->element('p', 'form_guide', _('You can upload your personal ' . + 'background image. The maximum file size is 2Mb.')); + $this->out->element('input', array('name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt())); + $this->out->elementEnd('li'); + + if (!empty($this->design->backgroundimage)) { + + $this->out->elementStart('li', array('id' => + 'design_background-image_onoff')); + + $this->out->element('img', array('src' => + Design::url($this->design->backgroundimage))); + + $attrs = array('name' => 'design_background-image_onoff', + 'type' => 'radio', + 'id' => 'design_background-image_on', + 'class' => 'radio', + 'value' => 'on'); + + if ($this->design->disposition & BACKGROUND_ON) { + $attrs['checked'] = 'checked'; + } + + $this->out->element('input', $attrs); + + $this->out->element('label', array('for' => 'design_background-image_on', + 'class' => 'radio'), + // TRANS: Radio button on profile design page that will enable use of the uploaded profile image. + _m('RADIO', 'On')); + + $attrs = array('name' => 'design_background-image_onoff', + 'type' => 'radio', + 'id' => 'design_background-image_off', + 'class' => 'radio', + 'value' => 'off'); + + if ($this->design->disposition & BACKGROUND_OFF) { + $attrs['checked'] = 'checked'; + } + + $this->out->element('input', $attrs); + + $this->out->element('label', array('for' => 'design_background-image_off', + 'class' => 'radio'), + // TRANS: Radio button on profile design page that will disable use of the uploaded profile image. + _m('RADIO', 'Off')); + // TRANS: Form guide for a set of radio buttons on the profile design page that will enable or disable + // TRANS: use of the uploaded profile image. + $this->out->element('p', 'form_guide', _('Turn background image on or off.')); + $this->out->elementEnd('li'); + + $this->out->elementStart('li'); + $this->out->checkbox('design_background-image_repeat', + // TRANS: Checkbox label on profile design page that will cause the profile image to be tiled. + _('Tile background image'), + ($this->design->disposition & BACKGROUND_TILE) ? true : false); + $this->out->elementEnd('li'); + } + + $this->out->elementEnd('ul'); + } + + function colourData() + { + $this->out->elementStart('ul', 'form_data'); + + try { + + $bgcolor = new WebColor($this->design->backgroundcolor); + + $this->out->elementStart('li'); + // TRANS: Label on profile design page for setting a profile page background colour. + $this->out->element('label', array('for' => 'swatch-1'), _('Background')); + $this->out->element('input', array('name' => 'design_background', + 'type' => 'text', + 'id' => 'swatch-1', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '')); + $this->out->elementEnd('li'); + + $ccolor = new WebColor($this->design->contentcolor); + + $this->out->elementStart('li'); + // TRANS: Label on profile design page for setting a profile page content colour. + $this->out->element('label', array('for' => 'swatch-2'), _('Content')); + $this->out->element('input', array('name' => 'design_content', + 'type' => 'text', + 'id' => 'swatch-2', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '')); + $this->out->elementEnd('li'); + + $sbcolor = new WebColor($this->design->sidebarcolor); + + $this->out->elementStart('li'); + // TRANS: Label on profile design page for setting a profile page sidebar colour. + $this->out->element('label', array('for' => 'swatch-3'), _('Sidebar')); + $this->out->element('input', array('name' => 'design_sidebar', + 'type' => 'text', + 'id' => 'swatch-3', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '')); + $this->out->elementEnd('li'); + + $tcolor = new WebColor($this->design->textcolor); + + $this->out->elementStart('li'); + // TRANS: Label on profile design page for setting a profile page text colour. + $this->out->element('label', array('for' => 'swatch-4'), _('Text')); + $this->out->element('input', array('name' => 'design_text', + 'type' => 'text', + 'id' => 'swatch-4', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '')); + $this->out->elementEnd('li'); + + $lcolor = new WebColor($this->design->linkcolor); + + $this->out->elementStart('li'); + // TRANS: Label on profile design page for setting a profile page links colour. + $this->out->element('label', array('for' => 'swatch-5'), _('Links')); + $this->out->element('input', array('name' => 'design_links', + 'type' => 'text', + 'id' => 'swatch-5', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '')); + $this->out->elementEnd('li'); + + } catch (WebColorException $e) { + common_log(LOG_ERR, 'Bad color values in design ID: ' .$this->design->id); + } + + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Button text on profile design page to save settings. + $this->out->submit('save', _m('BUTTON','Save'), 'submit form_action-secondary', + // TRANS: Title for button on profile design page to save settings. + 'save', _('Save design')); + } +} diff --git a/lib/designsettings.php b/lib/designsettings.php index 90296a64d..d0601c553 100644 --- a/lib/designsettings.php +++ b/lib/designsettings.php @@ -84,195 +84,9 @@ class DesignSettingsAction extends AccountSettingsAction */ function showDesignForm($design) { - $this->elementStart('form', array('method' => 'post', - 'enctype' => 'multipart/form-data', - 'id' => 'form_settings_design', - 'class' => 'form_settings', - 'action' => $this->submitaction)); - $this->elementStart('fieldset'); - $this->hidden('token', common_session_token()); - - $this->elementStart('fieldset', array('id' => - 'settings_design_background-image')); - // TRANS: Fieldset legend on profile design page. - $this->element('legend', null, _('Change background image')); - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->element('label', array('for' => 'design_background-image_file'), - // TRANS: Label in form on profile design page. - // TRANS: Field contains file name on user's computer that could be that user's custom profile background image. - _('Upload file')); - $this->element('input', array('name' => 'design_background-image_file', - 'type' => 'file', - 'id' => 'design_background-image_file')); - // TRANS: Instructions for form on profile design page. - $this->element('p', 'form_guide', _('You can upload your personal ' . - 'background image. The maximum file size is 2MB.')); - $this->element('input', array('name' => 'MAX_FILE_SIZE', - 'type' => 'hidden', - 'id' => 'MAX_FILE_SIZE', - 'value' => ImageFile::maxFileSizeInt())); - $this->elementEnd('li'); - - if (!empty($design->backgroundimage)) { - $this->elementStart('li', array('id' => - 'design_background-image_onoff')); - - $this->element('img', array('src' => - Design::url($design->backgroundimage))); - - $attrs = array('name' => 'design_background-image_onoff', - 'type' => 'radio', - 'id' => 'design_background-image_on', - 'class' => 'radio', - 'value' => 'on'); - - if ($design->disposition & BACKGROUND_ON) { - $attrs['checked'] = 'checked'; - } - - $this->element('input', $attrs); - - $this->element('label', array('for' => 'design_background-image_on', - 'class' => 'radio'), - // TRANS: Radio button on profile design page that will enable use of the uploaded profile image. - _m('RADIO','On')); - - $attrs = array('name' => 'design_background-image_onoff', - 'type' => 'radio', - 'id' => 'design_background-image_off', - 'class' => 'radio', - 'value' => 'off'); - - if ($design->disposition & BACKGROUND_OFF) { - $attrs['checked'] = 'checked'; - } - - $this->element('input', $attrs); - - $this->element('label', array('for' => 'design_background-image_off', - 'class' => 'radio'), - // TRANS: Radio button on profile design page that will disable use of the uploaded profile image. - _m('RADIO','Off')); - // TRANS: Form guide for a set of radio buttons on the profile design page that will enable or disable - // TRANS: use of the uploaded profile image. - $this->element('p', 'form_guide', _('Turn background image on or off.')); - $this->elementEnd('li'); - - $this->elementStart('li'); - $this->checkbox('design_background-image_repeat', - // TRANS: Checkbox label on profile design page that will cause the profile image to be tiled. - _('Tile background image'), - ($design->disposition & BACKGROUND_TILE) ? true : false); - $this->elementEnd('li'); - } - - $this->elementEnd('ul'); - $this->elementEnd('fieldset'); - - $this->elementStart('fieldset', array('id' => 'settings_design_color')); - // TRANS: Fieldset legend on profile design page to change profile page colours. - $this->element('legend', null, _('Change colours')); - $this->elementStart('ul', 'form_data'); - - try { - $bgcolor = new WebColor($design->backgroundcolor); - - $this->elementStart('li'); - // TRANS: Label on profile design page for setting a profile page background colour. - $this->element('label', array('for' => 'swatch-1'), _('Background')); - $this->element('input', array('name' => 'design_background', - 'type' => 'text', - 'id' => 'swatch-1', - 'class' => 'swatch', - 'maxlength' => '7', - 'size' => '7', - 'value' => '')); - $this->elementEnd('li'); - - $ccolor = new WebColor($design->contentcolor); - - $this->elementStart('li'); - // TRANS: Label on profile design page for setting a profile page content colour. - $this->element('label', array('for' => 'swatch-2'), _('Content')); - $this->element('input', array('name' => 'design_content', - 'type' => 'text', - 'id' => 'swatch-2', - 'class' => 'swatch', - 'maxlength' => '7', - 'size' => '7', - 'value' => '')); - $this->elementEnd('li'); - - $sbcolor = new WebColor($design->sidebarcolor); - - $this->elementStart('li'); - // TRANS: Label on profile design page for setting a profile page sidebar colour. - $this->element('label', array('for' => 'swatch-3'), _('Sidebar')); - $this->element('input', array('name' => 'design_sidebar', - 'type' => 'text', - 'id' => 'swatch-3', - 'class' => 'swatch', - 'maxlength' => '7', - 'size' => '7', - 'value' => '')); - $this->elementEnd('li'); - - $tcolor = new WebColor($design->textcolor); - - $this->elementStart('li'); - // TRANS: Label on profile design page for setting a profile page text colour. - $this->element('label', array('for' => 'swatch-4'), _('Text')); - $this->element('input', array('name' => 'design_text', - 'type' => 'text', - 'id' => 'swatch-4', - 'class' => 'swatch', - 'maxlength' => '7', - 'size' => '7', - 'value' => '')); - $this->elementEnd('li'); - - $lcolor = new WebColor($design->linkcolor); - - $this->elementStart('li'); - // TRANS: Label on profile design page for setting a profile page links colour. - $this->element('label', array('for' => 'swatch-5'), _('Links')); - $this->element('input', array('name' => 'design_links', - 'type' => 'text', - 'id' => 'swatch-5', - 'class' => 'swatch', - 'maxlength' => '7', - 'size' => '7', - 'value' => '')); - $this->elementEnd('li'); - - } catch (WebColorException $e) { - common_log(LOG_ERR, 'Bad color values in design ID: ' .$design->id); - } + $form = new DesignForm($this, $design, $this->selfUrl()); + $form->show(); - $this->elementEnd('ul'); - $this->elementEnd('fieldset'); - - // TRANS: Button text on profile design page to immediately reset all colour settings to default. - $this->submit('defaults', _('Use defaults'), 'submit form_action-default', - // TRANS: Title for button on profile design page to reset all colour settings to default. - 'defaults', _('Restore default designs')); - - $this->element('input', array('id' => 'settings_design_reset', - 'type' => 'reset', - // TRANS: Button text on profile design page to reset all colour settings to default without saving. - 'value' => _m('BUTTON','Reset'), - 'class' => 'submit form_action-primary', - // TRANS: Title for button on profile design page to reset all colour settings to default without saving. - 'title' => _('Reset back to default'))); - - // TRANS: Button text on profile design page to save settings. - $this->submit('save', _m('BUTTON','Save'), 'submit form_action-secondary', - // TRANS: Title for button on profile design page to save settings. - 'save', _('Save design')); - - $this->elementEnd('fieldset'); - $this->elementEnd('form'); } /** @@ -362,22 +176,21 @@ class DesignSettingsAction extends AccountSettingsAction // associated with the Design rather than the User was worth // it. -- Zach - if ($_FILES['design_background-image_file']['error'] == - UPLOAD_ERR_OK) { + if (array_key_exists('design_background-image_file', $_FILES) && + $_FILES['design_background-image_file']['error'] == UPLOAD_ERR_OK) { $filepath = null; try { - $imagefile = - ImageFile::fromUpload('design_background-image_file'); + $imagefile = ImageFile::fromUpload('design_background-image_file'); } catch (Exception $e) { $this->showForm($e->getMessage()); return; } $filename = Design::filename($design->id, - image_type_to_extension($imagefile->type), - common_timestamp()); + image_type_to_extension($imagefile->type), + common_timestamp()); $filepath = Design::path($filename); diff --git a/lib/framework.php b/lib/framework.php new file mode 100644 index 000000000..70987e086 --- /dev/null +++ b/lib/framework.php @@ -0,0 +1,154 @@ +<?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); } + +define('STATUSNET_VERSION', '0.9.6'); +define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility + +define('STATUSNET_CODENAME', 'Man on the Moon'); + +define('AVATAR_PROFILE_SIZE', 96); +define('AVATAR_STREAM_SIZE', 48); +define('AVATAR_MINI_SIZE', 24); + +define('NOTICES_PER_PAGE', 20); +define('PROFILES_PER_PAGE', 20); + +define('FOREIGN_NOTICE_SEND', 1); +define('FOREIGN_NOTICE_RECV', 2); +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... +if (!function_exists('dl')) { + // 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; + } + } +} + +# global configuration object + +require_once('PEAR.php'); +require_once('PEAR/Exception.php'); +require_once('DB/DataObject.php'); +require_once('DB/DataObject/Cast.php'); # for dates + +require_once(INSTALLDIR.'/lib/language.php'); + +// This gets included before the config file, so that admin code and plugins +// can use it + +require_once(INSTALLDIR.'/lib/event.php'); +require_once(INSTALLDIR.'/lib/plugin.php'); + +function addPlugin($name, $attrs = null) +{ + return StatusNet::addPlugin($name, $attrs); +} + +function _have_config() +{ + return StatusNet::haveConfig(); +} + +function __autoload($cls) +{ + if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) { + require_once(INSTALLDIR.'/classes/' . $cls . '.php'); + } else if (file_exists(INSTALLDIR.'/lib/' . strtolower($cls) . '.php')) { + require_once(INSTALLDIR.'/lib/' . strtolower($cls) . '.php'); + } else if (mb_substr($cls, -6) == 'Action' && + file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php')) { + require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); + } else if ($cls == 'OAuthRequest') { + require_once('OAuth.php'); + } else { + Event::handle('Autoload', array(&$cls)); + } +} + +// XXX: how many of these could be auto-loaded on use? +// XXX: note that these files should not use config options +// at compile time since DB config options are not yet loaded. + +require_once 'Validate.php'; +require_once 'markdown.php'; + +// XXX: other formats here + +/** + * Avoid the NICKNAME_FMT constant; use the Nickname class instead. + * + * Nickname::DISPLAY_FMT is more suitable for inserting into regexes; + * note that it includes the [] and repeating bits, so should be wrapped + * directly in a capture paren usually. + * + * For validation, use Nickname::normalize(), Nickname::isValid() etc. + * + * @deprecated + */ +define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER); + +require_once INSTALLDIR.'/lib/util.php'; +require_once INSTALLDIR.'/lib/action.php'; +require_once INSTALLDIR.'/lib/mail.php'; +require_once INSTALLDIR.'/lib/subs.php'; + +require_once INSTALLDIR.'/lib/clientexception.php'; +require_once INSTALLDIR.'/lib/serverexception.php'; + + +//set PEAR error handling to use regular PHP exceptions +function PEAR_ErrorToPEAR_Exception($err) +{ + //DB_DataObject throws error when an empty set would be returned + //That behavior is weird, and not how the rest of StatusNet works. + //So just ignore those errors. + if ($err->getCode() == DB_DATAOBJECT_ERROR_NODATA) { + return; + } + if ($err->getCode()) { + throw new PEAR_Exception($err->getMessage(), $err->getCode()); + } + throw new PEAR_Exception($err->getMessage()); +} +PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception'); diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index b341d1495..fdb693f92 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -184,7 +184,7 @@ class HTMLOutputter extends XMLOutputter $attrs = array('name' => $id, 'type' => 'text', 'id' => $id); - if ($value) { + if (!is_null($value)) { // value can be 0 or '' $attrs['value'] = $value; } $this->element('input', $attrs); diff --git a/lib/imchannel.php b/lib/imchannel.php new file mode 100644 index 000000000..61355a429 --- /dev/null +++ b/lib/imchannel.php @@ -0,0 +1,104 @@ +<?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); } + +class IMChannel extends Channel +{ + + var $imPlugin; + + function source() + { + return $imPlugin->transport; + } + + function __construct($imPlugin) + { + $this->imPlugin = $imPlugin; + } + + function on($user) + { + return $this->setNotify($user, 1); + } + + function off($user) + { + return $this->setNotify($user, 0); + } + + function output($user, $text) + { + $text = '['.common_config('site', 'name') . '] ' . $text; + $this->imPlugin->sendMessage($this->imPlugin->getScreenname($user), $text); + } + + function error($user, $text) + { + $text = '['.common_config('site', 'name') . '] ' . $text; + + $screenname = $this->imPlugin->getScreenname($user); + if($screenname){ + $this->imPlugin->sendMessage($screenname, $text); + return true; + }else{ + common_log(LOG_ERR, + 'Could not send error message to user ' . common_log_objstring($user) . + ' on transport ' . $this->imPlugin->transport .' : user preference does not exist'); + return false; + } + } + + function setNotify($user, $notify) + { + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->transport = $this->imPlugin->transport; + $user_im_prefs->user_id = $user->id; + if($user_im_prefs->find() && $user_im_prefs->fetch()){ + if($user_im_prefs->notify == $notify){ + //notify is already set the way they want + return true; + }else{ + $original = clone($user_im_prefs); + $user_im_prefs->notify = $notify; + $result = $user_im_prefs->update($original); + + if (!$result) { + $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError'); + common_log(LOG_ERR, + 'Could not set notify flag to ' . $notify . + ' for user ' . common_log_objstring($user) . + ' on transport ' . $this->imPlugin->transport .' : ' . $last_error->message); + return false; + } else { + common_log(LOG_INFO, + 'User ' . $user->nickname . ' set notify flag to ' . $notify); + return true; + } + } + }else{ + common_log(LOG_ERR, + 'Could not set notify flag to ' . $notify . + ' for user ' . common_log_objstring($user) . + ' on transport ' . $this->imPlugin->transport .' : user preference does not exist'); + return false; + } + } +} diff --git a/lib/immanager.php b/lib/immanager.php new file mode 100644 index 000000000..9563a5326 --- /dev/null +++ b/lib/immanager.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); } + +/** + * IKM background connection manager for IM-using queue handlers, + * allowing them to send outgoing messages on the right connection. + * + * In a multi-site queuedaemon.php run, one connection will be instantiated + * for each site being handled by the current process that has IM enabled. + * + * Implementations that extend this class will likely want to: + * 1) override start() with their connection process. + * 2) override handleInput() with what to do when data is waiting on + * one of the sockets + * 3) override idle($timeout) to do keepalives (if necessary) + * 4) implement send_raw_message() to send raw data that ImPlugin::enqueueOutgoingRaw + * enqueued + */ + +abstract class ImManager extends IoManager +{ + abstract function send_raw_message($data); + + function __construct($imPlugin) + { + $this->plugin = $imPlugin; + $this->plugin->imManager = $this; + } + + /** + * Fetch the singleton manager for the current site. + * @return mixed ImManager, or false if unneeded + */ + public static function get() + { + throw new Exception('ImManager should be created using it\'s constructor, not the static get method'); + } +} diff --git a/lib/implugin.php b/lib/implugin.php new file mode 100644 index 000000000..2811e7d64 --- /dev/null +++ b/lib/implugin.php @@ -0,0 +1,626 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Superclass for plugins that do instant messaging + * + * 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 + * + * Implementations will likely want to override onStartIoManagerClasses() so that their + * IO manager is used + * + * @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 ImPlugin extends Plugin +{ + //name of this IM transport + public $transport = null; + //list of screennames that should get all public notices + public $public = array(); + + /** + * normalize a screenname for comparison + * + * @param string $screenname screenname to normalize + * + * @return string an equivalent screenname in normalized form + */ + abstract function normalize($screenname); + + /** + * validate (ensure the validity of) a screenname + * + * @param string $screenname screenname to validate + * + * @return boolean + */ + abstract function validate($screenname); + + /** + * get the internationalized/translated display name of this IM service + * + * @return string + */ + abstract function getDisplayName(); + + /** + * send a single notice to a given screenname + * The implementation should put raw data, ready to send, into the outgoing + * queue using enqueueOutgoingRaw() + * + * @param string $screenname screenname to send to + * @param Notice $notice notice to send + * + * @return boolean success value + */ + function sendNotice($screenname, $notice) + { + return $this->sendMessage($screenname, $this->formatNotice($notice)); + } + + /** + * send a message (text) to a given screenname + * The implementation should put raw data, ready to send, into the outgoing + * queue using enqueueOutgoingRaw() + * + * @param string $screenname screenname to send to + * @param Notice $body text to send + * + * @return boolean success value + */ + abstract function sendMessage($screenname, $body); + + /** + * receive a raw message + * Raw IM data is taken from the incoming queue, and passed to this function. + * It should parse the raw message and call handleIncoming() + * + * Returning false may CAUSE REPROCESSING OF THE QUEUE ITEM, and should + * be used for temporary failures only. For permanent failures such as + * unrecognized addresses, return true to indicate your processing has + * completed. + * + * @param object $data raw IM data + * + * @return boolean true if processing completed, false for temporary failures + */ + abstract function receiveRawMessage($data); + + /** + * get the screenname of the daemon that sends and receives message for this service + * + * @return string screenname of this plugin + */ + abstract function daemonScreenname(); + + /** + * get the microid uri of a given screenname + * + * @param string $screenname screenname + * + * @return string microid uri + */ + function microiduri($screenname) + { + return $this->transport . ':' . $screenname; + } + //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - MISC ========================\ + + /** + * Put raw message data (ready to send) into the outgoing queue + * + * @param object $data + */ + function enqueueOutgoingRaw($data) + { + $qm = QueueManager::get(); + $qm->enqueue($data, $this->transport . '-out'); + } + + /** + * Put raw message data (received, ready to be processed) into the incoming queue + * + * @param object $data + */ + function enqueueIncomingRaw($data) + { + $qm = QueueManager::get(); + $qm->enqueue($data, $this->transport . '-in'); + } + + /** + * given a screenname, get the corresponding user + * + * @param string $screenname + * + * @return User user + */ + function getUser($screenname) + { + $user_im_prefs = $this->getUserImPrefsFromScreenname($screenname); + if($user_im_prefs){ + $user = User::staticGet('id', $user_im_prefs->user_id); + $user_im_prefs->free(); + return $user; + }else{ + return false; + } + } + + /** + * given a screenname, get the User_im_prefs object for this transport + * + * @param string $screenname + * + * @return User_im_prefs user_im_prefs + */ + function getUserImPrefsFromScreenname($screenname) + { + $user_im_prefs = User_im_prefs::pkeyGet( + array('transport' => $this->transport, + 'screenname' => $this->normalize($screenname))); + if ($user_im_prefs) { + return $user_im_prefs; + } else { + return false; + } + } + + /** + * given a User, get their screenname + * + * @param User $user + * + * @return string screenname of that user + */ + function getScreenname($user) + { + $user_im_prefs = $this->getUserImPrefsFromUser($user); + if ($user_im_prefs) { + return $user_im_prefs->screenname; + } else { + return false; + } + } + + /** + * given a User, get their User_im_prefs + * + * @param User $user + * + * @return User_im_prefs user_im_prefs of that user + */ + function getUserImPrefsFromUser($user) + { + $user_im_prefs = User_im_prefs::pkeyGet( + array('transport' => $this->transport, + 'user_id' => $user->id)); + if ($user_im_prefs){ + return $user_im_prefs; + } else { + return false; + } + } + //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - SENDING ========================\ + /** + * Send a message to a given screenname from the site + * + * @param string $screenname screenname to send the message to + * @param string $msg message contents to send + * + * @param boolean success + */ + protected function sendFromSite($screenname, $msg) + { + $text = '['.common_config('site', 'name') . '] ' . $msg; + $this->sendMessage($screenname, $text); + } + + /** + * send a confirmation code to a user + * + * @param string $screenname screenname sending to + * @param string $code the confirmation code + * @param User $user user sending to + * + * @return boolean success value + */ + function sendConfirmationCode($screenname, $code, $user) + { + $body = sprintf(_('User "%s" on %s has said that your %s screenname belongs to them. ' . + 'If that\'s true, you can confirm by clicking on this URL: ' . + '%s' . + ' . (If you cannot click it, copy-and-paste it into the ' . + 'address bar of your browser). If that user isn\'t you, ' . + 'or if you didn\'t request this confirmation, just ignore this message.'), + $user->nickname, common_config('site', 'name'), $this->getDisplayName(), common_local_url('confirmaddress', array('code' => $code))); + + return $this->sendMessage($screenname, $body); + } + + /** + * send a notice to all public listeners + * + * For notices that are generated on the local system (by users), we can optionally + * forward them to remote listeners by XMPP. + * + * @param Notice $notice notice to broadcast + * + * @return boolean success flag + */ + + function publicNotice($notice) + { + // Now, users who want everything + + // FIXME PRIV don't send out private messages here + // XXX: should we send out non-local messages if public,localonly + // = false? I think not + + foreach ($this->public as $screenname) { + common_log(LOG_INFO, + 'Sending notice ' . $notice->id . + ' to public listener ' . $screenname, + __FILE__); + $this->sendNotice($screenname, $notice); + } + + return true; + } + + /** + * broadcast a notice to all subscribers and reply recipients + * + * This function will send a notice to all subscribers on the local server + * who have IM addresses, and have IM notification enabled, and + * have this subscription enabled for IM. It also sends the notice to + * all recipients of @-replies who have IM addresses and IM notification + * enabled. This is really the heart of IM distribution in StatusNet. + * + * @param Notice $notice The notice to broadcast + * + * @return boolean success flag + */ + + function broadcastNotice($notice) + { + + $ni = $notice->whoGets(); + + foreach ($ni as $user_id => $reason) { + $user = User::staticGet($user_id); + if (empty($user)) { + // either not a local user, or just not found + continue; + } + $user_im_prefs = $this->getUserImPrefsFromUser($user); + if(!$user_im_prefs || !$user_im_prefs->notify){ + continue; + } + + switch ($reason) { + case NOTICE_INBOX_SOURCE_REPLY: + if (!$user_im_prefs->replies) { + 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 notice ' . $notice->id . ' to ' . $user_im_prefs->screenname, + __FILE__); + $this->sendNotice($user_im_prefs->screenname, $notice); + $user_im_prefs->free(); + } + + return true; + } + + /** + * makes a plain-text formatted version of a notice, suitable for IM distribution + * + * @param Notice $notice notice being sent + * + * @return string plain-text version of the notice, with user nickname prefixed + */ + + function formatNotice($notice) + { + $profile = $notice->getProfile(); + return $profile->nickname . ': ' . $notice->content . ' [' . $notice->id . ']'; + } + //========================UTILITY FUNCTIONS USEFUL TO IMPLEMENTATIONS - RECEIVING ========================\ + + /** + * Attempt to handle a message as a command + * @param User $user user the message is from + * @param string $body message text + * @return boolean true if the message was a command and was executed, false if it was not a command + */ + protected function handleCommand($user, $body) + { + $inter = new CommandInterpreter(); + $cmd = $inter->handle_command($user, $body); + if ($cmd) { + $chan = new IMChannel($this); + $cmd->execute($chan); + return true; + } else { + return false; + } + } + + /** + * Is some text an autoreply message? + * @param string $txt message text + * @return boolean true if autoreply + */ + protected function isAutoreply($txt) + { + if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) { + return true; + } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) { + return true; + } else { + return false; + } + } + + /** + * Is some text an OTR message? + * @param string $txt message text + * @return boolean true if OTR + */ + protected function isOtr($txt) + { + if (preg_match('/^\?OTR/', $txt)) { + return true; + } else { + return false; + } + } + + /** + * Helper for handling incoming messages + * Your incoming message handler will probably want to call this function + * + * @param string $from screenname the message was sent from + * @param string $message message contents + * + * @param boolean success + */ + protected function handleIncoming($from, $notice_text) + { + $user = $this->getUser($from); + // For common_current_user to work + global $_cur; + $_cur = $user; + + if (!$user) { + $this->sendFromSite($from, 'Unknown user; go to ' . + common_local_url('imsettings') . + ' to add your address to your account'); + common_log(LOG_WARNING, 'Message from unknown user ' . $from); + return; + } + if ($this->handleCommand($user, $notice_text)) { + common_log(LOG_INFO, "Command message by $from handled."); + return; + } else if ($this->isAutoreply($notice_text)) { + common_log(LOG_INFO, 'Ignoring auto reply from ' . $from); + return; + } else if ($this->isOtr($notice_text)) { + common_log(LOG_INFO, 'Ignoring OTR from ' . $from); + return; + } else { + + common_log(LOG_INFO, 'Posting a notice from ' . $user->nickname); + + $this->addNotice($from, $user, $notice_text); + } + + $user->free(); + unset($user); + unset($_cur); + unset($message); + } + + /** + * Helper for handling incoming messages + * Your incoming message handler will probably want to call this function + * + * @param string $from screenname the message was sent from + * @param string $message message contents + * + * @param boolean success + */ + protected function addNotice($screenname, $user, $body) + { + $body = trim(strip_tags($body)); + $content_shortened = common_shorten_links($body); + if (Notice::contentTooLong($content_shortened)) { + $this->sendFromSite($screenname, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'), + Notice::maxContent(), + mb_strlen($content_shortened))); + return; + } + + try { + $notice = Notice::saveNew($user->id, $content_shortened, $this->transport); + } catch (Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + $this->sendFromSite($from, $e->getMessage()); + return; + } + + common_log(LOG_INFO, + 'Added notice ' . $notice->id . ' from user ' . $user->nickname); + $notice->free(); + unset($notice); + } + + //========================EVENT HANDLERS========================\ + + /** + * Register notice queue handler + * + * @param QueueManager $manager + * + * @return boolean hook return + */ + function onEndInitializeQueueManager($manager) + { + $manager->connect($this->transport . '-in', new ImReceiverQueueHandler($this), 'im'); + $manager->connect($this->transport, new ImQueueHandler($this)); + $manager->connect($this->transport . '-out', new ImSenderQueueHandler($this), 'im'); + return true; + } + + function onStartImDaemonIoManagers(&$classes) + { + //$classes[] = new ImManager($this); // handles sending/receiving/pings/reconnects + return true; + } + + function onStartEnqueueNotice($notice, &$transports) + { + $profile = Profile::staticGet($notice->profile_id); + + if (!$profile) { + common_log(LOG_WARNING, 'Refusing to broadcast notice with ' . + 'unknown profile ' . common_log_objstring($notice), + __FILE__); + }else{ + $transports[] = $this->transport; + } + + return true; + } + + function onEndShowHeadElements($action) + { + $aname = $action->trimmed('action'); + + if ($aname == 'shownotice') { + + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->user_id = $action->profile->id; + $user_im_prefs->transport = $this->transport; + + if ($user_im_prefs->find() && $user_im_prefs->fetch() && $user_im_prefs->microid && $action->notice->uri) { + $id = new Microid($this->microiduri($user_im_prefs->screenname), + $action->notice->uri); + $action->element('meta', array('name' => 'microid', + 'content' => $id->toString())); + } + + } else if ($aname == 'showstream') { + + $user_im_prefs = new User_im_prefs(); + $user_im_prefs->user_id = $action->user->id; + $user_im_prefs->transport = $this->transport; + + if ($user_im_prefs->find() && $user_im_prefs->fetch() && $user_im_prefs->microid && $action->profile->profileurl) { + $id = new Microid($this->microiduri($user_im_prefs->screenname), + $action->selfUrl()); + $action->element('meta', array('name' => 'microid', + 'content' => $id->toString())); + } + } + } + + function onNormalizeImScreenname($transport, &$screenname) + { + if($transport == $this->transport) + { + $screenname = $this->normalize($screenname); + return false; + } + } + + function onValidateImScreenname($transport, $screenname, &$valid) + { + if($transport == $this->transport) + { + $valid = $this->validate($screenname); + return false; + } + } + + function onGetImTransports(&$transports) + { + $transports[$this->transport] = array( + 'display' => $this->getDisplayName(), + 'daemonScreenname' => $this->daemonScreenname()); + } + + function onSendImConfirmationCode($transport, $screenname, $code, $user) + { + if($transport == $this->transport) + { + $this->sendConfirmationCode($screenname, $code, $user); + return false; + } + } + + function onUserDeleteRelated($user, &$tables) + { + $tables[] = 'User_im_prefs'; + return true; + } + + function initialize() + { + if( ! common_config('queue', 'enabled')) + { + throw new ServerException("Queueing must be enabled to use IM plugins"); + } + + if(is_null($this->transport)){ + throw new ServerException('transport cannot be null'); + } + } +} diff --git a/lib/jabberqueuehandler.php b/lib/imqueuehandler.php index d6b4b7416..9c35890c6 100644 --- a/lib/jabberqueuehandler.php +++ b/lib/imqueuehandler.php @@ -17,31 +17,32 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +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. + * Common superclass for all IM sending queue handlers. */ -class JabberQueueHandler extends QueueHandler -{ - var $conn = null; - function transport() +class ImQueueHandler extends QueueHandler +{ + function __construct($plugin) { - return 'jabber'; + $this->plugin = $plugin; } + /** + * Handle a notice + * @param Notice $notice + * @return boolean success + */ function handle($notice) { - require_once(INSTALLDIR.'/lib/jabber.php'); - try { - return jabber_broadcast_notice($notice); - } catch (XMPPHP_Exception $e) { - common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); - return false; + $this->plugin->broadcastNotice($notice); + if ($notice->is_local == Notice::LOCAL_PUBLIC || + $notice->is_local == Notice::LOCAL_NONPUBLIC) { + $this->plugin->publicNotice($notice); } + return true; } + } diff --git a/lib/publicqueuehandler.php b/lib/imreceiverqueuehandler.php index a497d1385..aa4a663b7 100644 --- a/lib/publicqueuehandler.php +++ b/lib/imreceiverqueuehandler.php @@ -17,29 +17,26 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } /** - * Queue handler for pushing new notices to public XMPP subscribers. + * Common superclass for all IM receiving queue handlers. */ -class PublicQueueHandler extends QueueHandler -{ - function transport() +class ImReceiverQueueHandler extends QueueHandler +{ + function __construct($plugin) { - return 'public'; + $this->plugin = $plugin; } - function handle($notice) + /** + * Handle incoming IM data sent by a user to the IM bot + * @param object $data + * @return boolean success + */ + function handle($data) { - require_once(INSTALLDIR.'/lib/jabber.php'); - try { - return jabber_public_notice($notice); - } catch (XMPPHP_Exception $e) { - common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); - return false; - } + return $this->plugin->receiveRawMessage($data); } } diff --git a/lib/imsenderqueuehandler.php b/lib/imsenderqueuehandler.php new file mode 100644 index 000000000..790dd7b10 --- /dev/null +++ b/lib/imsenderqueuehandler.php @@ -0,0 +1,43 @@ +<?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); } + +/** + * Common superclass for all IM sending queue handlers. + */ + +class ImSenderQueueHandler extends QueueHandler +{ + function __construct($plugin) + { + $this->plugin = $plugin; + } + + /** + * Handle outgoing IM data to be sent from the bot to a user + * @param object $data + * @return boolean success + */ + function handle($data) + { + return $this->plugin->imManager->send_raw_message($data); + } +} + diff --git a/lib/installer.php b/lib/installer.php index a9d809011..ad1989f4e 100644 --- a/lib/installer.php +++ b/lib/installer.php @@ -2,7 +2,7 @@ /** * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * 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 @@ -32,9 +32,10 @@ * @author Sarven Capadisli <csarven@status.net> * @author Tom Adams <tom@holizz.com> * @author Zach Copley <zach@status.net> + * @copyright 2009-2010 StatusNet, Inc http://status.net * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @license GNU Affero General Public License http://www.gnu.org/licenses/ - * @version 0.9.x + * @version 1.0.x * @link http://status.net */ @@ -53,12 +54,12 @@ abstract class Installer 'mysql' => array( 'name' => 'MySQL', 'check_module' => 'mysqli', - 'installer' => 'mysql_db_installer', + 'scheme' => 'mysqli', // DSN prefix for PEAR::DB ), 'pgsql' => array( 'name' => 'PostgreSQL', 'check_module' => 'pgsql', - 'installer' => 'pgsql_db_installer', + 'scheme' => 'pgsql', // DSN prefix for PEAR::DB ), ); @@ -254,6 +255,7 @@ abstract class Installer * Set up the database with the appropriate function for the selected type... * Saves database info into $this->db. * + * @fixme escape things in the connection string in case we have a funny pass etc * @return mixed array of database connection params on success, false on failure */ function setupDatabase() @@ -261,134 +263,97 @@ abstract class Installer if ($this->db) { throw new Exception("Bad order of operations: DB already set up."); } - $method = self::$dbModules[$this->dbtype]['installer']; - $db = call_user_func(array($this, $method), - $this->host, - $this->database, - $this->username, - $this->password); - $this->db = $db; - return $this->db; - } - - /** - * Set up a database on PostgreSQL. - * Will output status updates during the operation. - * - * @param string $host - * @param string $database - * @param string $username - * @param string $password - * @return mixed array of database connection params on success, false on failure - * - * @fixme escape things in the connection string in case we have a funny pass etc - */ - function Pgsql_Db_installer($host, $database, $username, $password) - { - $connstring = "dbname=$database host=$host user=$username"; - - //No password would mean trust authentication used. - if (!empty($password)) { - $connstring .= " password=$password"; - } $this->updateStatus("Starting installation..."); - $this->updateStatus("Checking database..."); - $conn = pg_connect($connstring); - if ($conn ===false) { - $this->updateStatus("Failed to connect to database: $connstring"); - return false; + if (empty($this->password)) { + $auth = ''; + } else { + $auth = ":$this->password"; } + $scheme = self::$dbModules[$this->dbtype]['scheme']; + $dsn = "{$scheme}://{$this->username}{$auth}@{$this->host}/{$this->database}"; - //ensure database encoding is UTF8 - $record = pg_fetch_object(pg_query($conn, 'SHOW server_encoding')); - if ($record->server_encoding != 'UTF8') { - $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding)); - return false; + $this->updateStatus("Checking database..."); + $conn = $this->connectDatabase($dsn); + + // ensure database encoding is UTF8 + if ($this->dbtype == 'mysql') { + // @fixme utf8m4 support for mysql 5.5? + // Force the comms charset to utf8 for sanity + // This doesn't currently work. :P + //$conn->executes('set names utf8'); + } else if ($this->dbtype == 'pgsql') { + $record = $conn->getRow('SHOW server_encoding'); + if ($record->server_encoding != 'UTF8') { + $this->updateStatus("StatusNet requires UTF8 character encoding. Your database is ". htmlentities($record->server_encoding)); + return false; + } } - $this->updateStatus("Running database script..."); - //wrap in transaction; - pg_query($conn, 'BEGIN'); - $res = $this->runDbScript('statusnet_pg.sql', $conn, 'pgsql'); - - if ($res === false) { - $this->updateStatus("Can't run database script.", true); + $res = $this->updateStatus("Creating database tables..."); + if (!$this->createCoreTables($conn)) { + $this->updateStatus("Error creating tables.", true); return false; } + foreach (array('sms_carrier' => 'SMS carrier', 'notice_source' => 'notice source', 'foreign_services' => 'foreign service') as $scr => $name) { $this->updateStatus(sprintf("Adding %s data to database...", $name)); - $res = $this->runDbScript($scr.'.sql', $conn, 'pgsql'); + $res = $this->runDbScript($scr.'.sql', $conn); if ($res === false) { - $this->updateStatus(sprintf("Can't run %s script.", $name), true); + $this->updateStatus(sprintf("Can't run %d script.", $name), true); return false; } } - pg_query($conn, 'COMMIT'); - - if (empty($password)) { - $sqlUrl = "pgsql://$username@$host/$database"; - } else { - $sqlUrl = "pgsql://$username:$password@$host/$database"; - } - - $db = array('type' => 'pgsql', 'database' => $sqlUrl); + $db = array('type' => $this->dbtype, 'database' => $dsn); return $db; } /** - * Set up a database on MySQL. - * Will output status updates during the operation. - * - * @param string $host - * @param string $database - * @param string $username - * @param string $password - * @return mixed array of database connection params on success, false on failure - * - * @fixme escape things in the connection string in case we have a funny pass etc + * Open a connection to the database. + * + * @param <type> $dsn + * @return <type> */ - function Mysql_Db_installer($host, $database, $username, $password) + function connectDatabase($dsn) { - $this->updateStatus("Starting installation..."); - $this->updateStatus("Checking database..."); - - $conn = mysqli_init(); - if (!$conn->real_connect($host, $username, $password)) { - $this->updateStatus("Can't connect to server '$host' as '$username'.", true); - return false; - } - $this->updateStatus("Changing to database..."); - if (!$conn->select_db($database)) { - $this->updateStatus("Can't change to database.", true); - return false; - } + // @fixme move this someplace more sensible + //set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path()); + require_once 'DB.php'; + return DB::connect($dsn); + } - $this->updateStatus("Running database script..."); - $res = $this->runDbScript('statusnet.sql', $conn); - if ($res === false) { - $this->updateStatus("Can't run database script.", true); - return false; - } - foreach (array('sms_carrier' => 'SMS carrier', - 'notice_source' => 'notice source', - 'foreign_services' => 'foreign service') - as $scr => $name) { - $this->updateStatus(sprintf("Adding %s data to database...", $name)); - $res = $this->runDbScript($scr.'.sql', $conn); - if ($res === false) { - $this->updateStatus(sprintf("Can't run %d script.", $name), true); - return false; + /** + * Create core tables on the given database connection. + * + * @param DB_common $conn + */ + function createCoreTables(DB_common $conn) + { + $schema = Schema::get($conn); + $tableDefs = $this->getCoreSchema(); + foreach ($tableDefs as $name => $def) { + if (defined('DEBUG_INSTALLER')) { + echo " $name "; } + $schema->ensureTable($name, $def); } + return true; + } - $sqlUrl = "mysqli://$username:$password@$host/$database"; - $db = array('type' => 'mysql', 'database' => $sqlUrl); - return $db; + /** + * Fetch the core table schema definitions. + * + * @return array of table names => table def arrays + */ + function getCoreSchema() + { + $schema = array(); + include INSTALLDIR . '/db/core.php'; + return $schema; } /** @@ -463,13 +428,12 @@ abstract class Installer /** * Install schema into the database * - * @param string $filename location of database schema file - * @param dbconn $conn connection to database - * @param string $type type of database, currently mysql or pgsql + * @param string $filename location of database schema file + * @param DB_common $conn connection to database * * @return boolean - indicating success or failure */ - function runDbScript($filename, $conn, $type = 'mysqli') + function runDbScript($filename, DB_common $conn) { $sql = trim(file_get_contents(INSTALLDIR . '/db/' . $filename)); $stmts = explode(';', $sql); @@ -478,26 +442,12 @@ abstract class Installer if (!mb_strlen($stmt)) { continue; } - // FIXME: use PEAR::DB or PDO instead of our own switch - switch ($type) { - case 'mysqli': - $res = $conn->query($stmt); - if ($res === false) { - $error = $conn->error; - } - break; - case 'pgsql': - $res = pg_query($conn, $stmt); - if ($res === false) { - $error = pg_last_error(); - } - break; - default: - $this->updateStatus("runDbScript() error: unknown database type ". $type ." provided."); - } - if ($res === false) { + try { + $res = $conn->simpleQuery($stmt); + } catch (Exception $e) { + $error = $e->getMessage(); $this->updateStatus("ERROR ($error) for SQL '$stmt'"); - return $res; + return false; } } return true; @@ -510,9 +460,6 @@ abstract class Installer */ function registerInitialUser() { - define('STATUSNET', true); - define('LACONICA', true); // compatibility - require_once INSTALLDIR . '/lib/common.php'; $data = array('nickname' => $this->adminNick, @@ -559,10 +506,22 @@ abstract class Installer */ function doInstall() { - $this->db = $this->setupDatabase(); - - if (!$this->db) { - // database connection failed, do not move on to create config file. + $this->updateStatus("Initializing..."); + ini_set('display_errors', 1); + error_reporting(E_ALL); + define('STATUSNET', 1); + require_once INSTALLDIR . '/lib/framework.php'; + StatusNet::initDefaults($this->server, $this->path); + + try { + $this->db = $this->setupDatabase(); + if (!$this->db) { + // database connection failed, do not move on to create config file. + return false; + } + } catch (Exception $e) { + // Lower-level DB error! + $this->updateStatus("Database error: " . $e->getMessage(), true); return false; } diff --git a/lib/jabber.php b/lib/jabber.php deleted file mode 100644 index cdcfc4423..000000000 --- a/lib/jabber.php +++ /dev/null @@ -1,640 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * utility functions for Jabber/GTalk/XMPP messages - * - * 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 Network - * @package StatusNet - * @author Evan Prodromou <evan@status.net> - * @copyright 2008 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -require_once 'XMPPHP/XMPP.php'; - -/** - * Splits a Jabber ID (JID) into node, domain, and resource portions. - * - * Based on validation routine submitted by: - * @copyright 2009 Patrick Georgi <patrick@georgi-clan.de> - * @license Licensed under ISC-L, which is compatible with everything else that keeps the copyright notice intact. - * - * @param string $jid string to check - * - * @return array with "node", "domain", and "resource" indices - * @throws Exception if input is not valid - */ - -function jabber_split_jid($jid) -{ - $chars = ''; - /* the following definitions come from stringprep, Appendix C, - which is used in its entirety by nodeprop, Chapter 5, "Prohibited Output" */ - /* C1.1 ASCII space characters */ - $chars .= "\x{20}"; - /* C1.2 Non-ASCII space characters */ - $chars .= "\x{a0}\x{1680}\x{2000}-\x{200b}\x{202f}\x{205f}\x{3000a}"; - /* C2.1 ASCII control characters */ - $chars .= "\x{00}-\x{1f}\x{7f}"; - /* C2.2 Non-ASCII control characters */ - $chars .= "\x{80}-\x{9f}\x{6dd}\x{70f}\x{180e}\x{200c}\x{200d}\x{2028}\x{2029}\x{2060}-\x{2063}\x{206a}-\x{206f}\x{feff}\x{fff9}-\x{fffc}\x{1d173}-\x{1d17a}"; - /* C3 - Private Use */ - $chars .= "\x{e000}-\x{f8ff}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}"; - /* C4 - Non-character code points */ - $chars .= "\x{fdd0}-\x{fdef}\x{fffe}\x{ffff}\x{1fffe}\x{1ffff}\x{2fffe}\x{2ffff}\x{3fffe}\x{3ffff}\x{4fffe}\x{4ffff}\x{5fffe}\x{5ffff}\x{6fffe}\x{6ffff}\x{7fffe}\x{7ffff}\x{8fffe}\x{8ffff}\x{9fffe}\x{9ffff}\x{afffe}\x{affff}\x{bfffe}\x{bffff}\x{cfffe}\x{cffff}\x{dfffe}\x{dffff}\x{efffe}\x{effff}\x{ffffe}\x{fffff}\x{10fffe}\x{10ffff}"; - /* C5 - Surrogate codes */ - $chars .= "\x{d800}-\x{dfff}"; - /* C6 - Inappropriate for plain text */ - $chars .= "\x{fff9}-\x{fffd}"; - /* C7 - Inappropriate for canonical representation */ - $chars .= "\x{2ff0}-\x{2ffb}"; - /* C8 - Change display properties or are deprecated */ - $chars .= "\x{340}\x{341}\x{200e}\x{200f}\x{202a}-\x{202e}\x{206a}-\x{206f}"; - /* C9 - Tagging characters */ - $chars .= "\x{e0001}\x{e0020}-\x{e007f}"; - - /* Nodeprep forbids some more characters */ - $nodeprepchars = $chars; - $nodeprepchars .= "\x{22}\x{26}\x{27}\x{2f}\x{3a}\x{3c}\x{3e}\x{40}"; - - $parts = explode("/", $jid, 2); - if (count($parts) > 1) { - $resource = $parts[1]; - if ($resource == '') { - // Warning: empty resource isn't legit. - // But if we're normalizing, we may as well take it... - } - } else { - $resource = null; - } - - $node = explode("@", $parts[0]); - if ((count($node) > 2) || (count($node) == 0)) { - throw new Exception("Invalid JID: too many @s"); - } else if (count($node) == 1) { - $domain = $node[0]; - $node = null; - } else { - $domain = $node[1]; - $node = $node[0]; - if ($node == '') { - throw new Exception("Invalid JID: @ but no node"); - } - } - - // Length limits per http://xmpp.org/rfcs/rfc3920.html#addressing - if ($node !== null) { - if (strlen($node) > 1023) { - throw new Exception("Invalid JID: node too long."); - } - if (preg_match("/[".$nodeprepchars."]/u", $node)) { - throw new Exception("Invalid JID node '$node'"); - } - } - - if (strlen($domain) > 1023) { - throw new Exception("Invalid JID: domain too long."); - } - if (!common_valid_domain($domain)) { - throw new Exception("Invalid JID domain name '$domain'"); - } - - if ($resource !== null) { - if (strlen($resource) > 1023) { - throw new Exception("Invalid JID: resource too long."); - } - if (preg_match("/[".$chars."]/u", $resource)) { - throw new Exception("Invalid JID resource '$resource'"); - } - } - - return array('node' => is_null($node) ? null : mb_strtolower($node), - 'domain' => is_null($domain) ? null : mb_strtolower($domain), - 'resource' => $resource); -} - -/** - * Checks whether a string is a syntactically valid Jabber ID (JID), - * either with or without a resource. - * - * Note that a bare domain can be a valid JID. - * - * @param string $jid string to check - * @param bool $check_domain whether we should validate that domain... - * - * @return boolean whether the string is a valid JID - */ -function jabber_valid_full_jid($jid, $check_domain=false) -{ - try { - $parts = jabber_split_jid($jid); - if ($check_domain) { - if (!jabber_check_domain($parts['domain'])) { - return false; - } - } - return $parts['resource'] !== ''; // missing or present; empty ain't kosher - } catch (Exception $e) { - return false; - } -} - -/** - * Checks whether a string is a syntactically valid base Jabber ID (JID). - * A base JID won't include a resource specifier on the end; since we - * take it off when reading input we can't really use them reliably - * to direct outgoing messages yet (sorry guys!) - * - * Note that a bare domain can be a valid JID. - * - * @param string $jid string to check - * @param bool $check_domain whether we should validate that domain... - * - * @return boolean whether the string is a valid JID - */ -function jabber_valid_base_jid($jid, $check_domain=false) -{ - try { - $parts = jabber_split_jid($jid); - if ($check_domain) { - if (!jabber_check_domain($parts['domain'])) { - return false; - } - } - return ($parts['resource'] === null); // missing; empty ain't kosher - } catch (Exception $e) { - return false; - } -} - -/** - * Normalizes a Jabber ID for comparison, dropping the resource component if any. - * - * @param string $jid JID to check - * @param bool $check_domain if true, reject if the domain isn't findable - * - * @return string an equivalent JID in normalized (lowercase) form - */ - -function jabber_normalize_jid($jid) -{ - try { - $parts = jabber_split_jid($jid); - if ($parts['node'] !== null) { - return $parts['node'] . '@' . $parts['domain']; - } else { - return $parts['domain']; - } - } catch (Exception $e) { - return null; - } -} - -/** - * Check if this domain's got some legit DNS record - */ -function jabber_check_domain($domain) -{ - if (checkdnsrr("_xmpp-server._tcp." . $domain, "SRV")) { - return true; - } - if (checkdnsrr($domain, "ANY")) { - return true; - } - return false; -} - -/** - * the JID of the Jabber daemon for this StatusNet instance - * - * @return string JID of the Jabber daemon - */ - -function jabber_daemon_address() -{ - return common_config('xmpp', 'user') . '@' . common_config('xmpp', 'server'); -} - -class Sharing_XMPP extends XMPPHP_XMPP -{ - function getSocket() - { - return $this->socket; - } -} - -/** - * Build an XMPP proxy connection that'll save outgoing messages - * to the 'xmppout' queue to be picked up by xmppdaemon later. - * - * If queueing is disabled, we'll grab a live connection. - * - * @return XMPPHP - */ -function jabber_proxy() -{ - if (common_config('queue', 'enabled')) { - $proxy = new Queued_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'), - common_config('xmpp', 'resource') . 'daemon', - common_config('xmpp', 'server'), - common_config('xmpp', 'debug') ? - true : false, - common_config('xmpp', 'debug') ? - XMPPHP_Log::LEVEL_VERBOSE : null); - return $proxy; - } else { - return jabber_connect(); - } -} - -/** - * 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) - * - * @return XMPPHP connection to the configured server - */ - -function jabber_connect($resource=null) -{ - 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, - common_config('xmpp', 'server'), - common_config('xmpp', 'debug') ? - true : false, - common_config('xmpp', 'debug') ? - XMPPHP_Log::LEVEL_VERBOSE : null - ); - - if (!$conn) { - return false; - } - $connections[$site] = $conn; - - $conn->autoSubscribe(); - $conn->useEncryption(common_config('xmpp', 'encryption')); - - try { - 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; - } - - $conn->processUntil('session_start'); - } - return $connections[$site]; -} - -/** - * Queue send for a single notice to a given Jabber address - * - * @param string $to JID to send the notice to - * @param Notice $notice notice to send - * - * @return boolean success value - */ - -function jabber_send_notice($to, $notice) -{ - $conn = jabber_proxy(); - $profile = Profile::staticGet($notice->profile_id); - if (!$profile) { - common_log(LOG_WARNING, 'Refusing to send notice with ' . - 'unknown profile ' . common_log_objstring($notice), - __FILE__); - return false; - } - $msg = jabber_format_notice($profile, $notice); - $entry = jabber_format_entry($profile, $notice); - $conn->message($to, $msg, 'chat', null, $entry); - $profile->free(); - return true; -} - -/** - * extra information for XMPP messages, as defined by Twitter - * - * @param Profile $profile Profile of the sending user - * @param Notice $notice Notice being sent - * - * @return string Extra information (Atom, HTML, addresses) in string format - */ - -function jabber_format_entry($profile, $notice) -{ - $entry = $notice->asAtomEntry(true, true); - - $xs = new XMLStringer(); - $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im')); - $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml')); - $xs->element('a', array('href' => $profile->profileurl), - $profile->nickname); - $xs->text(": "); - if (!empty($notice->rendered)) { - $xs->raw($notice->rendered); - } else { - $xs->raw(common_render_content($notice->content, $notice)); - } - $xs->text(" "); - $xs->element('a', array( - 'href'=>common_local_url('conversation', - array('id' => $notice->conversation)).'#notice-'.$notice->id - ),sprintf(_('[%s]'),$notice->id)); - $xs->elementEnd('body'); - $xs->elementEnd('html'); - - $html = $xs->getString(); - - return $html . ' ' . $entry; -} - -/** - * sends a single text message to a given JID - * - * @param string $to JID to send the message to - * @param string $body body of the message - * @param string $type type of the message - * @param string $subject subject of the message - * - * @return boolean success flag - */ - -function jabber_send_message($to, $body, $type='chat', $subject=null) -{ - $conn = jabber_proxy(); - $conn->message($to, $body, $type, $subject); - return true; -} - -/** - * sends a presence stanza on the Jabber network - * - * @param string $status current status, free-form string - * @param string $show structured status value - * @param string $to recipient of presence, null for general - * @param string $type type of status message, related to $show - * @param int $priority priority of the presence - * - * @return boolean success value - */ - -function jabber_send_presence($status, $show='available', $to=null, - $type = 'available', $priority=null) -{ - $conn = jabber_connect(); - if (!$conn) { - return false; - } - $conn->presence($status, $show, $to, $type, $priority); - return true; -} - -/** - * sends a confirmation request to a JID - * - * @param string $code confirmation code for confirmation URL - * @param string $nickname nickname of confirming user - * @param string $address JID to send confirmation to - * - * @return boolean success flag - */ - -function jabber_confirm_address($code, $nickname, $address) -{ - $body = 'User "' . $nickname . '" on ' . common_config('site', 'name') . ' ' . - 'has said that your Jabber ID belongs to them. ' . - 'If that\'s true, you can confirm by clicking on this URL: ' . - common_local_url('confirmaddress', array('code' => $code)) . - ' . (If you cannot click it, copy-and-paste it into the ' . - 'address bar of your browser). If that user isn\'t you, ' . - 'or if you didn\'t request this confirmation, just ignore this message.'; - - return jabber_send_message($address, $body); -} - -/** - * sends a "special" presence stanza on the Jabber network - * - * @param string $type Type of presence - * @param string $to JID to send presence to - * @param string $show show value for presence - * @param string $status status value for presence - * - * @return boolean success flag - * - * @see jabber_send_presence() - */ - -function jabber_special_presence($type, $to=null, $show=null, $status=null) -{ - // FIXME: why use this instead of jabber_send_presence()? - $conn = jabber_connect(); - - $to = htmlspecialchars($to); - $status = htmlspecialchars($status); - - $out = "<presence"; - if ($to) { - $out .= " to='$to'"; - } - if ($type) { - $out .= " type='$type'"; - } - if ($show == 'available' and !$status) { - $out .= "/>"; - } else { - $out .= ">"; - if ($show && ($show != 'available')) { - $out .= "<show>$show</show>"; - } - if ($status) { - $out .= "<status>$status</status>"; - } - $out .= "</presence>"; - } - $conn->send($out); -} - -/** - * Queue broadcast of a notice to all subscribers and reply recipients - * - * This function will send a notice to all subscribers on the local server - * who have Jabber addresses, and have Jabber notification enabled, and - * have this subscription enabled for Jabber. It also sends the notice to - * all recipients of @-replies who have Jabber addresses and Jabber notification - * enabled. This is really the heart of Jabber distribution in StatusNet. - * - * @param Notice $notice The notice to broadcast - * - * @return boolean success flag - */ - -function jabber_broadcast_notice($notice) -{ - if (!common_config('xmpp', 'enabled')) { - return true; - } - $profile = Profile::staticGet($notice->profile_id); - - if (!$profile) { - common_log(LOG_WARNING, 'Refusing to broadcast notice with ' . - 'unknown profile ' . common_log_objstring($notice), - __FILE__); - return true; // not recoverable; discard. - } - - $msg = jabber_format_notice($profile, $notice); - $entry = jabber_format_entry($profile, $notice); - - $profile->free(); - unset($profile); - - $sent_to = array(); - - $conn = jabber_proxy(); - - $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 notice ' . $notice->id . ' to ' . $user->jabber, - __FILE__); - $conn->message($user->jabber, $msg, 'chat', null, $entry); - } - - return true; -} - -/** - * Queue send of a notice to all public listeners - * - * For notices that are generated on the local system (by users), we can optionally - * forward them to remote listeners by XMPP. - * - * @param Notice $notice notice to broadcast - * - * @return boolean success flag - */ - -function jabber_public_notice($notice) -{ - // Now, users who want everything - - $public = common_config('xmpp', 'public'); - - // FIXME PRIV don't send out private messages here - // XXX: should we send out non-local messages if public,localonly - // = false? I think not - - if ($public && $notice->is_local == Notice::LOCAL_PUBLIC) { - $profile = Profile::staticGet($notice->profile_id); - - if (!$profile) { - common_log(LOG_WARNING, 'Refusing to broadcast notice with ' . - 'unknown profile ' . common_log_objstring($notice), - __FILE__); - return true; // not recoverable; discard. - } - - $msg = jabber_format_notice($profile, $notice); - $entry = jabber_format_entry($profile, $notice); - - $conn = jabber_proxy(); - - foreach ($public as $address) { - common_log(LOG_INFO, - 'Sending notice ' . $notice->id . - ' to public listener ' . $address, - __FILE__); - $conn->message($address, $msg, 'chat', null, $entry); - } - $profile->free(); - } - - return true; -} - -/** - * makes a plain-text formatted version of a notice, suitable for Jabber distribution - * - * @param Profile &$profile profile of the sending user - * @param Notice &$notice notice being sent - * - * @return string plain-text version of the notice, with user nickname prefixed - */ - -function jabber_format_notice(&$profile, &$notice) -{ - return $profile->nickname . ': ' . $notice->content . ' [' . $notice->id . ']'; -} diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php index f9552c1dc..c3d3501c7 100644 --- a/lib/mysqlschema.php +++ b/lib/mysqlschema.php @@ -72,72 +72,127 @@ class MysqlSchema extends Schema * * Throws an exception if the table is not found. * - * @param string $name Name of the table to get + * @param string $table Name of the table to get * * @return TableDef tabledef for that table. * @throws SchemaTableMissingException */ - public function getTableDef($name) + public function getTableDef($table) { - $query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS " . - "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'"; - $schema = $this->conn->dsn['database']; - $sql = sprintf($query, $schema, $name); - $res = $this->conn->query($sql); + $def = array(); + $hasKeys = false; - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - if ($res->numRows() == 0) { - $res->free(); - throw new SchemaTableMissingException("No such table: $name"); + // Pull column data from INFORMATION_SCHEMA + $columns = $this->fetchMetaInfo($table, 'COLUMNS', 'ORDINAL_POSITION'); + if (count($columns) == 0) { + throw new SchemaTableMissingException("No such table: $table"); } - $td = new TableDef(); + foreach ($columns as $row) { - $td->name = $name; - $td->columns = array(); + $name = $row['COLUMN_NAME']; + $field = array(); - $row = array(); + // warning -- 'unsigned' attr on numbers isn't given in DATA_TYPE and friends. + // It is stuck in on COLUMN_TYPE though (eg 'bigint(20) unsigned') + $field['type'] = $type = $row['DATA_TYPE']; - while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) { + if ($type == 'char' || $type == 'varchar') { + if ($row['CHARACTER_MAXIMUM_LENGTH'] !== null) { + $field['length'] = intval($row['CHARACTER_MAXIMUM_LENGTH']); + } + } + if ($type == 'decimal') { + // Other int types may report these values, but they're irrelevant. + // Just ignore them! + if ($row['NUMERIC_PRECISION'] !== null) { + $field['precision'] = intval($row['NUMERIC_PRECISION']); + } + if ($row['NUMERIC_SCALE'] !== null) { + $field['scale'] = intval($row['NUMERIC_SCALE']); + } + } + if ($row['IS_NULLABLE'] == 'NO') { + $field['not null'] = true; + } + if ($row['COLUMN_DEFAULT'] !== null) { + // Hack for timestamp cols + if ($type == 'timestamp' && $row['COLUMN_DEFAULT'] == 'CURRENT_TIMESTAMP') { + // skip + } else { + $field['default'] = $row['COLUMN_DEFAULT']; + if ($this->isNumericType($type)) { + $field['default'] = intval($field['default']); + } + } + } + if ($row['COLUMN_KEY'] !== null) { + // We'll need to look up key info... + $hasKeys = true; + } + if ($row['COLUMN_COMMENT'] !== null && $row['COLUMN_COMMENT'] != '') { + $field['description'] = $row['COLUMN_COMMENT']; + } - $cd = new ColumnDef(); + $extra = $row['EXTRA']; + if ($extra) { + if (preg_match('/(^|\s)auto_increment(\s|$)/i', $extra)) { + $field['auto_increment'] = true; + } + // $row['EXTRA'] may contain 'on update CURRENT_TIMESTAMP' + // ^ ...... how to specify? + } - $cd->name = $row['COLUMN_NAME']; + if ($row['CHARACTER_SET_NAME'] !== null) { + // @fixme check against defaults? + //$def['charset'] = $row['CHARACTER_SET_NAME']; + //$def['collate'] = $row['COLLATION_NAME']; + } - $packed = $row['COLUMN_TYPE']; + $def['fields'][$name] = $field; + } - if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) { - $cd->type = $match[1]; - $cd->size = $match[2]; - } else { - $cd->type = $packed; - } + if ($hasKeys) { + // INFORMATION_SCHEMA's CONSTRAINTS and KEY_COLUMN_USAGE tables give + // good info on primary and unique keys but don't list ANY info on + // multi-value keys, which is lame-o. Sigh. + // + // Let's go old school and use SHOW INDEX :D + // + $keyInfo = $this->fetchIndexInfo($table); + $keys = array(); + foreach ($keyInfo as $row) { + $name = $row['Key_name']; + $column = $row['Column_name']; - $cd->nullable = ($row['IS_NULLABLE'] == 'YES') ? true : false; - $cd->key = $row['COLUMN_KEY']; - $cd->default = $row['COLUMN_DEFAULT']; - $cd->extra = $row['EXTRA']; - - // Autoincrement is stuck into the extra column. - // Pull it out so we don't accidentally mod it every time... - $extra = preg_replace('/(^|\s)auto_increment(\s|$)/i', '$1$2', $cd->extra); - if ($extra != $cd->extra) { - $cd->extra = trim($extra); - $cd->auto_increment = true; + if (!isset($keys[$name])) { + $keys[$name] = array(); + } + $keys[$name][] = $column; + + if ($name == 'PRIMARY') { + $type = 'primary key'; + } else if ($row['Non_unique'] == 0) { + $type = 'unique keys'; + } else if ($row['Index_type'] == 'FULLTEXT') { + $type = 'fulltext indexes'; + } else { + $type = 'indexes'; + } + $keyTypes[$name] = $type; } - // mysql extensions -- not (yet) used by base class - $cd->charset = $row['CHARACTER_SET_NAME']; - $cd->collate = $row['COLLATION_NAME']; - - $td->columns[] = $cd; + foreach ($keyTypes as $name => $type) { + if ($type == 'primary key') { + // there can be only one + $def[$type] = $keys[$name]; + } else { + $def[$type][$name] = $keys[$name]; + } + } } - $res->free(); - - return $td; + return $def; } /** @@ -150,483 +205,139 @@ class MysqlSchema extends Schema function getTableProperties($table, $props) { - $query = "SELECT %s FROM INFORMATION_SCHEMA.TABLES " . - "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'"; - $schema = $this->conn->dsn['database']; - $sql = sprintf($query, implode(',', $props), $schema, $table); - $res = $this->conn->query($sql); - - $row = array(); - $ok = $res->fetchInto($row, DB_FETCHMODE_ASSOC); - $res->free(); - - if ($ok) { - return $row; + $data = $this->fetchMetaInfo($table, 'TABLES'); + if ($data) { + return $data[0]; } else { throw new SchemaTableMissingException("No such table: $table"); } } /** - * Gets a ColumnDef object for a single column. + * Pull some INFORMATION.SCHEMA data for the given table. * - * Throws an exception if the table is not found. - * - * @param string $table name of the table - * @param string $column name of the column - * - * @return ColumnDef definition of the column or null - * if not found. + * @param string $table + * @return array of arrays */ - - public function getColumnDef($table, $column) + function fetchMetaInfo($table, $infoTable, $orderBy=null) { - $td = $this->getTableDef($table); - - foreach ($td->columns as $cd) { - if ($cd->name == $column) { - return $cd; - } + $query = "SELECT * FROM INFORMATION_SCHEMA.%s " . + "WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'"; + $schema = $this->conn->dsn['database']; + $sql = sprintf($query, $infoTable, $schema, $table); + if ($orderBy) { + $sql .= ' ORDER BY ' . $orderBy; } - - return null; + return $this->fetchQueryData($sql); } /** - * Creates a table with the given names and columns. + * Pull 'SHOW INDEX' data for the given table. * - * @param string $name Name of the table - * @param array $columns Array of ColumnDef objects - * for new table. - * - * @return boolean success flag - */ - - public function createTable($name, $columns) - { - $uniques = array(); - $primary = array(); - $indices = array(); - - $sql = "CREATE TABLE $name (\n"; - - for ($i = 0; $i < count($columns); $i++) { - - $cd =& $columns[$i]; - - if ($i > 0) { - $sql .= ",\n"; - } - - $sql .= $this->_columnSql($cd); - } - - $idx = $this->_indexList($columns); - - if ($idx['primary']) { - $sql .= ",\nconstraint primary key (" . implode(',', $idx['primary']) . ")"; - } - - foreach ($idx['uniques'] as $u) { - $key = $this->_uniqueKey($name, $u); - $sql .= ",\nunique index $key ($u)"; - } - - foreach ($idx['indices'] as $i) { - $key = $this->_key($name, $i); - $sql .= ",\nindex $key ($i)"; - } - - $sql .= ") ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; "; - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; - } - - /** - * Look over a list of column definitions and list up which - * indices will be present + * @param string $table + * @return array of arrays */ - private function _indexList(array $columns) + function fetchIndexInfo($table) { - $list = array('uniques' => array(), - 'primary' => array(), - 'indices' => array()); - foreach ($columns as $cd) { - switch ($cd->key) { - case 'UNI': - $list['uniques'][] = $cd->name; - break; - case 'PRI': - $list['primary'][] = $cd->name; - break; - case 'MUL': - $list['indices'][] = $cd->name; - break; - } - } - return $list; + $query = "SHOW INDEX FROM `%s`"; + $sql = sprintf($query, $table); + return $this->fetchQueryData($sql); } /** - * Get the unique index key name for a given column on this table - */ - function _uniqueKey($tableName, $columnName) - { - return $this->_key($tableName, $columnName); - } - - /** - * Get the index key name for a given column on this table - */ - function _key($tableName, $columnName) - { - return "{$tableName}_{$columnName}_idx"; - } - - /** - * Drops a table from the schema + * Append an SQL statement with an index definition for a full-text search + * index over one or more columns on a table. * - * Throws an exception if the table is not found. - * - * @param string $name Name of the table to drop - * - * @return boolean success flag + * @param array $statements + * @param string $table + * @param string $name + * @param array $def */ - - public function dropTable($name) + function appendCreateFulltextIndex(array &$statements, $table, $name, array $def) { - $res = $this->conn->query("DROP TABLE $name"); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + $statements[] = "CREATE FULLTEXT INDEX $name ON $table " . $this->buildIndexList($def); } /** - * Adds an index to a table. - * - * If no name is provided, a name will be made up based - * on the table name and column names. + * Close out a 'create table' SQL statement. * - * Throws an exception on database error, esp. if the table - * does not exist. + * @param string $name + * @param array $def + * @return string; * - * @param string $table Name of the table - * @param array $columnNames Name of columns to index - * @param string $name (Optional) name of the index - * - * @return boolean success flag + * @fixme ENGINE may need to be set differently in some cases, + * such as to support fulltext index. */ - - public function createIndex($table, $columnNames, $name=null) + function endCreateTable($name, array $def) { - if (!is_array($columnNames)) { - $columnNames = array($columnNames); - } - - if (empty($name)) { - $name = "{$table}_".implode("_", $columnNames)."_idx"; - } - - $res = $this->conn->query("ALTER TABLE $table ". - "ADD INDEX $name (". - implode(",", $columnNames).")"); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + $engine = $this->preferredEngine($def); + return ") ENGINE=$engine CHARACTER SET utf8 COLLATE utf8_bin"; } - - /** - * Drops a named index from a table. - * - * @param string $table name of the table the index is on. - * @param string $name name of the index - * - * @return boolean success flag - */ - - public function dropIndex($table, $name) + + function preferredEngine($def) { - $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name"); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + if (!empty($def['fulltext indexes'])) { + return 'MyISAM'; } - - return true; + return 'InnoDB'; } /** - * Adds a column to a table - * - * @param string $table name of the table - * @param ColumnDef $columndef Definition of the new - * column. - * - * @return boolean success flag + * Get the unique index key name for a given column on this table */ - - public function addColumn($table, $columndef) + function _uniqueKey($tableName, $columnName) { - $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef); - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + return $this->_key($tableName, $columnName); } /** - * Modifies a column in the schema. - * - * The name must match an existing column and table. - * - * @param string $table name of the table - * @param ColumnDef $columndef new definition of the column. - * - * @return boolean success flag + * Get the index key name for a given column on this table */ - - public function modifyColumn($table, $columndef) + function _key($tableName, $columnName) { - $sql = "ALTER TABLE $table MODIFY COLUMN " . - $this->_columnSql($columndef); - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + return "{$tableName}_{$columnName}_idx"; } + /** - * Drops a column from a table + * MySQL doesn't take 'DROP CONSTRAINT', need to treat unique keys as + * if they were indexes here. * - * The name must match an existing column. - * - * @param string $table name of the table - * @param string $columnName name of the column to drop - * - * @return boolean success flag + * @param array $phrase + * @param <type> $keyName MySQL */ - - public function dropColumn($table, $columnName) + function appendAlterDropUnique(array &$phrase, $keyName) { - $sql = "ALTER TABLE $table DROP COLUMN $columnName"; - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + $phrase[] = 'DROP INDEX ' . $keyName; } /** - * Ensures that a table exists with the given - * name and the given column definitions. - * - * If the table does not yet exist, it will - * create the table. If it does exist, it will - * alter the table to match the column definitions. - * - * @param string $tableName name of the table - * @param array $columns array of ColumnDef - * objects for the table - * - * @return boolean success flag + * Throw some table metadata onto the ALTER TABLE if we have a mismatch + * in expected type, collation. */ - - public function ensureTable($tableName, $columns) + function appendAlterExtras(array &$phrase, $tableName, array $def) { - // XXX: DB engine portability -> toilet - - try { - $td = $this->getTableDef($tableName); - } catch (SchemaTableMissingException $e) { - return $this->createTable($tableName, $columns); - } - - $cur = $this->_names($td->columns); - $new = $this->_names($columns); - - $dropIndex = array(); - $toadd = array_diff($new, $cur); - $todrop = array_diff($cur, $new); - $same = array_intersect($new, $cur); - $tomod = array(); - $addIndex = array(); - $tableProps = array(); - - foreach ($same as $m) { - $curCol = $this->_byName($td->columns, $m); - $newCol = $this->_byName($columns, $m); - - if (!$newCol->equals($curCol)) { - $tomod[] = $newCol->name; - continue; - } - - // Earlier versions may have accidentally left tables at default - // charsets which might be latin1 or other freakish things. - if ($this->_isString($curCol)) { - if ($curCol->charset != 'utf8') { - $tomod[] = $newCol->name; - continue; - } - } - } - - // Find any indices we have to change... - $curIdx = $this->_indexList($td->columns); - $newIdx = $this->_indexList($columns); - - if ($curIdx['primary'] != $newIdx['primary']) { - if ($curIdx['primary']) { - $dropIndex[] = 'drop primary key'; - } - if ($newIdx['primary']) { - $keys = implode(',', $newIdx['primary']); - $addIndex[] = "add constraint primary key ($keys)"; - } - } - - $dropUnique = array_diff($curIdx['uniques'], $newIdx['uniques']); - $addUnique = array_diff($newIdx['uniques'], $curIdx['uniques']); - foreach ($dropUnique as $columnName) { - $dropIndex[] = 'drop key ' . $this->_uniqueKey($tableName, $columnName); - } - foreach ($addUnique as $columnName) { - $addIndex[] = 'add constraint unique key ' . $this->_uniqueKey($tableName, $columnName) . " ($columnName)";; - } - - $dropMultiple = array_diff($curIdx['indices'], $newIdx['indices']); - $addMultiple = array_diff($newIdx['indices'], $curIdx['indices']); - foreach ($dropMultiple as $columnName) { - $dropIndex[] = 'drop key ' . $this->_key($tableName, $columnName); - } - foreach ($addMultiple as $columnName) { - $addIndex[] = 'add key ' . $this->_key($tableName, $columnName) . " ($columnName)"; - } - // Check for table properties: make sure we're using a sane // engine type and charset/collation. // @fixme make the default engine configurable? $oldProps = $this->getTableProperties($tableName, array('ENGINE', 'TABLE_COLLATION')); - if (strtolower($oldProps['ENGINE']) != 'innodb') { - $tableProps['ENGINE'] = 'InnoDB'; + $engine = $this->preferredEngine($def); + if (strtolower($oldProps['ENGINE']) != strtolower($engine)) { + $phrase[] = "ENGINE=$engine"; } if (strtolower($oldProps['TABLE_COLLATION']) != 'utf8_bin') { - $tableProps['DEFAULT CHARSET'] = 'utf8'; - $tableProps['COLLATE'] = 'utf8_bin'; - } - - if (count($dropIndex) + count($toadd) + count($todrop) + count($tomod) + count($addIndex) + count($tableProps) == 0) { - // nothing to do - return true; + $phrase[] = 'DEFAULT CHARSET=utf8'; + $phrase[] = 'COLLATE=utf8_bin'; } - - // For efficiency, we want this all in one - // query, instead of using our methods. - - $phrase = array(); - - foreach ($dropIndex as $indexSql) { - $phrase[] = $indexSql; - } - - foreach ($toadd as $columnName) { - $cd = $this->_byName($columns, $columnName); - - $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd); - } - - foreach ($todrop as $columnName) { - $phrase[] = 'DROP COLUMN ' . $columnName; - } - - foreach ($tomod as $columnName) { - $cd = $this->_byName($columns, $columnName); - - $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd); - } - - foreach ($addIndex as $indexSql) { - $phrase[] = $indexSql; - } - - foreach ($tableProps as $key => $val) { - $phrase[] = "$key=$val"; - } - - $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase); - - common_log(LOG_DEBUG, __METHOD__ . ': ' . $sql); - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; - } - - /** - * Returns the array of names from an array of - * ColumnDef objects. - * - * @param array $cds array of ColumnDef objects - * - * @return array strings for name values - */ - - private function _names($cds) - { - $names = array(); - - foreach ($cds as $cd) { - $names[] = $cd->name; - } - - return $names; } /** - * Get a ColumnDef from an array matching - * name. - * - * @param array $cds Array of ColumnDef objects - * @param string $name Name of the column - * - * @return ColumnDef matching item or null if no match. + * Is this column a string type? */ - - private function _byName($cds, $name) + private function _isString(array $cd) { - foreach ($cds as $cd) { - if ($cd->name == $name) { - return $cd; - } - } - - return null; + $strings = array('char', 'varchar', 'text'); + return in_array(strtolower($cd['type']), $strings); } /** @@ -641,43 +352,93 @@ class MysqlSchema extends Schema * @return string correct SQL for that column */ - private function _columnSql($cd) + function columnSql(array $cd) { - $sql = "{$cd->name} "; + $line = array(); + $line[] = parent::columnSql($cd); - if (!empty($cd->size)) { - $sql .= "{$cd->type}({$cd->size}) "; - } else { - $sql .= "{$cd->type} "; + // This'll have been added from our transform of 'serial' type + if (!empty($cd['auto_increment'])) { + $line[] = 'auto_increment'; } - if ($this->_isString($cd)) { - $sql .= " CHARACTER SET utf8 "; + if (!empty($cd['description'])) { + $line[] = 'comment'; + $line[] = $this->quoteValue($cd['description']); } - if (!empty($cd->default)) { - $sql .= "default {$cd->default} "; - } else { - $sql .= ($cd->nullable) ? "null " : "not null "; - } + return implode(' ', $line); + } + + function mapType($column) + { + $map = array('serial' => 'int', + 'integer' => 'int', + 'numeric' => 'decimal'); - if (!empty($cd->auto_increment)) { - $sql .= " auto_increment "; + $type = $column['type']; + if (isset($map[$type])) { + $type = $map[$type]; + } + + if (!empty($column['size'])) { + $size = $column['size']; + if ($type == 'int' && + in_array($size, array('tiny', 'small', 'medium', 'big'))) { + $type = $size . $type; + } else if (in_array($type, array('blob', 'text')) && + in_array($size, array('tiny', 'medium', 'long'))) { + $type = $size . $type; + } } - if (!empty($cd->extra)) { - $sql .= "{$cd->extra} "; - } + return $type; + } - return $sql; + function typeAndSize($column) + { + if ($column['type'] == 'enum') { + $vals = array_map(array($this, 'quote'), $column['enum']); + return 'enum(' . implode(',', $vals) . ')'; + } else if ($this->_isString($column)) { + $col = parent::typeAndSize($column); + if (!empty($column['charset'])) { + $col .= ' CHARSET ' . $column['charset']; + } + if (!empty($column['collate'])) { + $col .= ' COLLATE ' . $column['collate']; + } + return $col; + } else { + return parent::typeAndSize($column); + } } /** - * Is this column a string type? + * Filter the given table definition array to match features available + * in this database. + * + * This lets us strip out unsupported things like comments, foreign keys, + * or type variants that we wouldn't get back from getTableDef(). + * + * @param array $tableDef */ - private function _isString(ColumnDef $cd) + function filterDef(array $tableDef) { - $strings = array('char', 'varchar', 'text'); - return in_array(strtolower($cd->type), $strings); + foreach ($tableDef['fields'] as $name => &$col) { + if ($col['type'] == 'serial') { + $col['type'] = 'int'; + $col['auto_increment'] = true; + } + if ($col['type'] == 'datetime' && isset($col['default']) && $col['default'] == 'CURRENT_TIMESTAMP') { + $col['type'] = 'timestamp'; + } + $col['type'] = $this->mapType($col); + unset($col['size']); + } + if (!common_config('db', 'mysql_foreign_keys')) { + unset($tableDef['foreign keys']); + } + return $tableDef; } } diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php index 272f7eff6..d50e35f66 100644 --- a/lib/pgsqlschema.php +++ b/lib/pgsqlschema.php @@ -42,6 +42,7 @@ if (!defined('STATUSNET')) { * @package StatusNet * @author Evan Prodromou <evan@status.net> * @author Brenda Wallace <shiny@cpan.org> + * @author Brion Vibber <brion@status.net> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -50,167 +51,209 @@ class PgsqlSchema extends Schema { /** - * Returns a TableDef object for the table + * Returns a table definition array for the table * in the schema with the given name. * * Throws an exception if the table is not found. * - * @param string $name Name of the table to get + * @param string $table Name of the table to get * - * @return TableDef tabledef for that table. + * @return array tabledef for that table. */ - public function getTableDef($name) + public function getTableDef($table) { - $res = $this->conn->query("SELECT *, column_default as default, is_nullable as Null, - udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'"); + $def = array(); + $hasKeys = false; - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + // Pull column data from INFORMATION_SCHEMA + $columns = $this->fetchMetaInfo($table, 'columns', 'ordinal_position'); + if (count($columns) == 0) { + throw new SchemaTableMissingException("No such table: $table"); } - $td = new TableDef(); + // We'll need to match up fields by ordinal reference + $orderedFields = array(); - $td->name = $name; - $td->columns = array(); + foreach ($columns as $row) { - if ($res->numRows() == 0 ) { - throw new Exception('no such table'); //pretend to be the msyql error. yeah, this sucks. - } - $row = array(); + $name = $row['column_name']; + $orderedFields[$row['ordinal_position']] = $name; - while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) { - $cd = new ColumnDef(); + $field = array(); + $field['type'] = $row['udt_name']; - $cd->name = $row['field']; + if ($type == 'char' || $type == 'varchar') { + if ($row['character_maximum_length'] !== null) { + $field['length'] = intval($row['character_maximum_length']); + } + } + if ($type == 'numeric') { + // Other int types may report these values, but they're irrelevant. + // Just ignore them! + if ($row['numeric_precision'] !== null) { + $field['precision'] = intval($row['numeric_precision']); + } + if ($row['numeric_scale'] !== null) { + $field['scale'] = intval($row['numeric_scale']); + } + } + if ($row['is_nullable'] == 'NO') { + $field['not null'] = true; + } + if ($row['column_default'] !== null) { + $field['default'] = $row['column_default']; + if ($this->isNumericType($type)) { + $field['default'] = intval($field['default']); + } + } - $packed = $row['type']; + $def['fields'][$name] = $field; + } - if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) { - $cd->type = $match[1]; - $cd->size = $match[2]; - } else { - $cd->type = $packed; + // Pulling index info from pg_class & pg_index + // This can give us primary & unique key info, but not foreign key constraints + // so we exclude them and pick them up later. + $indexInfo = $this->getIndexInfo($table); + foreach ($indexInfo as $row) { + $keyName = $row['key_name']; + + // Dig the column references out! + // + // These are inconvenient arrays with partial references to the + // pg_att table, but since we've already fetched up the column + // info on the current table, we can look those up locally. + $cols = array(); + $colPositions = explode(' ', $row['indkey']); + foreach ($colPositions as $ord) { + if ($ord == 0) { + $cols[] = 'FUNCTION'; // @fixme + } else { + $cols[] = $orderedFields[$ord]; + } } - $cd->nullable = ($row['null'] == 'YES') ? true : false; - $cd->key = $row['Key']; - $cd->default = $row['default']; - $cd->extra = $row['Extra']; - - $td->columns[] = $cd; + $def['indexes'][$keyName] = $cols; } - return $td; - } - /** - * Gets a ColumnDef object for a single column. - * - * Throws an exception if the table is not found. - * - * @param string $table name of the table - * @param string $column name of the column - * - * @return ColumnDef definition of the column or null - * if not found. - */ - - public function getColumnDef($table, $column) - { - $td = $this->getTableDef($table); + // Pull constraint data from INFORMATION_SCHEMA: + // Primary key, unique keys, foreign keys + $keyColumns = $this->fetchMetaInfo($table, 'key_column_usage', 'constraint_name,ordinal_position'); + $keys = array(); - foreach ($td->columns as $cd) { - if ($cd->name == $column) { - return $cd; + foreach ($keyColumns as $row) { + $keyName = $row['constraint_name']; + $keyCol = $row['column_name']; + if (!isset($keys[$keyName])) { + $keys[$keyName] = array(); } + $keys[$keyName][] = $keyCol; } - return null; + foreach ($keys as $keyName => $cols) { + // name hack -- is this reliable? + if ($keyName == "{$table}_pkey") { + $def['primary key'] = $cols; + } else if (preg_match("/^{$table}_(.*)_fkey$/", $keyName, $matches)) { + $fkey = $this->getForeignKeyInfo($table, $keyName); + $colMap = array_combine($cols, $fkey['col_names']); + $def['foreign keys'][$keyName] = array($fkey['table_name'], $colMap); + } else { + $def['unique keys'][$keyName] = $cols; + } + } + return $def; } /** - * Creates a table with the given names and columns. - * - * @param string $name Name of the table - * @param array $columns Array of ColumnDef objects - * for new table. + * Pull some INFORMATION.SCHEMA data for the given table. * - * @return boolean success flag + * @param string $table + * @return array of arrays */ - - public function createTable($name, $columns) + function fetchMetaInfo($table, $infoTable, $orderBy=null) { - $uniques = array(); - $primary = array(); - $indices = array(); - $onupdate = array(); - - $sql = "CREATE TABLE $name (\n"; - - for ($i = 0; $i < count($columns); $i++) { - - $cd =& $columns[$i]; - - if ($i > 0) { - $sql .= ",\n"; - } - - $sql .= $this->_columnSql($cd); - switch ($cd->key) { - case 'UNI': - $uniques[] = $cd->name; - break; - case 'PRI': - $primary[] = $cd->name; - break; - case 'MUL': - $indices[] = $cd->name; - break; - } - } - - if (count($primary) > 0) { // it really should be... - $sql .= ",\n PRIMARY KEY (" . implode(',', $primary) . ")"; - } - - $sql .= "); "; - - - foreach ($uniques as $u) { - $sql .= "\n CREATE index {$name}_{$u}_idx ON {$name} ($u); "; + $query = "SELECT * FROM information_schema.%s " . + "WHERE table_name='%s'"; + $sql = sprintf($query, $infoTable, $table); + if ($orderBy) { + $sql .= ' ORDER BY ' . $orderBy; } + return $this->fetchQueryData($sql); + } - foreach ($indices as $i) { - $sql .= "CREATE index {$name}_{$i}_idx ON {$name} ($i)"; - } - $res = $this->conn->query($sql); + /** + * Pull some PG-specific index info + * @param string $table + * @return array of arrays + */ + function getIndexInfo($table) + { + $query = 'SELECT ' . + '(SELECT relname FROM pg_class WHERE oid=indexrelid) AS key_name, ' . + '* FROM pg_index ' . + 'WHERE indrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' . + 'AND indisprimary=\'f\' AND indisunique=\'f\' ' . + 'ORDER BY indrelid, indexrelid'; + $sql = sprintf($query, $table); + return $this->fetchQueryData($sql); + } - if (PEAR::isError($res)) { - throw new Exception($res->getMessage(). ' SQL was '. $sql); + /** + * Column names from the foreign table can be resolved with a call to getTableColumnNames() + * @param <type> $table + * @return array array of rows with keys: fkey_name, table_name, table_id, col_names (array of strings) + */ + function getForeignKeyInfo($table, $constraint_name) + { + // In a sane world, it'd be easier to query the column names directly. + // But it's pretty hard to work with arrays such as col_indexes in direct SQL here. + $query = 'SELECT ' . + '(SELECT relname FROM pg_class WHERE oid=confrelid) AS table_name, ' . + 'confrelid AS table_id, ' . + '(SELECT indkey FROM pg_index WHERE indexrelid=conindid) AS col_indexes ' . + 'FROM pg_constraint ' . + 'WHERE conrelid=(SELECT oid FROM pg_class WHERE relname=\'%s\') ' . + 'AND conname=\'%s\' ' . + 'AND contype=\'f\''; + $sql = sprintf($query, $table, $constraint_name); + $data = $this->fetchQueryData($sql); + if (count($data) < 1) { + throw new Exception("Could not find foreign key " . $constraint_name . " on table " . $table); } - return true; + $row = $data[0]; + return array( + 'table_name' => $row['table_name'], + 'col_names' => $this->getTableColumnNames($row['table_id'], $row['col_indexes']) + ); } /** - * Drops a table from the schema - * - * Throws an exception if the table is not found. - * - * @param string $name Name of the table to drop * - * @return boolean success flag + * @param int $table_id + * @param array $col_indexes + * @return array of strings */ - - public function dropTable($name) + function getTableColumnNames($table_id, $col_indexes) { - $res = $this->conn->query("DROP TABLE $name"); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + $indexes = array_map('intval', explode(' ', $col_indexes)); + $query = 'SELECT attnum AS col_index, attname AS col_name ' . + 'FROM pg_attribute where attrelid=%d ' . + 'AND attnum IN (%s)'; + $sql = sprintf($query, $table_id, implode(',', $indexes)); + $data = $this->fetchQueryData($sql); + + $byId = array(); + foreach ($data as $row) { + $byId[$row['col_index']] = $row['col_name']; } - return true; + $out = array(); + foreach ($indexes as $id) { + $out[] = $byId[$id]; + } + return $out; } /** @@ -230,303 +273,183 @@ class PgsqlSchema extends Schema } /** - * Adds an index to a table. - * - * If no name is provided, a name will be made up based - * on the table name and column names. + * Return the proper SQL for creating or + * altering a column. * - * Throws an exception on database error, esp. if the table - * does not exist. + * Appropriate for use in CREATE TABLE or + * ALTER TABLE statements. * - * @param string $table Name of the table - * @param array $columnNames Name of columns to index - * @param string $name (Optional) name of the index + * @param array $cd column to create * - * @return boolean success flag + * @return string correct SQL for that column */ - public function createIndex($table, $columnNames, $name=null) + function columnSql(array $cd) { - if (!is_array($columnNames)) { - $columnNames = array($columnNames); - } - - if (empty($name)) { - $name = "$table_".implode("_", $columnNames)."_idx"; - } - - $res = $this->conn->query("ALTER TABLE $table ". - "ADD INDEX $name (". - implode(",", $columnNames).")"); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + $line = array(); + $line[] = parent::columnSql($cd); + + /* + if ($table['foreign keys'][$name]) { + foreach ($table['foreign keys'][$name] as $foreignTable => $foreignColumn) { + $line[] = 'references'; + $line[] = $this->quoteIdentifier($foreignTable); + $line[] = '(' . $this->quoteIdentifier($foreignColumn) . ')'; + } } + */ - return true; + return implode(' ', $line); } /** - * Drops a named index from a table. - * - * @param string $table name of the table the index is on. - * @param string $name name of the index + * Append phrase(s) to an array of partial ALTER TABLE chunks in order + * to alter the given column from its old state to a new one. * - * @return boolean success flag + * @param array $phrase + * @param string $columnName + * @param array $old previous column definition as found in DB + * @param array $cd current column definition */ - - public function dropIndex($table, $name) + function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd) { - $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name"); + $prefix = 'ALTER COLUMN ' . $this->quoteIdentifier($columnName) . ' '; - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + $oldType = $this->mapType($old); + $newType = $this->mapType($cd); + if ($oldType != $newType) { + $phrase[] = $prefix . 'TYPE ' . $newType; } - return true; - } - - /** - * Adds a column to a table - * - * @param string $table name of the table - * @param ColumnDef $columndef Definition of the new - * column. - * - * @return boolean success flag - */ - - public function addColumn($table, $columndef) - { - $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef); - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + if (!empty($old['not null']) && empty($cd['not null'])) { + $phrase[] = $prefix . 'DROP NOT NULL'; + } else if (empty($old['not null']) && !empty($cd['not null'])) { + $phrase[] = $prefix . 'SET NOT NULL'; } - return true; + if (isset($old['default']) && !isset($cd['default'])) { + $phrase[] = $prefix . 'DROP DEFAULT'; + } else if (!isset($old['default']) && isset($cd['default'])) { + $phrase[] = $prefix . 'SET DEFAULT ' . $this->quoteDefaultValue($cd); + } } /** - * Modifies a column in the schema. - * - * The name must match an existing column and table. + * Append an SQL statement to drop an index from a table. + * Note that in PostgreSQL, index names are DB-unique. * - * @param string $table name of the table - * @param ColumnDef $columndef new definition of the column. - * - * @return boolean success flag + * @param array $statements + * @param string $table + * @param string $name + * @param array $def */ - - public function modifyColumn($table, $columndef) + function appendDropIndex(array &$statements, $table, $name) { - $sql = "ALTER TABLE $table ALTER COLUMN TYPE " . - $this->_columnSql($columndef); - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + $statements[] = "DROP INDEX $name"; } /** - * Drops a column from a table - * - * The name must match an existing column. + * Quote a db/table/column identifier if necessary. * - * @param string $table name of the table - * @param string $columnName name of the column to drop - * - * @return boolean success flag + * @param string $name + * @return string */ - - public function dropColumn($table, $columnName) + function quoteIdentifier($name) { - $sql = "ALTER TABLE $table DROP COLUMN $columnName"; - - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); - } - - return true; + return $this->conn->quoteIdentifier($name); } - /** - * Ensures that a table exists with the given - * name and the given column definitions. - * - * If the table does not yet exist, it will - * create the table. If it does exist, it will - * alter the table to match the column definitions. - * - * @param string $tableName name of the table - * @param array $columns array of ColumnDef - * objects for the table - * - * @return boolean success flag - */ - - public function ensureTable($tableName, $columns) + function mapType($column) { - // XXX: DB engine portability -> toilet - - try { - $td = $this->getTableDef($tableName); - - } catch (Exception $e) { - if (preg_match('/no such table/', $e->getMessage())) { - return $this->createTable($tableName, $columns); - } else { - throw $e; - } + $map = array('serial' => 'bigserial', // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance. + 'numeric' => 'decimal', + 'datetime' => 'timestamp', + 'blob' => 'bytea'); + + $type = $column['type']; + if (isset($map[$type])) { + $type = $map[$type]; } - $cur = $this->_names($td->columns); - $new = $this->_names($columns); - - $toadd = array_diff($new, $cur); - $todrop = array_diff($cur, $new); - $same = array_intersect($new, $cur); - $tomod = array(); - foreach ($same as $m) { - $curCol = $this->_byName($td->columns, $m); - $newCol = $this->_byName($columns, $m); - - - if (!$newCol->equals($curCol)) { - // BIG GIANT TODO! - // stop it detecting different types and trying to modify on every page request -// $tomod[] = $newCol->name; + if ($type == 'int') { + if (!empty($column['size'])) { + $size = $column['size']; + if ($size == 'small') { + return 'int2'; + } else if ($size == 'big') { + return 'int8'; + } } - } - if (count($toadd) + count($todrop) + count($tomod) == 0) { - // nothing to do - return true; - } - - // For efficiency, we want this all in one - // query, instead of using our methods. - - $phrase = array(); - - foreach ($toadd as $columnName) { - $cd = $this->_byName($columns, $columnName); - - $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd); - } - - foreach ($todrop as $columnName) { - $phrase[] = 'DROP COLUMN ' . $columnName; - } - - foreach ($tomod as $columnName) { - $cd = $this->_byName($columns, $columnName); - - /* brute force */ - $phrase[] = 'DROP COLUMN ' . $columnName; - $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd); - } - - $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase); - $res = $this->conn->query($sql); - - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + return 'int4'; } - return true; + return $type; } - /** - * Returns the array of names from an array of - * ColumnDef objects. - * - * @param array $cds array of ColumnDef objects - * - * @return array strings for name values - */ - - private function _names($cds) + // @fixme need name... :P + function typeAndSize($column) { - $names = array(); - - foreach ($cds as $cd) { - $names[] = $cd->name; + if ($column['type'] == 'enum') { + $vals = array_map(array($this, 'quote'), $column['enum']); + return "text check ($name in " . implode(',', $vals) . ')'; + } else { + return parent::typeAndSize($column); } - - return $names; } /** - * Get a ColumnDef from an array matching - * name. + * Filter the given table definition array to match features available + * in this database. * - * @param array $cds Array of ColumnDef objects - * @param string $name Name of the column + * This lets us strip out unsupported things like comments, foreign keys, + * or type variants that we wouldn't get back from getTableDef(). * - * @return ColumnDef matching item or null if no match. + * @param array $tableDef */ - - private function _byName($cds, $name) + function filterDef(array $tableDef) { - foreach ($cds as $cd) { - if ($cd->name == $name) { - return $cd; + foreach ($tableDef['fields'] as $name => &$col) { + // No convenient support for field descriptions + unset($col['description']); + + /* + if (isset($col['size'])) { + // Don't distinguish between tinyint and int. + if ($col['size'] == 'tiny' && $col['type'] == 'int') { + unset($col['size']); + } } + */ + $col['type'] = $this->mapType($col); + unset($col['size']); } - - return null; + if (!empty($tableDef['primary key'])) { + $tableDef['primary key'] = $this->filterKeyDef($tableDef['primary key']); + } + if (!empty($tableDef['unique keys'])) { + foreach ($tableDef['unique keys'] as $i => $def) { + $tableDef['unique keys'][$i] = $this->filterKeyDef($def); + } + } + return $tableDef; } /** - * Return the proper SQL for creating or - * altering a column. - * - * Appropriate for use in CREATE TABLE or - * ALTER TABLE statements. + * Filter the given key/index definition to match features available + * in this database. * - * @param ColumnDef $cd column to create - * - * @return string correct SQL for that column + * @param array $def + * @return array */ - private function _columnSql($cd) + function filterKeyDef(array $def) { - $sql = "{$cd->name} "; - $type = $this->_columnTypeTranslation($cd->type); - - //handle those mysql enum fields that postgres doesn't support - if (preg_match('!^enum!', $type)) { - $allowed_values = preg_replace('!^enum!', '', $type); - $sql .= " text check ({$cd->name} in $allowed_values)"; - return $sql; - } - if (!empty($cd->auto_increment)) { - $type = "bigserial"; // FIXME: creates the wrong name for the sequence for some internal sequence-lookup function, so better fix this to do the real 'create sequence' dance. - } - - if (!empty($cd->size)) { - $sql .= "{$type}({$cd->size}) "; - } else { - $sql .= "{$type} "; - } - - if (!empty($cd->default)) { - $sql .= "default {$cd->default} "; - } else { - $sql .= ($cd->nullable) ? "null " : "not null "; + // PostgreSQL doesn't like prefix lengths specified on keys...? + foreach ($def as $i => $item) + { + if (is_array($item)) { + $def[$i] = $item[0]; + } } - -// if (!empty($cd->extra)) { -// $sql .= "{$cd->extra} "; -// } - - return $sql; + return $def; } } diff --git a/lib/plugindisableform.php b/lib/plugindisableform.php new file mode 100644 index 000000000..3cbabdb2c --- /dev/null +++ b/lib/plugindisableform.php @@ -0,0 +1,93 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for enabling/disabling plugins + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @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/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Form for joining a group + * + * @category Form + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see PluginEnableForm + */ + +class PluginDisableForm extends PluginEnableForm +{ + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'plugin-disable-' . $this->plugin; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_plugin_disable'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('plugindisable', + array('plugin' => $this->plugin)); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Plugin admin panel controls + $this->out->submit('submit', _m('plugin', 'Disable')); + } + +} diff --git a/lib/pluginenableform.php b/lib/pluginenableform.php new file mode 100644 index 000000000..8683ffd0b --- /dev/null +++ b/lib/pluginenableform.php @@ -0,0 +1,114 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for enabling/disabling plugins + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @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/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/form.php'; + +/** + * Form for joining a group + * + * @category Form + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see PluginDisableForm + */ + +class PluginEnableForm extends Form +{ + /** + * Plugin to enable/disable + */ + + var $plugin = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param string $plugin plugin to enable/disable + */ + + function __construct($out=null, $plugin=null) + { + parent::__construct($out); + + $this->plugin = $plugin; + } + + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'plugin-enable-' . $this->plugin; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_plugin_enable'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('pluginenable', + array('plugin' => $this->plugin)); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Plugin admin panel controls + $this->out->submit('submit', _m('plugin', 'Enable')); + } +} diff --git a/lib/pluginlist.php b/lib/pluginlist.php new file mode 100644 index 000000000..07a17ba39 --- /dev/null +++ b/lib/pluginlist.php @@ -0,0 +1,213 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugins administration panel + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Settings + * @package StatusNet + * @author 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/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require INSTALLDIR . "/lib/pluginenableform.php"; +require INSTALLDIR . "/lib/plugindisableform.php"; + +/** + * Plugin list + * + * @category Admin + * @package StatusNet + * @author Brion Vibber <brion@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class PluginList extends Widget +{ + var $plugins = array(); + + function __construct($plugins, $out) + { + parent::__construct($out); + $this->plugins = $plugins; + } + + function show() + { + $this->startList(); + $this->showPlugins(); + $this->endList(); + } + + function startList() + { + $this->out->elementStart('table', 'plugin_list'); + } + + function endList() + { + $this->out->elementEnd('table'); + } + + function showPlugins() + { + foreach ($this->plugins as $plugin) { + $pli = $this->newListItem($plugin); + $pli->show(); + } + } + + function newListItem($plugin) + { + return new PluginListItem($plugin, $this->out); + } +} + +class PluginListItem extends Widget +{ + /** Current plugin. */ + var $plugin = null; + + /** Local cache for plugin version info */ + protected static $versions = false; + + function __construct($plugin, $out) + { + parent::__construct($out); + $this->plugin = $plugin; + } + + function show() + { + $meta = $this->metaInfo(); + + $this->out->elementStart('tr', array('id' => 'plugin-' . $this->plugin)); + + // Name and controls + $this->out->elementStart('td'); + $this->out->elementStart('div'); + if (!empty($meta['homepage'])) { + $this->out->elementStart('a', array('href' => $meta['homepage'])); + } + $this->out->text($this->plugin); + if (!empty($meta['homepage'])) { + $this->out->elementEnd('a'); + } + $this->out->elementEnd('div'); + + $form = $this->getControlForm(); + $form->show(); + + $this->out->elementEnd('td'); + + // Version and authors + $this->out->elementStart('td'); + if (!empty($meta['version'])) { + $this->out->elementStart('div'); + $this->out->text($meta['version']); + $this->out->elementEnd('div'); + } + if (!empty($meta['author'])) { + $this->out->elementStart('div'); + $this->out->text($meta['author']); + $this->out->elementEnd('div'); + } + $this->out->elementEnd('td'); + + // Description + $this->out->elementStart('td'); + if (!empty($meta['rawdescription'])) { + $this->out->raw($meta['rawdescription']); + } + $this->out->elementEnd('td'); + + $this->out->elementEnd('tr'); + } + + /** + * Pull up the appropriate control form for this plugin, depending + * on its current state. + * + * @return Form + */ + protected function getControlForm() + { + $key = 'disable-' . $this->plugin; + if (common_config('plugins', $key)) { + return new PluginEnableForm($this->out, $this->plugin); + } else { + return new PluginDisableForm($this->out, $this->plugin); + } + } + + /** + * Grab metadata about this plugin... + * Warning: horribly inefficient and may explode! + * Doesn't work for disabled plugins either. + * + * @fixme pull structured data from plugin source + */ + function metaInfo() + { + $versions = self::getPluginVersions(); + $found = false; + + foreach ($versions as $info) { + // hack for URL shorteners... "LilUrl (ur1.ca)" etc + list($name, ) = explode(' ', $info['name']); + + if ($name == $this->plugin) { + if ($found) { + // hack for URL shorteners... + $found['rawdescription'] .= "<br />\n" . $info['rawdescription']; + } else { + $found = $info; + } + } + } + + if ($found) { + return $found; + } else { + return array('name' => $this->plugin, + 'rawdescription' => _m('plugin-description', + '(Plugin descriptions unavailable when disabled.)')); + } + } + + /** + * Lazy-load the set of active plugin version info + * @return array + */ + protected static function getPluginVersions() + { + if (!is_array(self::$versions)) { + $versions = array(); + Event::handle('PluginVersion', array(&$versions)); + self::$versions = $versions; + } + return self::$versions; + } +} diff --git a/lib/queuehandler.php b/lib/queuehandler.php index 2909cd83b..2194dd161 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -37,20 +37,6 @@ class QueueHandler { /** - * Return transport keyword which identifies items this queue handler - * services; must be defined for all subclasses. - * - * Must be 8 characters or less to fit in the queue_item database. - * ex "email", "jabber", "sms", "irc", ... - * - * @return string - */ - function transport() - { - return null; - } - - /** * Here's the meat of your queue handler -- you're handed a Notice * or other object, which you may do as you will with. * diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 0829c8a8b..6666a6cb5 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -156,21 +156,14 @@ abstract class QueueManager extends IoManager } /** - * Encode an object or variable for queued storage. - * Notice objects are currently stored as an id reference; - * other items are serialized. + * Encode an object for queued storage. * * @param mixed $item * @return string */ protected function encode($item) { - if ($item instanceof Notice) { - // Backwards compat - return $item->id; - } else { - return serialize($item); - } + return serialize($item); } /** @@ -182,25 +175,7 @@ abstract class QueueManager extends IoManager */ protected function decode($frame) { - if (is_numeric($frame)) { - // Back-compat for notices... - return Notice::staticGet(intval($frame)); - } elseif (substr($frame, 0, 1) == '<') { - // Back-compat for XML source - return $frame; - } else { - // Deserialize! - #$old = error_reporting(); - #error_reporting($old & ~E_NOTICE); - $out = unserialize($frame); - #error_reporting($old); - - if ($out === false && $frame !== 'b:0;') { - common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame"); - return false; - } - return $out; - } + return unserialize($frame); } /** @@ -270,16 +245,6 @@ abstract class QueueManager extends IoManager // Broadcasting profile updates to OMB remote subscribers $this->connect('profile', 'ProfileQueueHandler'); - // XMPP output handlers... - if (common_config('xmpp', 'enabled')) { - // Delivery prep, read by queuedaemon.php: - $this->connect('jabber', 'JabberQueueHandler'); - $this->connect('public', 'PublicQueueHandler'); - - // Raw output, read by xmppdaemon.php: - $this->connect('xmppout', 'XmppOutQueueHandler', 'xmpp'); - } - // For compat with old plugins not registering their own handlers. $this->connect('plugin', 'PluginQueueHandler'); } diff --git a/lib/queuemonitor.php b/lib/queuemonitor.php index 1c306a629..3dc0ea65a 100644 --- a/lib/queuemonitor.php +++ b/lib/queuemonitor.php @@ -36,7 +36,7 @@ class QueueMonitor * 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' + * @param array $owners list of owner keys like 'queue:xmpp' or 'site:stat01' */ public function stats($key, $owners=array()) { diff --git a/lib/router.php b/lib/router.php index 24cda72b6..f33aebe3c 100644 --- a/lib/router.php +++ b/lib/router.php @@ -243,7 +243,7 @@ class Router // settings foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections', - 'oauthapps', 'email', 'sms', 'userdesign', 'other') as $s) { + 'oauthapps', 'email', 'sms', 'userdesign', 'url') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } @@ -393,6 +393,11 @@ class Router // statuses API + $m->connect('api', + array('action' => 'Redirect', + 'nextAction' => 'doc', + 'args' => array('title' => 'api'))); + $m->connect('api/statuses/public_timeline.:format', array('action' => 'ApiTimelinePublic', 'format' => '(xml|json|rss|atom)')); @@ -736,6 +741,12 @@ class Router $m->connect('api/statusnet/groups/create.:format', array('action' => 'ApiGroupCreate', 'format' => '(xml|json)')); + + $m->connect('api/statusnet/groups/update/:id.:format', + array('action' => 'ApiGroupProfileUpdate', + 'id' => '[a-zA-Z0-9]+', + 'format' => '(xml|json)')); + // Tags $m->connect('api/statusnet/tags/timeline/:tag.:format', array('action' => 'ApiTimelineTag', @@ -773,6 +784,14 @@ class Router $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel')); $m->connect('admin/license', array('action' => 'licenseadminpanel')); + $m->connect('admin/plugins', array('action' => 'pluginsadminpanel')); + $m->connect('admin/plugins/enable/:plugin', + array('action' => 'pluginenable'), + array('plugin' => '[A-Za-z0-9_]+')); + $m->connect('admin/plugins/disable/:plugin', + array('action' => 'plugindisable'), + array('plugin' => '[A-Za-z0-9_]+')); + $m->connect('getfile/:filename', array('action' => 'getfile'), array('filename' => '[A-Za-z0-9._-]+')); diff --git a/lib/schema.php b/lib/schema.php index e5def514e..2e2795588 100644 --- a/lib/schema.php +++ b/lib/schema.php @@ -41,6 +41,7 @@ if (!defined('STATUSNET')) { * @category Database * @package StatusNet * @author Evan Prodromou <evan@status.net> + * @author Brion Vibber <brion@status.net> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -118,65 +119,216 @@ class Schema /** * Creates a table with the given names and columns. * - * @param string $name Name of the table - * @param array $columns Array of ColumnDef objects - * for new table. + * @param string $tableName Name of the table + * @param array $def Table definition array listing fields and indexes. * * @return boolean success flag */ - public function createTable($name, $columns) + public function createTable($tableName, $def) { - $uniques = array(); - $primary = array(); - $indices = array(); + $statements = $this->buildCreateTable($tableName, $def); + return $this->runSqlSet($statements); + } - $sql = "CREATE TABLE $name (\n"; + /** + * Build a set of SQL statements to create a table with the given + * name and columns. + * + * @param string $name Name of the table + * @param array $def Table definition array + * + * @return boolean success flag + */ + public function buildCreateTable($name, $def) + { + $def = $this->validateDef($name, $def); + $def = $this->filterDef($def); + $sql = array(); - for ($i = 0; $i < count($columns); $i++) { + foreach ($def['fields'] as $col => $colDef) { + $this->appendColumnDef($sql, $col, $colDef); + } - $cd =& $columns[$i]; + // Primary, unique, and foreign keys are constraints, so go within + // the CREATE TABLE statement normally. + if (!empty($def['primary key'])) { + $this->appendPrimaryKeyDef($sql, $def['primary key']); + } - if ($i > 0) { - $sql .= ",\n"; + if (!empty($def['unique keys'])) { + foreach ($def['unique keys'] as $col => $colDef) { + $this->appendUniqueKeyDef($sql, $col, $colDef); } + } - $sql .= $this->_columnSql($cd); - - switch ($cd->key) { - case 'UNI': - $uniques[] = $cd->name; - break; - case 'PRI': - $primary[] = $cd->name; - break; - case 'MUL': - $indices[] = $cd->name; - break; + if (!empty($def['foreign keys'])) { + foreach ($def['foreign keys'] as $keyName => $keyDef) { + $this->appendForeignKeyDef($sql, $keyName, $keyDef); } } - if (count($primary) > 0) { // it really should be... - $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")"; + // Wrap the CREATE TABLE around the main body chunks... + $statements = array(); + $statements[] = $this->startCreateTable($name, $def) . "\n" . + implode($sql, ",\n") . "\n" . + $this->endCreateTable($name, $def); + + // Multi-value indexes are advisory and for best portability + // should be created as separate statements. + if (!empty($def['indexes'])) { + foreach ($def['indexes'] as $col => $colDef) { + $this->appendCreateIndex($statements, $name, $col, $colDef); + } } - - foreach ($uniques as $u) { - $sql .= ",\nunique index {$name}_{$u}_idx ($u)"; + if (!empty($def['fulltext indexes'])) { + foreach ($def['fulltext indexes'] as $col => $colDef) { + $this->appendCreateFulltextIndex($statements, $name, $col, $colDef); + } } - foreach ($indices as $i) { - $sql .= ",\nindex {$name}_{$i}_idx ($i)"; - } + return $statements; + } - $sql .= "); "; + /** + * Set up a 'create table' SQL statement. + * + * @param string $name table name + * @param array $def table definition + * @param $string + */ + function startCreateTable($name, array $def) + { + return 'CREATE TABLE ' . $this->quoteIdentifier($name) . ' ('; + } - $res = $this->conn->query($sql); + /** + * Close out a 'create table' SQL statement. + * + * @param string $name table name + * @param array $def table definition + * @return string + */ + function endCreateTable($name, array $def) + { + return ')'; + } - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + /** + * Append an SQL fragment with a column definition in a CREATE TABLE statement. + * + * @param array $sql + * @param string $name + * @param array $def + */ + function appendColumnDef(array &$sql, $name, array $def) + { + $sql[] = "$name " . $this->columnSql($def); + } + + /** + * Append an SQL fragment with a constraint definition for a primary + * key in a CREATE TABLE statement. + * + * @param array $sql + * @param array $def + */ + function appendPrimaryKeyDef(array &$sql, array $def) + { + $sql[] = "PRIMARY KEY " . $this->buildIndexList($def); + } + + /** + * Append an SQL fragment with a constraint definition for a unique + * key in a CREATE TABLE statement. + * + * @param array $sql + * @param string $name + * @param array $def + */ + function appendUniqueKeyDef(array &$sql, $name, array $def) + { + $sql[] = "CONSTRAINT $name UNIQUE " . $this->buildIndexList($def); + } + + /** + * Append an SQL fragment with a constraint definition for a foreign + * key in a CREATE TABLE statement. + * + * @param array $sql + * @param string $name + * @param array $def + */ + function appendForeignKeyDef(array &$sql, $name, array $def) + { + if (count($def) != 2) { + throw new Exception("Invalid foreign key def for $name: " . var_export($def, true)); } + list($refTable, $map) = $def; + $srcCols = array_keys($map); + $refCols = array_values($map); + $sql[] = "CONSTRAINT $name FOREIGN KEY " . + $this->buildIndexList($srcCols) . + " REFERENCES " . + $this->quoteIdentifier($refTable) . + " " . + $this->buildIndexList($refCols); + } - return true; + /** + * Append an SQL statement with an index definition for an advisory + * index over one or more columns on a table. + * + * @param array $statements + * @param string $table + * @param string $name + * @param array $def + */ + function appendCreateIndex(array &$statements, $table, $name, array $def) + { + $statements[] = "CREATE INDEX $name ON $table " . $this->buildIndexList($def); + } + + /** + * Append an SQL statement with an index definition for a full-text search + * index over one or more columns on a table. + * + * @param array $statements + * @param string $table + * @param string $name + * @param array $def + */ + function appendCreateFulltextIndex(array &$statements, $table, $name, array $def) + { + throw new Exception("Fulltext index not supported in this database"); + } + + /** + * Append an SQL statement to drop an index from a table. + * + * @param array $statements + * @param string $table + * @param string $name + * @param array $def + */ + function appendDropIndex(array &$statements, $table, $name) + { + $statements[] = "DROP INDEX $name ON " . $this->quoteIdentifier($table); + } + + function buildIndexList(array $def) + { + // @fixme + return '(' . implode(',', array_map(array($this, 'buildIndexItem'), $def)) . ')'; + } + + function buildIndexItem($def) + { + if (is_array($def)) { + list($name, $size) = $def; + return $this->quoteIdentifier($name) . '(' . intval($size) . ')'; + } + return $this->quoteIdentifier($def); } /** @@ -223,7 +375,7 @@ class Schema } if (empty($name)) { - $name = "$table_".implode("_", $columnNames)."_idx"; + $name = "{$table}_".implode("_", $columnNames)."_idx"; } $res = $this->conn->query("ALTER TABLE $table ". @@ -338,46 +490,80 @@ class Schema * alter the table to match the column definitions. * * @param string $tableName name of the table - * @param array $columns array of ColumnDef - * objects for the table + * @param array $def Table definition array * * @return boolean success flag */ - public function ensureTable($tableName, $columns) + public function ensureTable($tableName, $def) { - // XXX: DB engine portability -> toilet + $statements = $this->buildEnsureTable($tableName, $def); + return $this->runSqlSet($statements); + } - try { - $td = $this->getTableDef($tableName); - } catch (Exception $e) { - if (preg_match('/no such table/', $e->getMessage())) { - return $this->createTable($tableName, $columns); - } else { - throw $e; + /** + * Run a given set of SQL commands on the connection in sequence. + * Empty input is ok. + * + * @fixme if multiple statements, wrap in a transaction? + * @param array $statements + * @return boolean success flag + */ + function runSqlSet(array $statements) + { + $ok = true; + foreach ($statements as $sql) { + if (defined('DEBUG_INSTALLER')) { + echo "<tt>" . htmlspecialchars($sql) . "</tt><br/>\n"; + } + $res = $this->conn->query($sql); + + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); } } + return $ok; + } - $cur = $this->_names($td->columns); - $new = $this->_names($columns); + /** + * Check a table's status, and if needed build a set + * of SQL statements which change it to be consistent + * with the given table definition. + * + * If the table does not yet exist, statements will + * be returned to create the table. If it does exist, + * statements will be returned to alter the table to + * match the column definitions. + * + * @param string $tableName name of the table + * @param array $columns array of ColumnDef + * objects for the table + * + * @return array of SQL statements + */ - $toadd = array_diff($new, $cur); - $todrop = array_diff($cur, $new); - $same = array_intersect($new, $cur); - $tomod = array(); + function buildEnsureTable($tableName, array $def) + { + try { + $old = $this->getTableDef($tableName); + } catch (SchemaTableMissingException $e) { + return $this->buildCreateTable($tableName, $def); + } - foreach ($same as $m) { - $curCol = $this->_byName($td->columns, $m); - $newCol = $this->_byName($columns, $m); + // Filter the DB-independent table definition to match the current + // database engine's features and limitations. + $def = $this->validateDef($tableName, $def); + $def = $this->filterDef($def); - if (!$newCol->equals($curCol)) { - $tomod[] = $newCol->name; - } - } + $statements = array(); + $fields = $this->diffArrays($old, $def, 'fields', array($this, 'columnsEqual')); + $uniques = $this->diffArrays($old, $def, 'unique keys'); + $indexes = $this->diffArrays($old, $def, 'indexes'); + $foreign = $this->diffArrays($old, $def, 'foreign keys'); - if (count($toadd) + count($todrop) + count($tomod) == 0) { - // nothing to do - return true; + // Drop any obsolete or modified indexes ahead... + foreach ($indexes['del'] + $indexes['mod'] as $indexName) { + $this->appendDropIndex($statements, $tableName, $indexName); } // For efficiency, we want this all in one @@ -385,31 +571,200 @@ class Schema $phrase = array(); - foreach ($toadd as $columnName) { - $cd = $this->_byName($columns, $columnName); + foreach ($foreign['del'] + $foreign['mod'] as $keyName) { + $this->appendAlterDropForeign($phrase, $keyName); + } + + foreach ($uniques['del'] + $uniques['mod'] as $keyName) { + $this->appendAlterDropUnique($phrase, $keyName); + } - $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd); + foreach ($fields['add'] as $columnName) { + $this->appendAlterAddColumn($phrase, $columnName, + $def['fields'][$columnName]); } - foreach ($todrop as $columnName) { - $phrase[] = 'DROP COLUMN ' . $columnName; + foreach ($fields['mod'] as $columnName) { + $this->appendAlterModifyColumn($phrase, $columnName, + $old['fields'][$columnName], + $def['fields'][$columnName]); } - foreach ($tomod as $columnName) { - $cd = $this->_byName($columns, $columnName); + foreach ($fields['del'] as $columnName) { + $this->appendAlterDropColumn($phrase, $columnName); + } - $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd); + foreach ($uniques['mod'] + $uniques['add'] as $keyName) { + $this->appendAlterAddUnique($phrase, $keyName, $def['unique keys'][$keyName]); } - $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase); + foreach ($foreign['mod'] + $foreign['add'] as $keyName) { + $this->appendAlterAddForeign($phrase, $keyName, $def['foreign keys'][$keyName]); + } - $res = $this->conn->query($sql); + $this->appendAlterExtras($phrase, $tableName, $def); - if (PEAR::isError($res)) { - throw new Exception($res->getMessage()); + if (count($phrase) > 0) { + $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(",\n", $phrase); + $statements[] = $sql; } - return true; + // Now create any indexes... + foreach ($indexes['mod'] + $indexes['add'] as $indexName) { + $this->appendCreateIndex($statements, $tableName, $indexName, $def['indexes'][$indexName]); + } + + return $statements; + } + + function diffArrays($oldDef, $newDef, $section, $compareCallback=null) + { + $old = isset($oldDef[$section]) ? $oldDef[$section] : array(); + $new = isset($newDef[$section]) ? $newDef[$section] : array(); + + $oldKeys = array_keys($old); + $newKeys = array_keys($new); + + $toadd = array_diff($newKeys, $oldKeys); + $todrop = array_diff($oldKeys, $newKeys); + $same = array_intersect($newKeys, $oldKeys); + $tomod = array(); + $tokeep = array(); + + // Find which fields have actually changed definition + // in a way that we need to tweak them for this DB type. + foreach ($same as $name) { + if ($compareCallback) { + $same = call_user_func($compareCallback, $old[$name], $new[$name]); + } else { + $same = ($old[$name] == $new[$name]); + } + if ($same) { + $tokeep[] = $name; + continue; + } + $tomod[] = $name; + } + return array('add' => $toadd, + 'del' => $todrop, + 'mod' => $tomod, + 'keep' => $tokeep, + 'count' => count($toadd) + count($todrop) + count($tomod)); + } + + /** + * Append phrase(s) to an array of partial ALTER TABLE chunks in order + * to add the given column definition to the table. + * + * @param array $phrase + * @param string $columnName + * @param array $cd + */ + function appendAlterAddColumn(array &$phrase, $columnName, array $cd) + { + $phrase[] = 'ADD COLUMN ' . + $this->quoteIdentifier($columnName) . + ' ' . + $this->columnSql($cd); + } + + /** + * Append phrase(s) to an array of partial ALTER TABLE chunks in order + * to alter the given column from its old state to a new one. + * + * @param array $phrase + * @param string $columnName + * @param array $old previous column definition as found in DB + * @param array $cd current column definition + */ + function appendAlterModifyColumn(array &$phrase, $columnName, array $old, array $cd) + { + $phrase[] = 'MODIFY COLUMN ' . + $this->quoteIdentifier($columnName) . + ' ' . + $this->columnSql($cd); + } + + /** + * Append phrase(s) to an array of partial ALTER TABLE chunks in order + * to drop the given column definition from the table. + * + * @param array $phrase + * @param string $columnName + */ + function appendAlterDropColumn(array &$phrase, $columnName) + { + $phrase[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName); + } + + function appendAlterAddUnique(array &$phrase, $keyName, array $def) + { + $sql = array(); + $sql[] = 'ADD'; + $this->appendUniqueKeyDef($sql, $keyName, $def); + $phrase[] = implode(' ', $sql); + } + + function appendAlterAddForeign(array &$phrase, $keyName, array $def) + { + $sql = array(); + $sql[] = 'ADD'; + $this->appendForeignKeyDef($sql, $keyName, $def); + $phrase[] = implode(' ', $sql); + } + + function appendAlterDropUnique(array &$phrase, $keyName) + { + $phrase[] = 'DROP CONSTRAINT ' . $keyName; + } + + function appendAlterDropForeign(array &$phrase, $keyName) + { + $phrase[] = 'DROP FOREIGN KEY ' . $keyName; + } + + function appendAlterExtras(array &$phrase, $tableName, array $def) + { + // no-op + } + + /** + * Quote a db/table/column identifier if necessary. + * + * @param string $name + * @return string + */ + function quoteIdentifier($name) + { + return $name; + } + + function quoteDefaultValue($cd) + { + if ($cd['type'] == 'datetime' && $cd['default'] == 'CURRENT_TIMESTAMP') { + return $cd['default']; + } else { + return $this->quoteValue($cd['default']); + } + } + + function quoteValue($val) + { + return $this->conn->quoteSmart($val); + } + + /** + * Check if two column definitions are equivalent. + * The default implementation checks _everything_ but in many cases + * you may be able to discard a bunch of equivalencies. + * + * @param array $a + * @param array $b + * @return boolean + */ + function columnsEqual(array $a, array $b) + { + return !array_diff_assoc($a, $b) && !array_diff_assoc($b, $a); } /** @@ -421,7 +776,7 @@ class Schema * @return array strings for name values */ - private function _names($cds) + protected function _names($cds) { $names = array(); @@ -442,7 +797,7 @@ class Schema * @return ColumnDef matching item or null if no match. */ - private function _byName($cds, $name) + protected function _byName($cds, $name) { foreach ($cds as $cd) { if ($cd->name == $name) { @@ -465,32 +820,194 @@ class Schema * @return string correct SQL for that column */ - private function _columnSql($cd) + function columnSql(array $cd) { - $sql = "{$cd->name} "; + $line = array(); + $line[] = $this->typeAndSize($cd); + + if (isset($cd['default'])) { + $line[] = 'default'; + $line[] = $this->quoteDefaultValue($cd); + } else if (!empty($cd['not null'])) { + // Can't have both not null AND default! + $line[] = 'not null'; + } - if (!empty($cd->size)) { - $sql .= "{$cd->type}({$cd->size}) "; - } else { - $sql .= "{$cd->type} "; + return implode(' ', $line); + } + + /** + * + * @param string $column canonical type name in defs + * @return string native DB type name + */ + function mapType($column) + { + return $column; + } + + function typeAndSize($column) + { + //$type = $this->mapType($column); + $type = $column['type']; + if (isset($column['size'])) { + $type = $column['size'] . $type; } + $lengths = array(); - if (!empty($cd->default)) { - $sql .= "default {$cd->default} "; + if (isset($column['precision'])) { + $lengths[] = $column['precision']; + if (isset($column['scale'])) { + $lengths[] = $column['scale']; + } + } else if (isset($column['length'])) { + $lengths[] = $column['length']; + } + + if ($lengths) { + return $type . '(' . implode(',', $lengths) . ')'; } else { - $sql .= ($cd->nullable) ? "null " : "not null "; + return $type; } + } + + /** + * Convert an old-style set of ColumnDef objects into the current + * Drupal-style schema definition array, for backwards compatibility + * with plugins written for 0.9.x. + * + * @param string $tableName + * @param array $defs: array of ColumnDef objects + * @return array + */ + protected function oldToNew($tableName, array $defs) + { + $table = array(); + $prefixes = array( + 'tiny', + 'small', + 'medium', + 'big', + ); + foreach ($defs as $cd) { + $column = array(); + $column['type'] = $cd->type; + foreach ($prefixes as $prefix) { + if (substr($cd->type, 0, strlen($prefix)) == $prefix) { + $column['type'] = substr($cd->type, strlen($prefix)); + $column['size'] = $prefix; + break; + } + } - if (!empty($cd->auto_increment)) { - $sql .= " auto_increment "; + if ($cd->size) { + if ($cd->type == 'varchar' || $cd->type == 'char') { + $column['length'] = $cd->size; + } + } + if (!$cd->nullable) { + $column['not null'] = true; + } + if ($cd->auto_increment) { + $column['type'] = 'serial'; + } + if ($cd->default) { + $column['default'] = $cd->default; + } + $table['fields'][$cd->name] = $column; + + if ($cd->key == 'PRI') { + // If multiple columns are defined as primary key, + // we'll pile them on in sequence. + if (!isset($table['primary key'])) { + $table['primary key'] = array(); + } + $table['primary key'][] = $cd->name; + } else if ($cd->key == 'MUL') { + // Individual multiple-value indexes are only per-column + // using the old ColumnDef syntax. + $idx = "{$tableName}_{$cd->name}_idx"; + $table['indexes'][$idx] = array($cd->name); + } else if ($cd->key == 'UNI') { + // Individual unique-value indexes are only per-column + // using the old ColumnDef syntax. + $idx = "{$tableName}_{$cd->name}_idx"; + $table['unique keys'][$idx] = array($cd->name); + } } - if (!empty($cd->extra)) { - $sql .= "{$cd->extra} "; + return $table; + } + + /** + * Filter the given table definition array to match features available + * in this database. + * + * This lets us strip out unsupported things like comments, foreign keys, + * or type variants that we wouldn't get back from getTableDef(). + * + * @param array $tableDef + */ + function filterDef(array $tableDef) + { + return $tableDef; + } + + /** + * Validate a table definition array, checking for basic structure. + * + * If necessary, converts from an old-style array of ColumnDef objects. + * + * @param string $tableName + * @param array $def: table definition array + * @return array validated table definition array + * + * @throws Exception on wildly invalid input + */ + function validateDef($tableName, array $def) + { + if (isset($def[0]) && $def[0] instanceof ColumnDef) { + $def = $this->oldToNew($tableName, $def); + } + + // A few quick checks :D + if (!isset($def['fields'])) { + throw new Exception("Invalid table definition for $tableName: no fields."); } - return $sql; + return $def; + } + + function isNumericType($type) + { + $type = strtolower($type); + $known = array('int', 'serial', 'numeric'); + return in_array($type, $known); } + + /** + * Pull info from the query into a fun-fun array of dooooom + * + * @param string $sql + * @return array of arrays + */ + protected function fetchQueryData($sql) + { + $res = $this->conn->query($sql); + if (PEAR::isError($res)) { + throw new Exception($res->getMessage()); + } + + $out = array(); + $row = array(); + while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) { + $out[] = $row; + } + $res->free(); + + return $out; + } + } class SchemaTableMissingException extends Exception diff --git a/lib/schemaupdater.php b/lib/schemaupdater.php new file mode 100644 index 000000000..64f7c596d --- /dev/null +++ b/lib/schemaupdater.php @@ -0,0 +1,126 @@ +<?php + +/** + * StatusNet, the distributed open-source microblogging tool + * + * Database schema utilities + * + * 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 Database + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class SchemaUpdater +{ + public function __construct($schema) + { + $this->schema = $schema; + $this->checksums = $this->getChecksums(); + } + + /** + * @param string $tableName + * @param array $tableDef + */ + public function register($tableName, array $tableDef) + { + $this->tables[$tableName] = $tableDef; + } + + /** + * Go ping em! + * + * @fixme handle tables that belong on different database servers...? + */ + public function checkSchema() + { + $checksums = $this->checksums; + foreach ($this->tables as $table => $def) { + $checksum = $this->checksum($def); + if (empty($checksums[$table])) { + common_log(LOG_DEBUG, "No previous schema_version for $table: updating to $checksum"); + } else if ($checksums[$table] == $checksum) { + common_log(LOG_DEBUG, "Last schema_version for $table up to date: $checksum"); + continue; + } else { + common_log(LOG_DEBUG, "Last schema_version for $table is {$checksums[$table]}: updating to $checksum"); + } + //$this->conn->query('BEGIN'); + $this->schema->ensureTable($table, $def); + $this->saveChecksum($table, $checksum); + //$this->conn->commit(); + } + } + + /** + * Calculate a checksum for this table definition array. + * + * @param array $def + * @return string + */ + public function checksum(array $def) + { + $flat = serialize($def); + return sha1($flat); + } + + /** + * Pull all known table checksums into an array for easy lookup. + * + * @return array: associative array of table names to checksum strings + */ + protected function getChecksums() + { + $checksums = array(); + + $sv = new Schema_version(); + $sv->find(); + while ($sv->fetch()) { + $checksums[$sv->table_name] = $sv->checksum; + } + + return $checksums; + } + + /** + * Save or update current available checksums. + * + * @param string $table + * @param string $checksum + */ + protected function saveChecksum($table, $checksum) + { + $sv = new Schema_version(); + $sv->table_name = $table; + $sv->checksum = $checksum; + $sv->modified = common_sql_now(); + if (isset($this->checksums[$table])) { + $sv->update(); + } else { + $sv->insert(); + } + $this->checksums[$table] = $checksum; + } +} diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php index 2f9f6e32e..ea09b6fb2 100644 --- a/lib/spawningdaemon.php +++ b/lib/spawningdaemon.php @@ -204,7 +204,7 @@ abstract class SpawningDaemon extends Daemon // Reconnect main memcached, or threads will stomp on // each other and corrupt their requests. - $cache = common_memcache(); + $cache = Cache::instance(); if ($cache) { $cache->reconnect(); } diff --git a/lib/statusnet.php b/lib/statusnet.php index 85b46bbb3..4c2aacd8f 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -176,6 +176,11 @@ class StatusNet { // Load default plugins foreach (common_config('plugins', 'default') as $name => $params) { + $key = 'disable-' . $name; + if (common_config('plugins', $key)) { + continue; + } + if (is_null($params)) { addPlugin($name); } else if (is_array($params)) { @@ -240,7 +245,7 @@ class StatusNet * Establish default configuration based on given or default server and path * Sets global $_server, $_path, and $config */ - protected static function initDefaults($server, $path) + public static function initDefaults($server, $path) { global $_server, $_path, $config; @@ -356,7 +361,6 @@ class StatusNet } // Backwards compatibility - if (array_key_exists('memcached', $config)) { if ($config['memcached']['enabled']) { addPlugin('Memcache', array('servers' => $config['memcached']['server'])); @@ -366,6 +370,21 @@ class StatusNet $config['cache']['base'] = $config['memcached']['base']; } } + if (array_key_exists('xmpp', $config)) { + if ($config['xmpp']['enabled']) { + addPlugin('xmpp', array( + 'server' => $config['xmpp']['server'], + 'port' => $config['xmpp']['port'], + 'user' => $config['xmpp']['user'], + 'resource' => $config['xmpp']['resource'], + 'encryption' => $config['xmpp']['encryption'], + 'password' => $config['xmpp']['password'], + 'host' => $config['xmpp']['host'], + 'debug' => $config['xmpp']['debug'], + 'public' => $config['xmpp']['public'] + )); + } + } } /** diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index fc98c77d4..1d9a5ad20 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -578,7 +578,7 @@ class StompQueueManager extends QueueManager function incDeliveryCount($msgId) { $count = 0; - $cache = common_memcache(); + $cache = Cache::instance(); if ($cache) { $key = 'statusnet:stomp:message-retries:' . $msgId; $count = $cache->increment($key); diff --git a/lib/urlshortenerplugin.php b/lib/urlshortenerplugin.php new file mode 100644 index 000000000..8acfac26f --- /dev/null +++ b/lib/urlshortenerplugin.php @@ -0,0 +1,155 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Superclass for plugins that do URL shortening + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Superclass for plugins that do URL shortening + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +abstract class UrlShortenerPlugin extends Plugin +{ + public $shortenerName; + public $freeService = false; + + // Url Shortener plugins should implement some (or all) + // of these methods + + /** + * Make an URL shorter. + * + * @param string $url URL to shorten + * + * @return string shortened version of the url, or null on failure + */ + + protected abstract function shorten($url); + + /** + * Utility to get the data at an URL + * + * @param string $url URL to fetch + * + * @return string response body + * + * @todo rename to code-standard camelCase httpGet() + */ + + protected function http_get($url) + { + $request = HTTPClient::start(); + $response = $request->get($url); + return $response->getBody(); + } + + /** + * Utility to post a request and get a response URL + * + * @param string $url URL to fetch + * @param array $data post parameters + * + * @return string response body + * + * @todo rename to code-standard httpPost() + */ + + protected function http_post($url, $data) + { + $request = HTTPClient::start(); + $response = $request->post($url, null, $data); + return $response->getBody(); + } + + // Hook handlers + + /** + * Called when all plugins have been initialized + * + * @return boolean hook value + */ + + function onInitializePlugin() + { + if (!isset($this->shortenerName)) { + throw new Exception("must specify a shortenerName"); + } + return true; + } + + /** + * Called when a showing the URL shortener drop-down box + * + * Properties of the shortening service currently only + * include whether it's a free service. + * + * @param array &$shorteners array mapping shortener name to properties + * + * @return boolean hook value + */ + + function onGetUrlShorteners(&$shorteners) + { + $shorteners[$this->shortenerName] = + array('freeService' => $this->freeService); + return true; + } + + /** + * Called to shorten an URL + * + * @param string $url URL to shorten + * @param string $shortenerName Shortening service. Don't handle if it's + * not you! + * @param string &$shortenedUrl URL after shortening; out param. + * + * @return boolean hook value + */ + + function onStartShortenUrl($url, $shortenerName, &$shortenedUrl) + { + if ($shortenerName == $this->shortenerName) { + $result = $this->shorten($url); + if (isset($result) && $result != null && $result !== false) { + $shortenedUrl = $result; + common_log(LOG_INFO, + __CLASS__ . ": $this->shortenerName ". + "shortened $url to $shortenedUrl"); + return false; + } + } + return true; + } +} diff --git a/lib/util.php b/lib/util.php index da36121ff..3d4adcf4b 100644 --- a/lib/util.php +++ b/lib/util.php @@ -157,22 +157,38 @@ function common_timezone() return common_config('site', 'timezone'); } +function common_valid_language($lang) +{ + if ($lang) { + // Validate -- we don't want to end up with a bogus code + // left over from some old junk. + foreach (common_config('site', 'languages') as $code => $info) { + if ($info['lang'] == $lang) { + return true; + } + } + } + return false; +} + function common_language() { + // Allow ?uselang=xx override, very useful for debugging + // and helping translators check usage and context. + if (isset($_GET['uselang'])) { + $uselang = strval($_GET['uselang']); + if (common_valid_language($uselang)) { + return $uselang; + } + } + // If there is a user logged in and they've set a language preference // then return that one... if (_have_config() && common_logged_in()) { $user = common_current_user(); - $user_language = $user->language; - - if ($user->language) { - // Validate -- we don't want to end up with a bogus code - // left over from some old junk. - foreach (common_config('site', 'languages') as $code => $info) { - if ($info['lang'] == $user_language) { - return $user_language; - } - } + + if (common_valid_language($user->language)) { + return $user->language; } } @@ -1012,9 +1028,21 @@ function common_linkify($url) { */ function common_shorten_links($text, $always = false, User $user=null) { - $maxLength = Notice::maxContent(); - if (!$always && ($maxLength == 0 || mb_strlen($text) <= $maxLength)) return $text; - return common_replace_urls_callback($text, array('File_redirection', 'makeShort'), $user); + common_debug("common_shorten_links() called"); + + $user = common_current_user(); + + $maxLength = User_urlshortener_prefs::maxNoticeLength($user); + + common_debug("maxLength = $maxLength"); + + if ($always || mb_strlen($text) > $maxLength) { + common_debug("Forcing shortening"); + return common_replace_urls_callback($text, array('File_redirection', 'forceShort'), $user); + } else { + common_debug("Not forcing shortening"); + return common_replace_urls_callback($text, array('File_redirection', 'makeShort'), $user); + } } /** @@ -1429,14 +1457,8 @@ function common_redirect($url, $code=307) exit; } -function common_broadcast_notice($notice, $remote=false) -{ - // DO NOTHING! -} +// Stick the notice on the queue -/** - * Stick the notice on the queue. - */ function common_enqueue_notice($notice) { static $localTransports = array('omb', @@ -1450,18 +1472,9 @@ function common_enqueue_notice($notice) $transports[] = 'plugin'; } - $xmpp = common_config('xmpp', 'enabled'); - - if ($xmpp) { - $transports[] = 'jabber'; - } - // We can skip these for gatewayed notices. if ($notice->isLocal()) { $transports = array_merge($transports, $localTransports); - if ($xmpp) { - $transports[] = 'public'; - } } if (Event::handle('StartEnqueueNotice', array($notice, &$transports))) { @@ -2000,21 +2013,6 @@ function common_session_token() return $_SESSION['token']; } -function common_cache_key($extra) -{ - return Cache::key($extra); -} - -function common_keyize($str) -{ - return Cache::keyize($str); -} - -function common_memcache() -{ - return Cache::instance(); -} - function common_license_terms($uri) { if(preg_match('/creativecommons.org\/licenses\/([^\/]+)/', $uri, $matches)) { @@ -2055,33 +2053,40 @@ function common_database_tablename($tablename) /** * Shorten a URL with the current user's configured shortening service, * or ur1.ca if configured, or not at all if no shortening is set up. - * Length is not considered. * - * @param string $long_url + * @param string $long_url original URL * @param User $user to specify a particular user's options + * @param boolean $force Force shortening (used when notice is too long) * @return string may return the original URL if shortening failed * * @fixme provide a way to specify a particular shortener */ -function common_shorten_url($long_url, User $user=null) +function common_shorten_url($long_url, User $user=null, $force = false) { + common_debug("Shortening URL '$long_url' (force = $force)"); + $long_url = trim($long_url); - if (empty($user)) { - // Current web session - $user = common_current_user(); - } - if (empty($user)) { - // common current user does not find a user when called from the XMPP daemon - // therefore we'll set one here fix, so that XMPP given URLs may be shortened - $shortenerName = 'ur1.ca'; - } else { - $shortenerName = $user->urlshorteningservice; + + $user = common_current_user(); + + $maxUrlLength = User_urlshortener_prefs::maxUrlLength($user); + common_debug("maxUrlLength = $maxUrlLength"); + + // $force forces shortening even if it's not strictly needed + + if (mb_strlen($long_url) < $maxUrlLength && !$force) { + common_debug("Skipped shortening URL."); + return $long_url; } - if(Event::handle('StartShortenUrl', array($long_url,$shortenerName,&$shortenedUrl))){ + $shortenerName = User_urlshortener_prefs::urlShorteningService($user); + + common_debug("Shortener name = '$shortenerName'"); + + if (Event::handle('StartShortenUrl', array($long_url, $shortenerName, &$shortenedUrl))) { //URL wasn't shortened, so return the long url return $long_url; - }else{ + } else { //URL was shortened, so return the result return trim($shortenedUrl); } diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php deleted file mode 100644 index 585d044c7..000000000 --- a/lib/xmppmanager.php +++ /dev/null @@ -1,491 +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); } - -/** - * 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; - protected $conn = 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 = StatusNet::currentSite(); - 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 = StatusNet::currentSite(); - $this->resource = common_config('xmpp', 'resource') . 'daemon'; - } - - /** - * Initialize connection to server. - * @return boolean true on success - */ - public function start($master) - { - parent::start($master); - $this->switchSite(); - - require_once INSTALLDIR . "/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->log(LOG_DEBUG, "Initializing stanza handlers."); - - $this->conn->addEventHandler('message', 'handle_message', $this); - $this->conn->addEventHandler('presence', 'handle_presence', $this); - $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this); - - $this->conn->setReconnectTimeout(600); - // @todo Needs i18n? - jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', 100); - - 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() - { - if ($this->conn) { - return array($this->conn->getSocket()); - } else { - return array(); - } - } - - /** - * 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()); - } - } - } - } - - /** - * For queue handlers to pass us a message to push out, - * if we're active. - * - * @fixme should this be blocking etc? - * - * @param string $msg XML stanza to send - * @return boolean success - */ - public function send($msg) - { - if ($this->conn && !$this->conn->isDisconnected()) { - $bytes = $this->conn->send($msg); - if ($bytes > 0) { - $this->conn->processTime(0); - return true; - } else { - common_log(LOG_ERR, __METHOD__ . ' failed: 0 bytes sent'); - return false; - } - } else { - // Can't send right now... - common_log(LOG_ERR, __METHOD__ . ' failed: XMPP server connection currently down'); - return false; - } - } - - /** - * 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', 100); - } - - - function get_user($from) - { - $user = User::staticGet('jabber', jabber_normalize_jid($from)); - return $user; - } - - /** - * XMPP callback for handling message input... - * @param array $pl XMPP payload - */ - function handle_message(&$pl) - { - $from = jabber_normalize_jid($pl['from']); - - if ($pl['type'] != 'chat') { - $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from: " . $pl['xml']->toString()); - return; - } - - if (mb_strlen($pl['body']) == 0) { - $this->log(LOG_WARNING, "Ignoring message with empty body from $from: " . $pl['xml']->toString()); - return; - } - - // Forwarded from another daemon for us to handle; this shouldn't - // happen any more but we might get some legacy items. - if ($this->is_self($from)) { - $this->log(LOG_INFO, "Got forwarded notice from self ($from)."); - $from = $this->get_ofrom($pl); - $this->log(LOG_INFO, "Originally sent by $from."); - if (is_null($from) || $this->is_self($from)) { - $this->log(LOG_INFO, "Ignoring notice originally sent by $from."); - return; - } - } - - $user = $this->get_user($from); - - // For common_current_user to work - global $_cur; - $_cur = $user; - - if (!$user) { - // TRANS: %s is the URL to the StatusNet site's Instant Messaging settings. - $this->from_site($from, sprintf(_('Unknown user. Go to %s ' . - 'to add your address to your account'),common_local_url('imsettings'))); - $this->log(LOG_WARNING, 'Message from unknown user ' . $from); - return; - } - if ($this->handle_command($user, $pl['body'])) { - $this->log(LOG_INFO, "Command message by $from handled."); - return; - } else if ($this->is_autoreply($pl['body'])) { - $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from); - return; - } else if ($this->is_otr($pl['body'])) { - $this->log(LOG_INFO, 'Ignoring OTR from ' . $from); - return; - } else { - - $this->log(LOG_INFO, 'Posting a notice from ' . $user->nickname); - - $this->add_notice($user, $pl); - } - - $user->free(); - unset($user); - unset($_cur); - - unset($pl['xml']); - $pl['xml'] = null; - - $pl = null; - unset($pl); - } - - function is_self($from) - { - return preg_match('/^'.strtolower(jabber_daemon_address()).'/', strtolower($from)); - } - - function get_ofrom($pl) - { - $xml = $pl['xml']; - $addresses = $xml->sub('addresses'); - if (!$addresses) { - $this->log(LOG_WARNING, 'Forwarded message without addresses'); - return null; - } - $address = $addresses->sub('address'); - if (!$address) { - $this->log(LOG_WARNING, 'Forwarded message without address'); - return null; - } - if (!array_key_exists('type', $address->attrs)) { - $this->log(LOG_WARNING, 'No type for forwarded message'); - return null; - } - $type = $address->attrs['type']; - if ($type != 'ofrom') { - $this->log(LOG_WARNING, 'Type of forwarded message is not ofrom'); - return null; - } - if (!array_key_exists('jid', $address->attrs)) { - $this->log(LOG_WARNING, 'No jid for forwarded message'); - return null; - } - $jid = $address->attrs['jid']; - if (!$jid) { - $this->log(LOG_WARNING, 'Could not get jid from address'); - return null; - } - $this->log(LOG_DEBUG, 'Got message forwarded from jid ' . $jid); - return $jid; - } - - function is_autoreply($txt) - { - if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) { - return true; - } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) { - return true; - } else { - return false; - } - } - - function is_otr($txt) - { - if (preg_match('/^\?OTR/', $txt)) { - return true; - } else { - return false; - } - } - - function from_site($address, $msg) - { - $text = '['.common_config('site', 'name') . '] ' . $msg; - jabber_send_message($address, $text); - } - - function handle_command($user, $body) - { - $inter = new CommandInterpreter(); - $cmd = $inter->handle_command($user, $body); - if ($cmd) { - $chan = new XMPPChannel($this->conn); - $cmd->execute($chan); - return true; - } else { - return false; - } - } - - function add_notice(&$user, &$pl) - { - $body = trim($pl['body']); - $content_shortened = $user->shortenLinks($body); - if (Notice::contentTooLong($content_shortened)) { - $from = jabber_normalize_jid($pl['from']); - // TRANS: Response to XMPP source when it sent too long a message. - // TRANS: %1$d the maximum number of allowed characters (used for plural), %2$d is the sent number. - $this->from_site($from, sprintf(_m('Message too long. Maximum is %1$d character, you sent %2$d.', - 'Message too long. Maximum is %1$d characters, you sent %2$d.', - Notice::maxContent()), - Notice::maxContent(), - mb_strlen($content_shortened))); - return; - } - - try { - $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp'); - } catch (Exception $e) { - $this->log(LOG_ERR, $e->getMessage()); - $this->from_site($user->jabber, $e->getMessage()); - return; - } - - common_broadcast_notice($notice); - $this->log(LOG_INFO, - 'Added notice ' . $notice->id . ' from user ' . $user->nickname); - $notice->free(); - unset($notice); - } - - function handle_presence(&$pl) - { - $from = jabber_normalize_jid($pl['from']); - switch ($pl['type']) { - case 'subscribe': - # We let anyone subscribe - $this->subscribed($from); - $this->log(LOG_INFO, - 'Accepted subscription from ' . $from); - break; - case 'subscribed': - case 'unsubscribed': - case 'unsubscribe': - $this->log(LOG_INFO, - 'Ignoring "' . $pl['type'] . '" from ' . $from); - break; - default: - if (!$pl['type']) { - $user = User::staticGet('jabber', $from); - if (!$user) { - $this->log(LOG_WARNING, 'Presence from unknown user ' . $from); - return; - } - if ($user->updatefrompresence) { - $this->log(LOG_INFO, 'Updating ' . $user->nickname . - ' status from presence.'); - $this->add_notice($user, $pl); - } - $user->free(); - unset($user); - } - break; - } - unset($pl['xml']); - $pl['xml'] = null; - - $pl = null; - unset($pl); - } - - function log($level, $msg) - { - $text = 'XMPPDaemon('.$this->resource.'): '.$msg; - common_log($level, $text); - } - - function subscribed($to) - { - jabber_special_presence('subscribed', $to); - } - - /** - * Make sure we're on the right site configuration - */ - protected function switchSite() - { - if ($this->site != StatusNet::currentSite()) { - common_log(LOG_DEBUG, __METHOD__ . ": switching to site $this->site"); - $this->stats('switch'); - StatusNet::switchSite($this->site); - } - } -} diff --git a/lib/xmppoutqueuehandler.php b/lib/xmppoutqueuehandler.php deleted file mode 100644 index a4c9bbc4d..000000000 --- a/lib/xmppoutqueuehandler.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/* - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 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/>. - */ - -/** - * Queue handler for pre-processed outgoing XMPP messages. - * Formatted XML stanzas will have been pushed into the queue - * via the Queued_XMPP connection proxy, probably from some - * other queue processor. - * - * Here, the XML stanzas are simply pulled out of the queue and - * pushed out over the wire; an XmppManager is needed to set up - * and maintain the actual server connection. - * - * This queue will be run via XmppDaemon rather than QueueDaemon. - * - * @author Brion Vibber <brion@status.net> - */ -class XmppOutQueueHandler extends QueueHandler -{ - function transport() { - return 'xmppout'; - } - - /** - * Take a previously-queued XMPP stanza and send it out ot the server. - * @param string $msg - * @return boolean true on success - */ - function handle($msg) - { - assert(is_string($msg)); - - $xmpp = XmppManager::get(); - $ok = $xmpp->send($msg); - - return $ok; - } -} diff --git a/plugins/AccountManager/AccountManagementControlDocumentAction.php b/plugins/AccountManager/AccountManagementControlDocumentAction.php new file mode 100644 index 000000000..3fcea5af4 --- /dev/null +++ b/plugins/AccountManager/AccountManagementControlDocumentAction.php @@ -0,0 +1,98 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Implements the JSON Account Management endpoint + * + * 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 AccountManager + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Implements the JSON Account Management endpoint + * + * @category AccountManager + * @package StatusNet + * @author ECraig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class AccountManagementControlDocumentAction extends Action +{ + /** + * handle the action + * + * @param array $args unused. + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + header('Content-Type: application/json; charset=utf-8'); + + $amcd = array(); + + if(Event::handle('StartAccountManagementControlDocument', array(&$amcd))) { + + $amcd['version'] = 1; + $amcd['sessionstatus'] = array( + 'method' => 'GET', + 'path' => common_local_url('AccountManagementSessionStatus') + ); + $amcd['auth-methods'] = array( + 'username-password-form' => array( + 'connect' => array( + 'method' => 'POST', + 'path' => common_local_url('login'), + 'params' => array( + 'username' => 'nickname', + 'password' => 'password' + ) + ), + 'disconnect' => array( + 'method' => 'GET', + 'path' => common_local_url('logout') + ) + ) + ); + + Event::handle('EndAccountManagementControlDocument', array(&$amcd)); + } + + print json_encode($amcd); + + return true; + } + + function isReadOnly() + { + return true; + } +} diff --git a/plugins/AccountManager/AccountManagementSessionStatusAction.php b/plugins/AccountManager/AccountManagementSessionStatusAction.php new file mode 100644 index 000000000..48b6034ff --- /dev/null +++ b/plugins/AccountManager/AccountManagementSessionStatusAction.php @@ -0,0 +1,73 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Implements the session status Account Management endpoint + * + * 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 AccountManager + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Implements the session status Account Management endpoint + * + * @category AccountManager + * @package StatusNet + * @author ECraig Andrews <candrews@integralblue.com> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class AccountManagementSessionStatusAction extends Action +{ + /** + * handle the action + * + * @param array $args unused. + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $cur = common_current_user(); + if(empty($cur)) { + print 'none'; + } else { + //TODO it seems " should be escaped in the name and id, but the spec doesn't seem to indicate how to do that + print 'active; name="' . $cur->nickname . '"; id="' . $cur->nickname . '"'; + } + + return true; + } + + function isReadOnly() + { + return true; + } +} diff --git a/plugins/AccountManager/AccountManagerPlugin.php b/plugins/AccountManager/AccountManagerPlugin.php new file mode 100644 index 000000000..52dd64a24 --- /dev/null +++ b/plugins/AccountManager/AccountManagerPlugin.php @@ -0,0 +1,115 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to implement the Account Manager specification + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class AccountManagerPlugin extends Plugin +{ + + const AM_REL = 'acct-mgmt'; + + function __construct() + { + parent::__construct(); + } + + function onAutoload($cls) + { + switch ($cls) + { + case 'AccountManagementControlDocumentAction': + require_once(INSTALLDIR.'/plugins/AccountManager/AccountManagementControlDocumentAction.php'); + return false; + case 'AccountManagementSessionStatusAction': + require_once(INSTALLDIR.'/plugins/AccountManager/AccountManagementSessionStatusAction.php'); + return false; + } + } + + /** + * Hook for RouterInitialized event. + * + * @param Net_URL_Mapper $m path-to-action mapper + * @return boolean hook return + */ + function onRouterInitialized($m) + { + // Discovery actions + $m->connect('main/amcd.json', + array('action' => 'AccountManagementControlDocument')); + $m->connect('main/amsessionstatus', + array('action' => 'AccountManagementSessionStatus')); + return true; + } + + function onStartHostMetaLinks(&$links) { + $links[] = array('rel' => AccountManagerPlugin::AM_REL, + 'href' => common_local_url('AccountManagementControlDocument')); + } + + function onStartShowHTML($action) + { + //Account management discovery link + header('Link: <'.common_local_url('AccountManagementControlDocument').'>; rel="'. AccountManagerPlugin::AM_REL.'"; type="application/json"'); + + //Account management login status + $cur = common_current_user(); + if(empty($cur)) { + header('X-Account-Management-Status: none'); + } else { + //TODO it seems " should be escaped in the name and id, but the spec doesn't seem to indicate how to do that + header('X-Account-Management-Status: active; name="' . $cur->nickname . '"; id="' . $cur->nickname . '"'); + } + } + + function onLoginAction($action, &$login) { + switch ($action) + { + case 'AccountManagementControlDocument': + $login = true; + return false; + default: + return true; + } + + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'AccountManager', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:AccountManager', + 'rawdescription' => + _m('The Account Manager plugin implements the Account Manager specification.')); + return true; + } +} diff --git a/plugins/AccountManager/README b/plugins/AccountManager/README new file mode 100644 index 000000000..a0715927b --- /dev/null +++ b/plugins/AccountManager/README @@ -0,0 +1,16 @@ +The Account Manager plugin implements the Account Manager specification to "allow a server to describe an account-based user identification process in terms that a user-agent can understand." + +See https://wiki.mozilla.org/Labs/Weave/Identity/Account_Manager/Spec/Latest + +Installation +============ +add "addPlugin('accountManager');" +to the bottom of your config.php + +Settings +======== +none + +Example +======= +addPlugin('accountManager'); diff --git a/plugins/Aim/AimPlugin.php b/plugins/Aim/AimPlugin.php new file mode 100644 index 000000000..3a1799a2d --- /dev/null +++ b/plugins/Aim/AimPlugin.php @@ -0,0 +1,169 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Send and receive notices using the AIM network + * + * PHP version 5 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category IM + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} +// We bundle the phptoclib library... +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phptoclib'); + +/** + * Plugin for AIM + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AimPlugin extends ImPlugin +{ + public $user = null; + public $password = null; + public $publicFeed = array(); + + public $transport = 'aim'; + + function getDisplayName() + { + return _m('AIM'); + } + + function normalize($screenname) + { + $screenname = str_replace(" ","", $screenname); + return strtolower($screenname); + } + + function daemonScreenname() + { + return $this->user; + } + + function validate($screenname) + { + if(preg_match('/^[a-z]\w{2,15}$/i', $screenname)) { + return true; + }else{ + return false; + } + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'Aim': + require_once(INSTALLDIR.'/plugins/Aim/extlib/phptoclib/aimclassw.php'); + return false; + case 'AimManager': + include_once $dir . '/'.strtolower($cls).'.php'; + return false; + case 'Fake_Aim': + include_once $dir . '/'. $cls .'.php'; + return false; + default: + return true; + } + } + + function onStartImDaemonIoManagers(&$classes) + { + parent::onStartImDaemonIoManagers(&$classes); + $classes[] = new AimManager($this); // handles sending/receiving + return true; + } + + function microiduri($screenname) + { + return 'aim:' . $screenname; + } + + function sendMessage($screenname, $body) + { + $this->fake_aim->sendIm($screenname, $body); + $this->enqueueOutgoingRaw($this->fake_aim->would_be_sent); + return true; + } + + /** + * Accept a queued input message. + * + * @return true if processing completed, false if message should be reprocessed + */ + function receiveRawMessage($message) + { + $info=Aim::getMessageInfo($message); + $from = $info['from']; + $user = $this->getUser($from); + $notice_text = $info['message']; + + $this->handleIncoming($from, $notice_text); + + return true; + } + + function initialize(){ + if(!isset($this->user)){ + throw new Exception("must specify a user"); + } + if(!isset($this->password)){ + throw new Exception("must specify a password"); + } + + $this->fake_aim = new Fake_Aim($this->user,$this->password,4); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'AIM', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:AIM', + 'rawdescription' => + _m('The AIM plugin allows users to send and receive notices over the AIM network.')); + return true; + } +} + diff --git a/plugins/Aim/Fake_Aim.php b/plugins/Aim/Fake_Aim.php new file mode 100644 index 000000000..139b68f82 --- /dev/null +++ b/plugins/Aim/Fake_Aim.php @@ -0,0 +1,43 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Instead of sending AIM messages, retrieve the raw data that would be sent + * + * 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 Network + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @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/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class Fake_Aim extends Aim +{ + public $would_be_sent = null; + + function sflapSend($sflap_type, $sflap_data, $no_null, $formatted) + { + $this->would_be_sent = array($sflap_type, $sflap_data, $no_null, $formatted); + } +} + diff --git a/plugins/Aim/README b/plugins/Aim/README new file mode 100644 index 000000000..7d486a036 --- /dev/null +++ b/plugins/Aim/README @@ -0,0 +1,27 @@ +The AIM plugin allows users to send and receive notices over the AIM network. + +Installation +============ +add "addPlugin('aim', + array('setting'=>'value', 'setting2'=>'value2', ...);" +to the bottom of your config.php + +scripts/imdaemon.php included with StatusNet must be running. It will be started by +the plugin along with their other daemons when you run scripts/startdaemons.sh. +See the StatusNet README for more about queuing and daemons. + +Settings +======== +user*: username (screenname) to use when logging into AIM +password*: password for that user + +* required +default values are in (parenthesis) + +Example +======= +addPlugin('aim', array( + 'user=>'...', + 'password'=>'...' +)); + diff --git a/plugins/Aim/aimmanager.php b/plugins/Aim/aimmanager.php new file mode 100644 index 000000000..8ff7ab7e7 --- /dev/null +++ b/plugins/Aim/aimmanager.php @@ -0,0 +1,100 @@ +<?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); } + +/** + * AIM background connection manager for AIM-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 handled. + * + * 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 AimManager extends ImManager +{ + + public $conn = null; + /** + * Initialize connection to server. + * @return boolean true on success + */ + public function start($master) + { + if(parent::start($master)) + { + $this->connect(); + return true; + }else{ + return false; + } + } + + public function getSockets() + { + $this->connect(); + if($this->conn){ + return array($this->conn->myConnection); + }else{ + return array(); + } + } + + /** + * Process AIM events that have come in over the wire. + * @param resource $socket + */ + public function handleInput($socket) + { + common_log(LOG_DEBUG, "Servicing the AIM queue."); + $this->stats('aim_process'); + $this->conn->receive(); + } + + function connect() + { + if (!$this->conn) { + $this->conn=new Aim($this->plugin->user,$this->plugin->password,4); + $this->conn->registerHandler("IMIn",array($this,"handle_aim_message")); + $this->conn->myServer="toc.oscar.aol.com"; + $this->conn->signon(); + $this->conn->setProfile(_m('Send me a message to post a notice'),false); + } + return $this->conn; + } + + function handle_aim_message($data) + { + $this->plugin->enqueueIncomingRaw($data); + return true; + } + + function send_raw_message($data) + { + $this->connect(); + if (!$this->conn) { + return false; + } + $this->conn->sflapSend($data[0],$data[1],$data[2],$data[3]); + return true; + } +} diff --git a/plugins/Aim/extlib/phptoclib/README.txt b/plugins/Aim/extlib/phptoclib/README.txt new file mode 100755 index 000000000..0eec13af8 --- /dev/null +++ b/plugins/Aim/extlib/phptoclib/README.txt @@ -0,0 +1,169 @@ +phpTOCLib version 1.0 RC1
+
+This is released under the LGPL. AIM,TOC,OSCAR, and all other related protocols/terms are
+copyright AOL/Time Warner. This project is in no way affiliated with them, nor is this
+project supported by them.
+
+Some of the code is loosely based off of a script by Jeffrey Grafton. Mainly the decoding of packets, and the
+function for roasting passwords is entirly his.
+
+TOC documentation used is available at http://simpleaim.sourceforge.net/docs/TOC.txt
+
+
+About:
+phpTOCLib aims to be a PHP equivalent to the PERL module NET::AIM. Due to some limitations,
+this is difficult. Many features have been excluded in the name of simplicity, and leaves
+you alot of room to code with externally, providing function access to the variables that
+need them.
+
+I have aimed to make this extensible, and easy to use, therefore taking away some built in
+functionality that I had originally out in. This project comes after several months of
+researching the TOC protocol.
+
+example.php is included with the class. It needs to be executed from the command line
+(ie:php -q testscript.php) and you need to call php.exe with the -q
+example is provided as a demonstaration only. Though it creats a very simple, functional bot, it lacks any sort of commands, it merely resends the message it recieves in reverse.
+
+
+Revisions:
+
+-----------------------------------
+by Rajiv Makhijani
+(02/24/04)
+ - Fixed Bug in Setting Permit/Deny Mode
+ - Fixes so Uninitialized string offset notice doesn't appear
+ - Replaced New Lines Outputed for Each Flap Read with " . " so
+ that you can still tell it is active but it does not take so much space
+ - Removed "eh?" message
+ - Added MySQL Database Connection Message
+ - New Functions:
+ update_profile(profile data string, powered by boolean)
+ * The profile data string is the text that goes in the profile.
+ * The powered by boolean if set to true displays a link to the
+ sourceforge page of the script.
+(02/28/04)
+ - Silent option added to set object not to output any information
+ - To follow silent rule use sEcho function instead of Echo
+-----------------------------------
+by Jeremy (pickleman78)
+(05/26/04) beta 1 release
+ -Complete overhaul of class design and message handling
+ -Fixed bug involving sign off after long periods of idling
+ -Added new function $Aim->registerHandler
+ -Added the capability to handle all AIM messages
+ -Processing the messages is still the users responsibility
+ -Did a little bit of code cleanup
+ -Added a few internal functions to make the classes internal life easier
+ -Improved AIM server error message processing
+ -Updated this document (hopefully Rajiv will clean it up some, since I'm a terrible documenter)
+-------------------------------------------------------------------------------------------------------------
+
+
+
+Functions:
+
+Several methods are provided in the class that allow for simple access to some of the
+common features of AIM. Below are details.
+
+$Aim->Aim($sn,$password,$pdmode, $silent=false)
+The constructor, it takes 4 arguments.
+$sn is your screen name
+$password is you password, in plain text
+$pdmode is the permit deny mode. This can be as follows:
+1 - Allow All
+2 - Deny All
+3 - Permit only those on your permit list
+4 - Permit all those not on your deny list
+$silent if set to true prints out nothing
+
+So, if your screen-name is JohnDoe746 and your password is fertu, and you want to allow
+all users of the AIM server to contact you, you would code as follows
+$myaim=new Aim("JohnDoe746","fertu",1);
+
+
+$Aim->add_permit($buddy)
+This adds the buddy passed to the function to your permit list.
+ie: $myaim->add_permit("My friend22");
+
+$Aim->block_buddy($buddy)
+Blocks a user. This will switch your pd mode to 4. After using this, for the user to remain
+out of contact with you, it is required to provide the constructor with a pd mode of 4
+ie:$myaim->block_buddy("Annoying guy 4");
+
+$Aim->send_im($to,$message,$auto=false)
+Sends $message to $user. If you set the 3rd argument to true, then the recipient will receive it in
+the same format as an away message. (Auto Response from me:)
+A message longer than 65535 will be truncated
+ie:$myaim->send_im("myfriend","This is a happy message");
+
+$Aim->set_my_info()
+Sends an update buddy command to the server and allows some internal values about yourself
+to be set.
+ie:$myaim->set_my_info();
+
+$Aim->signon()
+Call this to connect to the server. This must be called before any other methods will work
+properly
+ie:$mybot->signon();
+
+$Aim->getLastReceived()
+Returns $this->myLastReceived['decoded']. This should be the only peice of the gotten data
+you need to concern yourself with. This is a preferred method of accessing this variable to prevent
+accidental modification of $this->myLastReceived. Accidently modifying this variable can
+cause some internal failures.
+
+$Aim->read_from_aim()
+This is a wrapper for $Aim->sflap_read(), and only returns the $this->myLastReceived['data']
+portion of the message. It is preferred that you do not call $Aim->sflap_read() and use this
+function instead. This function has a return value. Calling this prevents the need to call
+$Aim->getLastReceived()
+
+$Aim->setWarning($wl)
+This allows you to update the bots warning level when warned.
+
+$Aim->getBuddies()
+Returns the $this->myBuddyList array. Use this instead of modifying the internal variable
+
+$Aim->getPermit()
+Returns the $this->myPermitList array. Use this instead of modifying the internal variable
+
+$Aim->getBlocked()
+Returns the $this->myBlockedList array. Use this instead of modifying the internal variable
+
+$Aim->warn_user($user,$anon=false)
+Warn $user. If anon is set to true, then it warns the user anonomously
+
+$Aim->update_profile($information, $poweredby=false)
+Updates Profile to $information. If $poweredby is true a link to
+sourceforge page for this script is appended to profile
+
+$Aim->registerHandler($function_name,$command)
+This is by far the best thing about the new release.
+For more information please reas supplement.txt. It is not included here because of the sheer size of the document.
+supplement.txt contains full details on using registerHandler and what to expect for each input.
+
+
+For convenience, I have provided some functions to simplify message processing.
+
+They can be read about in the file "supplement.txt". I chose not to include the text here because it
+is a huge document
+
+
+
+There are a few things you should note about AIM
+1)An incoming message has HTML tags in it. You are responsible for stripping those tags
+2)Outgoing messages can have HTML tags, but will work fine if they don't. To include things
+ in the time feild next to the users name, send it as a comment
+
+Conclusion:
+The class is released under the LGPL. If you have any bug reports, comments, questions
+feature requests, or want to help/show me what you've created with this(I am very interested in this),
+please drop me an email: pickleman78@users.sourceforge.net. This code was written by
+Jeremy(a.k.a pickleman78) and Rajiv M (a.k.a compwiz562).
+
+
+Special thanks:
+I'd like to thank all of the people who have contributed ideas, testing, bug reports, and code additions to
+this project. I'd like to especially thank Rajiv, who has done do much for the project, and has kept this documnet
+looking nice. He also has done alot of testing of this script too. I'd also like to thank SpazLink for his help in
+testing. And finally I'd like to thank Jeffery Grafton, whose script inspired me to start this project.
diff --git a/plugins/Aim/extlib/phptoclib/aimclassw.php b/plugins/Aim/extlib/phptoclib/aimclassw.php new file mode 100755 index 000000000..0657910d9 --- /dev/null +++ b/plugins/Aim/extlib/phptoclib/aimclassw.php @@ -0,0 +1,2370 @@ +<?php +/* +* PHPTOCLIB: A library for AIM connectivity through PHP using the TOC protocal. +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +* +*/ +/** +* The version of PHPTOCLIB we are running right now +* +* @access private +* @var int +*/ +define("PHPTOCLIB_VERSION","1.0.0 RC1"); + +// Prevents Script from Timing Out +//set_time_limit(0); + +// Constant Declarations + +/** +* Maximum size for a direct connection IM in bytes +* +* @access private +* @var int +*/ + +define("MAX_DIM_SIZE",3072); //Default to 3kb + +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_WARN",74); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_MSG",75); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_UPDATEBUDDY",76); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_SIGNON",77); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_NICK",78); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_ERROR",79); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_CHATJ",80); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_CHATI",81); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_CHATUPDBUD",82); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_CHATINV",83); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_CHATLE",84); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_URL",85); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_NICKSTAT",86); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_PASSSTAT",87); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_RVOUSP",88); +/** +* Internally used for message type +* +* @access private +* @var int +*/ +define("AIM_TYPE_NOT_IMPLEMENTED",666); + + + +/** +* Internally used for connection type +* +* Internal type for a normal connection +* +* @access private +* @var int +*/ +define("CONN_TYPE_NORMAL",1); +/** +* Internally used for connection type +* +* Internal type of a Dirct Connection +* +* @access private +* @var int +*/ +define("CONN_TYPE_DC",2); +/** +* Internally used for connection type +* +*Internal type for a file transfer connection +* +* @access private +* @var int +*/ +define("CONN_TYPE_FT",3); +/** +* Internally used for connection type +* +*Internal type for a file get connection +* +* @access private +* @var int +*/ +define("CONN_TYPE_FTG",4); + +/** +* Maximum size for a TOC packet +* +* @access private +* @var int +*/ +define("MAX_PACKLENGTH",2048); + +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_TYPE_SIGNON",1); +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_TYPE_DATA",2); +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_TYPE_ERROR",3); +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_TYPE_SIGNOFF",4); +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_TYPE_KEEPALIVE",5); +/** +* TOC packet type +* +* @access private +* @var int +*/ +define("SFLAP_MAX_LENGTH",1024); + + + +/** +* Service UID for a voice connection +* +* @access private +* @var int +*/ +define('VOICE_UID', '09461341-4C7F-11D1-8222-444553540000'); +/** +* Service UID for file sending +* +* @access private +* @var int +*/ +define('FILE_SEND_UID', '09461343-4C7F-11D1-8222-444553540000'); +/** +* Service UID for file getting +* +* @access private +* @var int +*/ +define('FILE_GET_UID', '09461348-4C7F-11D1-8222-444553540000'); +/** +* Service UID for Direct connections +* +* @access private +* @var int +*/ +define('IMAGE_UID', '09461345-4C7F-11D1-8222-444553540000'); +/** +* Service UID for Buddy Icons +* +* @access private +* @var int +*/ +define('BUDDY_ICON_UID', '09461346-4C7F-11D1-8222-444553540000'); +/** +* Service UID for stocks +* +* @access private +* @var int +*/ +define('STOCKS_UID', '09461347-4C7F-11D1-8222-444553540000'); +/** +* Service UID for games +* +* @access private +* @var int +*/ +define('GAMES_UID', '0946134a-4C7F-11D1-8222-444553540000'); + +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_SUCCESS",0); +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_ERR_UNKNOWN",1); +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_ERR_ARGS",2); +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_ERR_LENGTH",3); +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_ERR_READ",4); +/** +* FLAP return code +* +* @access private +* @var int +*/ +define("SFLAP_ERR_SEND",5); + +/** +* FLAP version number +* +* @access private +* @var int +*/ +define("SFLAP_FLAP_VERSION",1); +/** +* FLAP TLV code +* +* @access private +* @var int +*/ +define("SFLAP_TLV_TAG",1); +/** +* Bytes in a FLAP header +* +* @access private +* @var int +*/ +define("SFLAP_HEADER_LEN",6); + +/** + * PHPTocLib AIM Class + * + * @author Jeremy Bryant <pickleman78@users.sourceforge.net> + * @author Rajiv Makhijani <rajiv@blue-tech.org> + * @package phptoclib + * @version 1.0RC1 + * @copyright 2005 + * @access public + * + */ +class Aim +{ + /** + * AIM ScreenName + * + * @var String + * @access private + */ + var $myScreenName; + + /** + * AIM Password (Plain Text) + * + * @var String + * @access private + */ + var $myPassword; + + + /** + * AIM TOC Server + * + * @var String + * @access public + */ + var $myServer="toc.oscar.aol.com"; + + /** + * AIM Formatted ScreenName + * + * @var String + * @access private + */ + var $myFormatSN; + + /** + * AIM TOC Server Port + * + * @var String + * @access public + */ + var $myPort="5190"; + + /** + * Profile Data + * Use setProfile() to update + * + * @var String + * @access private + */ + var $myProfile="Powered by phpTOCLib. Please visit http://sourceforge.net/projects/phptoclib for more information"; //The profile of the bot + + /** + * Socket Connection Resource ID + * + * @var Resource + * @access private + */ + var $myConnection; //Connection resource ID + + /** + * Roasted AIM Password + * + * @var String + * @access private + */ + var $myRoastedPass; + + /** + * Last Message Recieved From Server + * + * @var String + * @access private + */ + var $myLastReceived; + + /** + * Current Seq Number Used to Communicate with Server + * + * @var Integer + * @access private + */ + var $mySeqNum; + + /** + * Current Warning Level + * Getter: getWarning() + * Setter: setWarning() + * + * @var Integer + * @access private + */ + var $myWarnLevel; //Warning Level of the bot + + /** + * Auth Code + * + * @var Integer + * @access private + */ + var $myAuthCode; + + /** + * Buddies + * Getter: getBuddies() + * + * @var Array + * @access private + */ + var $myBuddyList; + + /** + * Blocked Buddies + * Getter: getBlocked() + * + * @var Array + * @access private + */ + var $myBlockedList; + + /** + * Permited Buddies + * Getter: getBlocked() + * + * @var Array + * @access private + */ + var $myPermitList; + + /** + * Permit/Deny Mode + * 1 - Allow All + * 2 - Deny All + * 3 - Permit only those on your permit list + * 4 - Permit all those not on your deny list + * + * @var Integer + * @access private + */ + var $myPdMode; + + //Below variables added 4-29 by Jeremy: Implementing chat + + /** + * Contains Chat Room Info + * $myChatRooms['roomid'] = people in room + * + * @var Array + * @access private + */ + var $myChatRooms; + + //End of chat implementation + + + /** + * Event Handler Functions + * + * @var Array + * @access private + */ + var $myEventHandlers = array(); + + /** + * Array of direct connection objects(including file transfers) + * + * @var Array + * @access private + */ + var $myDirectConnections = array(); + + /** + * Array of the actual connections + * + * @var Array + * @access private + */ + var $myConnections = array(); + + /** + * The current state of logging + * + * @var Boolean + * @access private + */ + + var $myLogging = false; + + /** + * Constructor + * + * Permit/Deny Mode Options + * 1 - Allow All + * 2 - Deny All + * 3 - Permit only those on your permit list + * 4 - Permit all those not on your deny list + * + * @param String $sn AIM Screenname + * @param String $password AIM Password + * @param Integer $pdmode Permit/Deny Mode + * @access public + */ + function Aim($sn, $password, $pdmode) + { + //Constructor assignment + $this->myScreenName = $this->normalize($sn); + $this->myPassword = $password; + $this->myRoastedPass = $this->roastPass($password); + $this->mySeqNum = 1; + $this->myConnection = 0; + $this->myWarnLevel = 0; + $this->myAuthCode = $this->makeCode(); + $this->myPdMode = $pdmode; + $this->myFormatSN = $this->myScreenName; + + $this->log("PHPTOCLIB v" . PHPTOCLIB_VERSION . " Object Created"); + + } + + /** + * Enables debug logging (Logging is disabled by default) + * + * + * @access public + * @return void + */ + + function setLogging($enable) + { + $this->myLogging=$enable; + } + + function log($data) + { + if($this->myLogging){ + error_log($data); + } + } + + /** + * Logs a packet + * + * + * @access private + * @param Array $packary Packet + * @param String $in Prepend + * @return void + */ + function logPacket($packary,$in) + { + if(!$this->myLogging || sizeof($packary)<=0 || (@strlen($packary['decoded'])<=0 && @isset($packary['decoded']))) + return; + $towrite=$in . ": "; + foreach($packary as $k=>$d) + { + $towrite.=$k . ":" . $d . "\r\n"; + } + $towrite.="\r\n\r\n"; + $this->log($towrite); + } + /** + * Roasts/Hashes Password + * + * @param String $password Password + * @access private + * @return String Roasted Password + */ + function roastPass($password) + { + $roaststring = 'Tic/Toc'; + $roasted_password = '0x'; + for ($i = 0; $i < strlen($password); $i++) + $roasted_password .= bin2hex($password[$i] ^ $roaststring[($i % 7)]); + return $roasted_password; + } + + /** + * Access Method for myScreenName + * + * @access public + * @param $formated Returns formatted Screenname if true as returned by server + * @return String Screenname + */ + function getMyScreenName($formated = false) + { + if ($formated) + { + return $this->myFormatSN; + } + else + { + return $this->normalize($this->myScreenName); + } + } + + /** + * Generated Authorization Code + * + * @access private + * @return Integer Auth Code + */ + function makeCode() + { + $sn = ord($this->myScreenName[0]) - 96; + $pw = ord($this->myPassword[0]) - 96; + $a = $sn * 7696 + 738816; + $b = $sn * 746512; + $c = $pw * $a; + + return $c - $a + $b + 71665152; + } + + + /** + * Reads from Socket + * + * @access private + * @return String Data + */ + function sflapRead() + { + if ($this->socketcheck($this->myConnection)) + { + $this->log("Disconnected.... Reconnecting in 60 seconds"); + sleep(60); + $this->signon(); + } + + $header = fread($this->myConnection,SFLAP_HEADER_LEN); + + if (strlen($header) == 0) + { + $this->myLastReceived = ""; + return ""; + } + $header_data = unpack("aast/Ctype/nseq/ndlen", $header); + $this->log(" . ", false); + $packet = fread($this->myConnection, $header_data['dlen']); + if (strlen($packet) <= 0 && $sockinfo['blocked']) + $this->derror("Could not read data"); + + if ($header_data['type'] == SFLAP_TYPE_SIGNON) + { + $packet_data=unpack("Ndecoded", $packet); + } + + if ($header_data['type'] == SFLAP_TYPE_KEEPALIVE) + { + $this->myLastReceived = ''; + return 0; + } + else if (strlen($packet)>0) + { + $packet_data = unpack("a*decoded", $packet); + } + $this->log("socketcheck check now"); + if ($this->socketcheck($this->myConnection)) + { + $this->derror("Connection ended unexpectedly"); + } + + $data = array_merge($header_data, $packet_data); + $this->myLastReceived = $data; + $this->logPacket($data,"in"); + return $data; + } + + /** + * Sends Data on Socket + * + * @param String $sflap_type Type + * @param String $sflap_data Data + * @param boolean $no_null No Null + * @param boolean $formatted Format + * @access private + * @return String Roasted Password + */ + function sflapSend($sflap_type, $sflap_data, $no_null, $formatted) + { + $packet = ""; + if (strlen($sflap_data) >= MAX_PACKLENGTH) + $sflap_data = substr($sflap_data,0,MAX_PACKLENGTH); + + if ($formatted) + { + $len = strlen($sflap_len); + $sflap_header = pack("aCnn",'*', $sflap_type, $this->mySeqNum, $len); + $packet = $sflap_header . $sflap_data; + } else { + if (!$no_null) + { + $sflap_data = str_replace("\0","", trim($sflap_data)); + $sflap_data .= "\0"; + } + $data = pack("a*", $sflap_data); + $len = strlen($sflap_data); + $header = pack("aCnn","*", $sflap_type, $this->mySeqNum, $len); + $packet = $header . $data; + } + + //Make sure we are still connected + if ($this->socketcheck($this->myConnection)) + { + $this->log("Disconnected.... reconnecting in 60 seconds"); + sleep(60); + $this->signon(); + } + $sent = fputs($this->myConnection, $packet) or $this->derror("Error sending packet to AIM"); + $this->mySeqNum++; + sleep(ceil($this->myWarnLevel/10)); + $this->logPacket(array($sflap_type,$sflap_data),"out"); + } + + /** + * Escape the thing that TOC doesn't like,that would be + * ",', $,{,},[,] + * + * @param String $data Data to Escape + * @see decodeData + * @access private + * @return String $data Escaped Data + */ + function encodeData($data) + { + $data = str_replace('"','\"', $data); + $data = str_replace('$','\$', $data); + $data = str_replace("'","\'", $data); + $data = str_replace('{','\{', $data); + $data = str_replace('}','\}', $data); + $data = str_replace('[','\[', $data); + $data = str_replace(']','\]', $data); + return $data; + } + + /** + * Unescape data TOC has escaped + * ",', $,{,},[,] + * + * @param String $data Data to Unescape + * @see encodeData + * @access private + * @return String $data Unescape Data + */ + function decodeData($data) + { + $data = str_replace('\"','"', $data); + $data = str_replace('\$','$', $data); + $data = str_replace("\'","'", $data); + $data = str_replace('\{','{', $data); + $data = str_replace('\}','}', $data); + $data = str_replace('\[','[', $data); + $data = str_replace('\]',']', $data); + $data = str_replace('"','"', $data); + $data = str_replace('&','&', $data); + return $data; + } + + /** + * Normalize ScreenName + * no spaces and all lowercase + * + * @param String $nick ScreenName + * @access public + * @return String $nick Normalized ScreenName + */ + function normalize($nick) + { + $nick = str_replace(" ","", $nick); + $nick = strtolower($nick); + return $nick; + } + + /** + * Sets internal info with update buddy + * Currently only sets warning level + * + * @access public + * @return void + */ + function setMyInfo() + { + //Sets internal values bvase on the update buddy command + $this->log("Setting my warning level ..."); + $this->sflapSend(SFLAP_TYPE_DATA,"toc_get_status " . $this->normalize($this->myScreenName),0,0); + //The rest of this will now be handled by the other functions. It is assumed + //that we may have other data queued in the socket, do we should just add this + //message to the queue instead of trying to set it in here + } + + /** + * Connects to AIM and Signs On Using Info Provided in Constructor + * + * @access public + * @return void + */ + function signon() + { + $this->log("Ready to sign on to the server"); + $this->myConnection = fsockopen($this->myServer, $this->myPort, $errno, $errstr,10) or die("$errorno:$errstr"); + $this->log("Connected to server"); + $this->mySeqNum = (time() % 65536); //Select an arbitrary starting point for + //sequence numbers + if (!$this->myConnection) + $this->derror("Error connecting to toc.oscar.aol.com"); + $this->log("Connected to AOL"); + //Send the flapon packet + fputs($this->myConnection,"FLAPON\r\n\n\0"); //send the initial handshake + $this->log("Sent flapon"); + $this->sflapRead(); //Make sure the server responds with what we expect + if (!$this->myLastReceived) + $this->derror("Error sending the initialization string"); + + //send the FLAP SIGNON packet back with what it needs + //There are 2 parts to the signon packet. They are sent in succession, there + //is no indication if either packet was correctly sent + $signon_packet = pack("Nnna".strlen($this->myScreenName),1,1,strlen($this->myScreenName), $this->myScreenName); + $this->sflapSend(SFLAP_TYPE_SIGNON, $signon_packet,1,0); + $this->log("sent signon packet part one"); + + $signon_packet_part2 = 'toc2_signon login.oscar.aol.com 29999 ' . $this->myScreenName . ' ' . $this->myRoastedPass . ' english-US "TIC:TOC2:REVISION" 160 ' . $this->myAuthCode; + $this->log($signon_packet_part2 . ""); + $this->sflapSend(SFLAP_TYPE_DATA, $signon_packet_part2,0,0); + $this->log("Sent signon packet part 2... Awaiting response..."); + + $this->sflapRead(); + $this->log("Received Sign on packet, beginning initilization..."); + $message = $this->getLastReceived(); + $this->log($message . "\n"); + if (strstr($message,"ERROR:")) + { + $this->onError($message); + die("Fatal signon error"); + } + stream_set_timeout($this->myConnection,2); + //The information sent before the config2 command is utterly useless to us + //So we will just skim through them until we reach it + + //Add the first entry to the connection array + $this->myConnections[] = $this->myConnection; + + + //UPDATED 4/12/03: Now this will use the receive function and send the + //received messaged to the assigned handlers. This is where the signon + //method has no more use + + $this->log("Done with signon proccess"); + //socket_set_blocking($this->myConnection,false); + } + + /** + * Sends Instant Message + * + * @param String $to Message Recipient SN + * @param String $message Message to Send + * @param boolean $auto Sent as Auto Response / Away Message Style + * @access public + * @return void + */ + function sendIM($to, $message, $auto = false) + { + if ($auto) $auto = "auto"; + else $auto = ""; + $to = $this->normalize($to); + $message = $this->encodeData($message); + $command = 'toc2_send_im "' . $to . '" "' . $message . '" ' . $auto; + $this->sflapSend(SFLAP_TYPE_DATA, trim($command),0,0); + $cleanedmessage = str_replace("<br>", " ", $this->decodeData($message)); + $cleanedmessage = strip_tags($cleanedmessage); + $this->log("TO - " . $to . " : " . $cleanedmessage); + } + + /** + * Set Away Message + * + * @param String $message Away message (some HTML supported). + * Use null to remove the away message + * @access public + * @return void + */ + function setAway($message) + { + $message = $this->encodeData($message); + $command = 'toc_set_away "' . $message . '"'; + $this->sflapSend(SFLAP_TYPE_DATA, trim($command),0,0); + $this->log("SET AWAY MESSAGE - " . $this->decodeData($message)); + } + + /** + * Fills Buddy List + * Not implemented fully yet + * + * @access public + * @return void + */ + function setBuddyList() + { + //This better be the right message + $message = $this->myLastReceived['decoded']; + if (strpos($message,"CONFIG2:") === false) + { + $this->log("setBuddyList cannot be called at this time because I got $message"); + return false; + } + $people = explode("\n",trim($message,"\n")); + //The first 3 elements of the array are who knows what, element 3 should be + //a letter followed by a person + for($i = 1; $i<sizeof($people); $i++) + { + @list($mode, $name) = explode(":", $people[$i]); + switch($mode) + { + case 'p': + $this->myPermitList[] = $name; + break; + case 'd': + $this->myBlockedList[] = $name; + break; + case 'b': + $this->myBuddyList[] = $name; + break; + case 'done': + break; + default: + // + } + } + } + + /** + * Adds buddy to Permit list + * + * @param String $buddy Buddy's Screenname + * @access public + * @return void + */ + function addPermit($buddy) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc2_add_permit " . $this->normalize($buddy),0,0); + $this->myPermitList[] = $this->normalize($buddy); + return 1; + } + + /** + * Blocks buddy + * + * @param String $buddy Buddy's Screenname + * @access public + * @return void + */ + function blockBuddy($buddy) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc2_add_deny " . $this->normalize($buddy),0,0); + $this->myBlockedList[] = $this->normalize($buddy); + return 1; + } + + /** + * Returns last message received from server + * + * @access private + * @return String Last Message from Server + */ + function getLastReceived() + { + if (@$instuff = $this->myLastReceived['decoded']){ + return $this->myLastReceived['decoded']; + }else{ + return; + } + } + + /** + * Returns Buddy List + * + * @access public + * @return array Buddy List + */ + function getBuddies() + { + return $this->myBuddyList; + } + + /** + * Returns Permit List + * + * @access public + * @return array Permit List + */ + function getPermit() + { + return $this->myPermitList; + } + + /** + * Returns Blocked Buddies + * + * @access public + * @return array Blocked List + */ + function getBlocked() + { + return $this->myBlockedList; + } + + + + + /** + * Reads and returns data from server + * + * This is a wrapper for $Aim->sflap_read(), and only returns the $this->myLastReceived['data'] + * portion of the message. It is preferred that you do not call $Aim->sflap_read() and use this + * function instead. This function has a return value. Calling this prevents the need to call + * $Aim->getLastReceived() + * + * @access public + * @return String Data recieved from server + */ + function read_from_aim() + { + $this->sflapRead(); + $returnme = $this->getLastReceived(); + return $returnme; + } + + /** + * Sets current internal warning level + * + * This allows you to update the bots warning level when warned. + * + * @param int Warning Level % + * @access private + * @return void + */ + function setWarningLevel($warnlevel) + { + $this->myWarnLevel = $warnlevel; + } + + /** + * Warns / "Evils" a User + * + * To successfully warn another user they must have sent you a message. + * There is a limit on how much and how often you can warn another user. + * Normally when you warn another user they are aware who warned them, + * however there is the option to warn anonymously. When warning anon. + * note that the warning is less severe. + * + * @param String $to Screenname to warn + * @param boolean $anon Warn's anonymously if true. (default = false) + * @access public + * @return void + */ + function warnUser($to, $anon = false) + { + if (!$anon) + $anon = '"norm"'; + + else + $anon = '"anon"'; + $this->sflapSend(SFLAP_TYPE_DATA,"toc_evil " . $this->normalize($to) . " $anon",0,0); + } + + /** + * Returns warning level of bot + * + * @access public + * @return void + */ + function getWarningLevel() + { + return $this->myWarningLevel; + } + + /** + * Sets bot's profile/info + * + * Limited to 1024 bytes. + * + * @param String $profiledata Profile Data (Can contain limited html: br,hr,font,b,i,u etc) + * @param boolean $poweredby If true, appends link to phpTOCLib project to profile + * @access public + * @return void + */ + function setProfile($profiledata, $poweredby = false) + { + if ($poweredby == false){ + $this->myProfile = $profiledata; + }else{ + $this->myProfile = $profiledata . "<font size=1 face=tahoma><br><br>Powered by phpTOCLib<br>http://sourceforge.net/projects/phptoclib</font>"; + } + + $this->sflapSend(SFLAP_TYPE_DATA,"toc_set_info \"" . $this->encodeData($this->myProfile) . "\"",0,0); + $this->setMyInfo(); + $this->log("Profile has been updated..."); + } + + //6/29/04 by Jeremy: + //Added mthod to accept a rvous,decline it, and + //read from the rvous socket + + //Decline + + /** + * Declines a direct connection request (rvous) + * + * @param String $nick ScreenName request was from + * @param String $cookie Request cookie (from server) + * @param String $uuid UUID + * + * @access public + * @return void + */ + function declineRvous($nick, $cookie, $uuid) + { + $nick = $this->normalize($nick); + $this->sflapSend(SFLAP_TYPE_DATA,"toc_rvous_cancel $nick $cookie $uuid",0,0); + } + + /** + * Accepts a direct connection request (rvous) + * + * @param String $nick ScreenName request was from + * @param String $cookie Request cookie (from server) + * @param String $uuid UUID + * @param String $vip IP of User DC with + * @param int $port Port number to connect to + * + * @access public + * @return void + */ + function acceptRvous($nick, $cookie, $uuid, $vip, $port) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc_rvous_accept $nick $cookie $uuid",0,0); + + //Now open the connection to that user + if ($uuid == IMAGE_UID) + { + $dcon = new Dconnect($vip, $port); + } + else if ($uuid == FILE_SEND_UID) + { + $dcon = new FileSendConnect($vip, $port); + } + if (!$dcon->connected) + { + $this->log("The connection failed"); + return false; + } + + //Place this dcon object inside the array + $this->myDirectConnections[] = $dcon; + //Place the socket in an array to + $this->myConnections[] = $dcon->sock; + + + //Get rid of the first packet because its worthless + //and confusing + $dcon->readDIM(); + //Assign the cookie + $dcon->cookie = $dcon->lastReceived['cookie']; + $dcon->connectedTo = $this->normalize($nick); + return $dcon; + } + + /** + * Sends a Message over a Direct Connection + * + * Only works if a direct connection is already established with user + * + * @param String $to Message Recipient SN + * @param String $message Message to Send + * + * @access public + * @return void + */ + function sendDim($to, $message) + { + //Find the connection + for($i = 0;$i<sizeof($this->myDirectConnections);$i++) + { + if ($this->normalize($to) == $this->myDirectConnections[$i]->connectedTo && $this->myDirectConnections[$i]->type == CONN_TYPE_DC) + { + $dcon = $this->myDirectConnections[$i]; + break; + } + } + if (!$dcon) + { + $this->log("Could not find a direct connection to $to"); + return false; + } + $dcon->sendMessage($message, $this->normalize($this->myScreenName)); + return true; + } + + /** + * Closes an established Direct Connection + * + * @param DConnect $dcon Direct Connection Object to Close + * + * @access public + * @return void + */ + function closeDcon($dcon) + { + + $nary = array(); + for($i = 0;$i<sizeof($this->myConnections);$i++) + { + if ($dcon->sock == $this->myConnections[$i]) + unset($this->myConnections[$i]); + } + + $this->myConnections = array_values($this->myConnections); + unset($nary); + $nary2 = array(); + + for($i = 0;$i<sizeof($this->myDirectConnections);$i++) + { + if ($dcon == $this->myDirectConnections[$i]) + unset($this->myDirectConnections[$i]); + } + $this->myDirectConnections = array_values($this->myDirectConnections); + $dcon->close(); + unset($dcon); + } + + //Added 4/29/04 by Jeremy: + //Various chat related methods + + /** + * Accepts a Chat Room Invitation (Joins room) + * + * @param String $chatid ID of Chat Room + * + * @access public + * @return void + */ + function joinChat($chatid) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc_chat_accept " . $chatid,0,0); + } + + /** + * Leaves a chat room + * + * @param String $chatid ID of Chat Room + * + * @access public + * @return void + */ + function leaveChat($chatid) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc_chat_leave " . $chatid,0,0); + } + + /** + * Sends a message in a chat room + * + * @param String $chatid ID of Chat Room + * @param String $message Message to send + * + * @access public + * @return void + */ + function chatSay($chatid, $message) + { + $this->sflapSend(SFLAP_TYPE_DATA,"toc_chat_send " . $chatid . " \"" . $this->encodeData($message) . "\"",0,0); + } + + /** + * Invites a user to a chat room + * + * @param String $chatid ID of Chat Room + * @param String $who Screenname of user + * @param String $message Note to include with invitiation + * + * @access public + * @return void + */ + function chatInvite($chatid, $who, $message) + { + $who = $this->normalize($who); + $this->sflapSend(SFLAP_TYPE_DATA,"toc_chat_invite " . $chatid . " \"" . $this->encodeData($message) . "\" " . $who,0,0); + } + + /** + * Joins/Creates a new chat room + * + * @param String $name Name of the new chat room + * @param String $exchange Exchange of new chat room + * + * @access public + * @return void + */ + function joinNewChat($name, $exchange) + { + //Creates a new chat + $this->sflapSend(SFLAP_TYPE_DATA,"toc_chat_join " . $exchange . " \"" . $name . "\"",0,0); + } + + /** + * Disconnect error handler, attempts to reconnect in 60 seconds + * + * @param String $message Error message (desc of where error encountered etc) + * + * @access private + * @return void + */ + function derror($message) + { + $this->log($message); + $this->log("Error"); + fclose($this->myConnection); + if ((time() - $GLOBALS['errortime']) < 600){ + $this->log("Reconnecting in 60 Seconds"); + sleep(60); + } + $this->signon(); + $GLOBALS['errortime'] = time(); + } + + /** + * Returns connection type of socket (main or rvous etc) + * + * Helper method for recieve() + * + * @param Resource $sock Socket to determine type for + * + * @access private + * @return void + * @see receive + */ + function connectionType($sock) + { + //Is it the main connection? + if ($sock == $this->myConnection) + return CONN_TYPE_NORMAL; + else + { + for($i = 0;$i<sizeof($this->myDirectConnections);$i++) + { + if ($sock == $this->myDirectConnections[$i]->sock) + return $this->myDirectConnections[$i]->type; + } + } + return false; + } + + /** + * Checks for new data and calls appropriate methods + * + * This method is usually called in an infinite loop to keep checking for new data + * + * @access public + * @return void + * @see connectionType + */ + function receive() + { + //This function will be used to get the incoming data + //and it will be used to call the event handlers + + //First, get an array of sockets that have data that is ready to be read + $ready = array(); + $ready = $this->myConnections; + $numrdy = stream_select($ready, $w = NULL, $x = NULL,NULL); + + //Now that we've waited for something, go through the $ready + //array and read appropriately + + for($i = 0;$i<sizeof($ready);$i++) + { + //Get the type + $type = $this->connectionType($ready[$i]); + if ($type == CONN_TYPE_NORMAL) + { + //Next step:Get the data sitting in the socket + $message = $this->read_from_aim(); + if (strlen($message) <= 0) + { + return; + } + + //Third step: Get the command from the server + @list($cmd, $rest) = explode(":", $message); + + //Fourth step, take the command, test the type, and pass it off + //to the correct internal handler. The internal handler will + //do what needs to be done on the class internals to allow + //it to work, then proceed to pass it off to the user created handle + //if there is one + $this->log($cmd); + switch($cmd) + { + case 'SIGN_ON': + $this->onSignOn($message); + break; + case 'CONFIG2': + $this->onConfig($message); + break; + case 'ERROR': + $this->onError($message); + break; + case 'NICK': + $this->onNick($message); + break; + case 'IM_IN2': + $this->onImIn($message); + break; + case 'UPDATE_BUDDY2': + $this->onUpdateBuddy($message); + break; + case 'EVILED': + $this->onWarn($message); + break; + case 'CHAT_JOIN': + $this->onChatJoin($message); + break; + case 'CHAT_IN': + $this->onChatIn($message); + break; + case 'CHAT_UPDATE_BUDDY': + $this->onChatUpdate($message); + break; + case 'CHAT_INVITE': + $this->onChatInvite($message); + break; + case 'CHAT_LEFT': + $this->onChatLeft($message); + break; + case 'GOTO_URL': + $this->onGotoURL($message); + break; + case 'DIR_STATUS': + $this->onDirStatus($message); + break; + case 'ADMIN_NICK_STATUS': + $this->onAdminNick($message); + break; + case 'ADMIN_PASSWD_STATUS': + $this->onAdminPasswd($message); + break; + case 'PAUSE': + $this->onPause($message); + break; + case 'RVOUS_PROPOSE': + $this->onRvous($message); + break; + default: + $this->log("Fell through: $message"); + $this->CatchAll($message); + break; + } + } + else + { + for($j = 0;$j<sizeof($this->myDirectConnections);$j++) + { + if ($this->myDirectConnections[$j]->sock == $ready[$i]) + { + $dcon = $this->myDirectConnections[$j]; + break; + } + } + //Now read from the dcon + if ($dcon->type == CONN_TYPE_DC) + { + if ($dcon->readDIM() == false) + { + $this->closeDcon($dcon); + continue; + } + + $message['message'] = $dcon->lastMessage; + if ($message['message'] == "too big") + { + $this->sendDim("Connection dropped because you sent a message larger that " . MAX_DCON_SIZE . " bytes.", $dcon->connectedTo); + $this->closeDcon($dcon); + continue; + } + $message['from'] = $dcon->connectedTo; + $this->onDimIn($message); + } + } + } + $this->conn->myLastReceived=""; + //Now get out of this function because the handlers should take care + //of everything + } + + //The next block of code is all the event handlers needed by the class + //Some are left blank and only call the users handler because the class + //either does not support the command, or cannot do anything with it + // --------------------------------------------------------------------- + + /** + * Direct IM In Event Handler + * + * Called when Direct IM is received. + * Call's user handler (if available) for DimIn. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onDimIn($data) + { + $this->callHandler("DimIn", $data); + } + + /** + * Sign On Event Handler + * + * Called when Sign On event occurs. + * Call's user handler (if available) for SIGN_ON. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onSignOn($data) + { + $this->callHandler("SignOn", $data); + } + + /** + * Config Event Handler + * + * Called when Config data received. + * Call's user handler (if available) for Config. + * + * Loads buddy list and other info + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onConfig($data) + { + $this->log("onConfig Message: " . $data); + + if (strpos($data,"CONFIG2:") === false) + { + $this->log("get_buddy_list cannot be called at this time because I got $data"); + //return false; + } + $people = explode("\n",trim($data,"\n")); + //The first 3 elements of the array are who knows what, element 3 should be + //a letter followed by a person + + //AIM decided to add this wonderful new feature, the recent buddy thing, this kind of + //messes this funtion up, so we need to adapt it... unfortuneately, its not really + //clear how this works, so we are just going to add their name to the permit list. + + //Recent buddies I believe are in the format + //number:name:number.... I think the first number counts down from 25 how long its + //been... but I don't know the second number,,,, + + //TODO: Figure out the new recent buddies system + + //Note: adding that at the bottom is a quick hack and may have adverse consequences... + for($i = 1;$i<sizeof($people);$i++) + { + @list($mode, $name) = explode(":", $people[$i]); + switch($mode) + { + case 'p': + $this->myPermitList[] = $name; + break; + case 'd': + $this->myBlockedList[] = $name; + break; + case 'b': + $this->myBuddyList[] = $name; + break; + case 'done': + break; + default: + //This is assumed to be recent buddies... + $this->myPermitList[]=$name; + } + } + + //We only get the config message once, so now we should send our pd mode + + $this->sflapSend(SFLAP_TYPE_DATA,"toc2_set_pdmode " . $this->myPdMode,0,0); + //Adds yourself to the permit list + //This is to fix an odd behavior if you have nobody on your list + //the server won't send the config command... so this takes care of it + $this->sflapSend(SFLAP_TYPE_DATA,"toc2_add_permit " . $this->normalize($this->myScreenName),0,0); + + //Now we allow the user to send a list, update anything they want, etc + $this->callHandler("Config", $data); + //Now that we have taken care of what the user wants, send the init_done message + $this->sflapSend(SFLAP_TYPE_DATA,"toc_init_done",0,0); + //'VOICE_UID' + //'FILE_GET_UID' + //'IMAGE_UID' + //'BUDDY_ICON_UID' + //'STOCKS_UID' + //'GAMES_UID' + $this->sflapSend(SFLAP_TYPE_DATA, "toc_set_caps " . IMAGE_UID . " " . FILE_SEND_UID ." " . FILE_GET_UID . " " . BUDDY_ICON_UID . "",0,0); + } + + + /** + * Error Event Handler + * + * Called when an Error occurs. + * Call's user handler (if available) for Error. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onError($data) + { + static $errarg = ''; + static $ERRORS = array(
+ 0=>'Success',
+ 901 =>'$errarg not currently available',
+ 902 =>'Warning of $errarg not currently available',
+ 903 =>'A message has been dropped, you are exceeding
+ the server speed limit',
+ 911 =>'Error validating input',
+ 912 =>'Invalid account',
+ 913 =>'Error encountered while processing request',
+ 914 =>'Service unavailable',
+ 950 =>'Chat in $errarg is unavailable.',
+ 960 =>'You are sending message too fast to $errarg',
+ 961 =>'You missed an im from $errarg because it was too big.',
+ 962 =>'You missed an im from $errarg because it was sent too fast.',
+ 970 =>'Failure',
+ 971 =>'Too many matches',
+ 972 =>'Need more qualifiers',
+ 973 =>'Dir service temporarily unavailable',
+ 974 =>'Email lookup restricted',
+ 975 =>'Keyword Ignored',
+ 976 =>'No Keywords',
+ 977 =>'Language not supported',
+ 978 =>'Country not supported',
+ 979 =>'Failure unknown $errarg',
+ 980 =>'Incorrect nickname or password.',
+ 981 =>'The service is temporarily unavailable.',
+ 982 =>'Your warning level is currently too high to sign on.',
+ 983 =>'You have been connecting and
+ disconnecting too frequently. Wait 10 minutes and try again.
+ If you continue to try, you will need to wait even longer.',
+ 989 =>'An unknown signon error has occurred $errarg'
+ ); + $data_array = explode(":", $data); + for($i=0; $i<count($data_array); $i++) + { + switch($i) + { + case 0: + $cmd = $data_array[$i]; + break; + case 1: + $errornum = $data_array[$i]; + break; + case 2: + $errargs = $data_array[$i]; + break; + } + } + eval("\$errorstring=\"\$ERRORS[" . $errornum . "]\";"); + $string = "\$errorstring=\"\$ERRORS[$errornum]\";"; + //This is important information! We need + // a A different outputter for errors + // b Just to echo it + //I'm just going to do a straight echo here, becuse we assume that + //the user will NEED to see this error. An option to supress it will + //come later I think. Perhaps if we did an error reporting level, similar + //to PHP's, and we could probably even use PHP's error outputting system + //I think that may be an idea.... + + $this->log($errorstring . "\n"); + + $this->callHandler("Error", $data); + } + + /** + * Nick Event Handler + * + * Called when formatted own ScreenName is receieved + * Call's user handler (if available) for Nick. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onNick($data) + { + //This is our nick, so set a field called "myFormatSN" which will represent + //the actual name given by the server to us, NOT the normalized screen name + @list($cmd, $nick) = explode(":", $data); + $this->myFormatSN = $nick; + + $this->callHandler("Nick", $data); + } + + /** + * IM In Event Handler + * + * Called when an Instant Message is received. + * Call's user handler (if available) for IMIn. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onImIn($data) + { + //Perhaps we should add an internal log for debugging purposes?? + //But now, this should probably be handled by the user purely + + $this->callHandler("IMIn", $data); + } + + /** + * UpdateBuddy Event Handler + * + * Called when a Buddy Update is receieved. + * Call's user handler (if available) for UpdateBuddy. + * If info is about self, updates self info (Currently ownly warning). + * + * ToDo: Keep track of idle, warning etc on Buddy List + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onUpdateBuddy($data) + { + //Again, since our class currently does not deal with other people without + //outside help, then this is also probably best left to the user. Though + //we should probably allow this to replace the setMyInfo function above + //by handling the input if and only if it is us + //Check and see that this is the command expected + if (strpos($data,"UPDATE_BUDDY2:") == -1) + { + $this->log("A different message than expected was received"); + return false; + } + + //@list($cmd, $info['sn'], $info['online'], $info['warnlevel'], $info['signon'], $info['idle'], $info['uc']) = explode(":", $command['incoming']); + + //@list($cmd, $sn, $online, $warning, $starttime, $idletime, $uc) = explode(":", $data); + $info = $this->getMessageInfo($data); + if ($this->normalize($info['sn']) == $this->normalize($this->myScreenName)) + { + $warning = rtrim($info['warnlevel'],"%"); + $this->myWarnLevel = $warning; + $this->log("My warning level is $this->myWarnLevel %"); + } + + $this->callHandler("UpdateBuddy", $data); + } + + /** + * Warning Event Handler + * + * Called when bot is warned. + * Call's user handler (if available) for Warn. + * Updates internal warning level + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onWarn($data) + { + /* + For reference: + $command['incoming'] .= ":0"; + $it = explode(":", $command['incoming']); + $info['warnlevel'] = $it[1]; + $info['from'] = $it[2]; + */ + //SImply update our warning level + //@list($cmd, $newwarn, $user) = explode(":", $data); + + $info = $this->getMessageInfo($data); + + $this->setWarningLevel(trim($info['warnlevel'],"%")); + $this->log("My warning level is $this->myWarnLevel %"); + + $this->callHandler("Warned", $data); + } + + /** + * Chat Join Handler + * + * Called when bot joins a chat room. + * Call's user handler (if available) for ChatJoin. + * Adds chat room to internal chat room list. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onChatJoin($data) + { + @list($cmd, $rmid, $rmname) = explode(":", $data); + $this->myChatRooms[$rmid] = 0; + + $this->callHandler("ChatJoin", $data); + } + + /** + * Returns number of chat rooms bot is in + * + * @access public + * @param String $data Raw message from server + * @return int + */ + function getNumChats() + { + return count($this->myChatRooms); + } + + /** + * Chat Update Handler + * + * Called when bot received chat room data (user update). + * Call's user handler (if available) for ChatUpdate. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onChatUpdate($data) + { + $stuff = explode(":", $data); + $people = sizeof($stuff); + $people -= 2; + + $this->callHandler("ChatUpdate", $data); + } + + /** + * Chat Message In Handler + * + * Called when chat room message is received. + * Call's user handler (if available) for ChatIn. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onChatIn($data) + { + $this->callHandler("ChatIn", $data); + } + + + /** + * Chat Invite Handler + * + * Called when bot is invited to a chat room. + * Call's user handler (if available) for ChatInvite. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onChatInvite($data) + { + //@list($cmd, $name, $id, $from, $data) = explode(":", $data,6); + //$data = explode(":",$data,6); + //$nm = array(); + //@list($nm['cmd'],$nm['name'],$nm['id'],$nm['from'],$nm['message']) = $data; + + + $this->callHandler("ChatInvite", $data); + } + + /** + * Chat Left Handler + * + * Called when bot leaves a chat room + * Call's user handler (if available) for ChatLeft. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onChatLeft($data) + { + $info = $this->getMessageInfo($data); + unset($this->myChatRooms[$info['chatid']]); + $this->callHandler("ChatLeft", $data); + } + + /** + * Goto URL Handler + * + * Called on GotoURL. + * Call's user handler (if available) for GotoURL. + * No detailed info available for this / Unsupported. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onGotoURL($data) + { + //This is of no use to the internal class + + $this->callHandler("GotoURL", $data); + } + + /** + * Dir Status Handler + * + * Called on DirStatus. + * Call's user handler (if available) for DirStatus. + * No detailed info available for this / Unsupported. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onDirStatus($data) + { + //This is not currently suported + + $this->callHandler("DirStatus", $data); + } + + /** + * AdminNick Handler + * + * Called on AdminNick. + * Call's user handler (if available) for AdminNick. + * No detailed info available for this / Unsupported. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onAdminNick($data) + { + //NOt particularly useful to us + $this->callHandler("AdminNick", $data); + } + + /** + * AdminPasswd Handler + * + * Called on AdminPasswd. + * Call's user handler (if available) for AdminPasswd. + * No detailed info available for this / Unsupported. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onAdminPasswd($data) + { + //Also not particlualry useful to the internals + $this->callHandler("AdminPasswd", $data); + } + + /** + * Pause Handler + * + * Called on Pause. + * Call's user handler (if available) for Pause. + * No detailed info available for this / Unsupported. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onPause($data) + { + //This is pretty useless to us too... + + $this->callHandler("Pause", $data); + } + + /** + * Direct Connection Handler + * + * Called on Direct Connection Request(Rvous). + * Call's user handler (if available) for Rvous. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function onRvous($data) + { + $this->callHandler("Rvous", $data); + } + + /** + * CatchAll Handler + * + * Called for unrecognized commands. + * Logs unsupported messages to array. + * Call's user handler (if available) for CatchAll. + * + * @access private + * @param String $data Raw message from server + * @return void + */ + function CatchAll($data) + { + //Add to a log of unsupported messages. + + $this->unsupported[] = $data; + //$this->log($data); + //print_r($data); + + $this->callHandler("CatchAll", $data); + } + + /** + * Calls User Handler + * + * Calls registered handler for a specific event. + * + * @access private + * @param String $event Command (event) name (Rvous etc) + * @param String $data Raw message from server + * @see registerHandler + * @return void + */ + function callHandler($event, $data) + { + + if (isset($this->myEventHandlers[$event])) + { + //$function = $this->myEventHandlers[$event] . "(\$data);"; + //eval($function); + call_user_func($this->myEventHandlers[$event], $data); + } + else + { + $this->noHandler($data); + } + } + + /** + * Registers a user handler + * + * Handler List + * SignOn, Config, ERROR, NICK, IMIn, UpdateBuddy, Eviled, Warned, ChatJoin + * ChatIn, ChatUpdate, ChatInvite, ChatLeft, GotoURL, DirStatus, AdminNick + * AdminPasswd, Pause, Rvous, DimIn, CatchAll + * + * @access private + * @param String $event Event name + * @param String $handler User function to call + * @see callHandler + * @return boolean Returns true if successful + */ + function registerHandler($event, $handler) + { + if (is_callable($handler)) + { + $this->myEventHandlers[$event] = $handler; + return true; + } + else + { + return false; + } + } + + /** + * No user handler method fall back. + * + * Does nothing with message. + * + * @access public + * @param String $message Raw server message + * @return void + */ + function noHandler($message) + { + //This function intentionally left blank + //This is where the handlers will fall to for now. I plan on including a more + //efficent check to avoid the apparent stack jumps that this code will produce + //But for now, just fall into here, and be happy + return; + } + + //GLOBAL FUNCTIONS + + /** + * Finds type, and returns as part of array ['type'] + * Puts message in ['incoming'] + * + * Helper method for getMessageInfo. + * + * @access public + * @param String $message Raw server message + * @see msg_parse + * @see getMessageInfo + * @return array + */ + static function msg_type($message) + { + $command = array(); + @list($cmd, $rest) = explode(":", $message); + switch($cmd) + { + case 'IM_IN2': + $type = AIM_TYPE_MSG; + break; + + case 'UPDATE_BUDDY2': + $type = AIM_TYPE_UPDATEBUDDY; + break; + + case 'EVILED': + $type = AIM_TYPE_WARN; + break; + + case 'SIGN_ON': + $type = AIM_TYPE_SIGNON; + break; + + case 'NICK': + $type = AIM_TYPE_NICK; + break; + + case 'ERROR': + $type = AIM_TYPE_ERROR; + break; + + case 'CHAT_JOIN': + $type = AIM_TYPE_CHATJ; + break; + + case 'CHAT_IN': + $type = AIM_TYPE_CHATI; + break; + + case 'CHAT_UPDATE_BUDDY': + $type = AIM_TYPE_CHATUPDBUD; + break; + + case 'CHAT_INVITE': + $type = AIM_TYPE_CHATINV; + break; + + case 'CHAT_LEFT': + $type = AIM_TYPE_CHATLE; + break; + + case 'GOTO_URL': + $type = AIM_TYPE_URL; + break; + + case 'ADMIN_NICK_STATUS': + $type = AIM_TYPE_NICKSTAT; + break; + + case 'ADMIN_PASSWD_STATUS': + $type = AIM_TYPE_PASSSTAT; + break; + + case 'RVOUS_PROPOSE': + $type = AIM_TYPE_RVOUSP; + break; + + default: + $type = AIM_TYPE_NOT_IMPLEMENTED; + break; + } + $command['type'] = $type; + $command['incoming'] = $message; + return $command; + } + + /** + * Parses message and splits into info array + * + * Helper method for getMessageInfo. + * + * @access public + * @param String $command Message and type (after msg_type) + * @see msg_type + * @see getMessageInfo + * @return array + */ + static function msg_parse($command) + { + $info = array(); + switch($command['type']) + { + case AIM_TYPE_WARN: + $command['incoming'] .= ":0"; + $it = explode(":", $command['incoming']); + $info['warnlevel'] = $it[1]; + $info['from'] = $it[2]; + + break; + + case AIM_TYPE_MSG: + $it = explode(":", $command['incoming'],5); + $info['auto'] = $it[2]; + $info['from'] = $it[1]; + $info['message'] = $it[4]; + break; + + case AIM_TYPE_UPDATEBUDDY: + @list($cmd, $info['sn'], $info['online'], $info['warnlevel'], $info['signon'], $info['idle'], $info['uc']) = explode(":", $command['incoming']); + break; + + case AIM_TYPE_SIGNON: + @list($cmd, $info['version']) = explode(":", $command['incoming']); + break; + + case AIM_TYPE_NICK: + @list($cmd, $info['nickname']) = explode(":", $command['incoming']); + break; + case AIM_TYPE_ERROR: + @list($cmd, $info['errorcode'], $info['args']) = explode(":", $command['incoming']); + break; + + case AIM_TYPE_CHATJ: + @list($cmd, $info['chatid'], $info['chatname']) = explode(":", $command['incoming']); + break; + + case AIM_TYPE_CHATI: + @list($cmd, $info['chatid'], $info['user'], $info['whisper'], $info['message']) = explode(":", $command['incoming'],5); + break; + + case AIM_TYPE_CHATUPDBUD: + @list($cmd, $info['chatid'], $info['inside'], $info['userlist']) = explode(":", $command['incoming'],3); + break; + + case AIM_TYPE_CHATINV: + @list($cmd, $info['chatname'], $info['chatid'], $info['from'], $info['message']) = explode(":", $command['incoming'],5); + break; + + case AIM_TYPE_CHATLE: + @list($cmd, $info['chatid']) = explode(":", $command['incoming']); + break; + + case AIM_TYPE_URL: + @list($cmd, $info['windowname'], $info['url']) = explode(":", $command['incoming'],3); + break; + + case AIM_TYPE_RVOUSP: + @list($cmd,$info['user'],$info['uuid'],$info['cookie'],$info['seq'],$info['rip'],$info['pip'],$info['vip'],$info['port'],$info['tlvs']) = explode(":",$command['incoming'],10); + break; + + case AIM_TYPE_NICKSTAT: + case AIM_TYPE_PASSSTAT: + @list($cmd, $info['returncode'], $info['opt']) = explode(":", $command['incoming'],3); + break; + + default: + $info['command'] = $command['incoming']; + } + return $info; + } + + /** + * Returns a parsed message + * + * Calls msg_parse(msg_type( to first determine message type and then parse accordingly + * + * @access public + * @param String $command Raw server message + * @see msg_type + * @see msg_parse + * @return array + */ + static function getMessageInfo($message) + { + return self::msg_parse(self::msg_type($message)); + } + + /** + * Checks socket for end of file + * + * @access public + * @param Resource $socket Socket to check + * @return boolean true if end of file (socket) + */ + static function socketcheck($socket){ + $info = stream_get_meta_data($socket); + return $info['eof']; + //return(feof($socket)); + } +} + +?> diff --git a/plugins/Aim/extlib/phptoclib/dconnection.php b/plugins/Aim/extlib/phptoclib/dconnection.php new file mode 100755 index 000000000..c6be25ffb --- /dev/null +++ b/plugins/Aim/extlib/phptoclib/dconnection.php @@ -0,0 +1,229 @@ +<?php + +//The following class was created June 30th 2004 by Jeremy(pickle) +//This class is designed to handle a direct connection + +class Dconnect +{ + var $sock; + var $lastReceived; + var $lastMessage; + var $connected; + var $cookie; + var $type=2; + var $connectedTo; + + + function Dconnect($ip,$port) + { + if(!$this->connect($ip,$port)) + { + sEcho("Connection failed constructor"); + $this->connected=false; + } + else + $this->connected=true; + + $this->lastMessage=""; + $this->lastReceived=""; + } + + function readDIM() + { + /* + if(!$this->stuffToRead()) + { + sEcho("Nothing to read"); + $this->lastMessage=$this->lastReceived=""; + return false; + } + */ + $head=fread($this->sock,6); + if(strlen($head)<=0) + { + sEcho("The direct connection has been closed"); + return false; + } + $minihead=unpack("a4ver/nsize",$head); + if($minihead['size'] <=0) + return; + $headerinfo=unpack("nchan/nsix/nzero/a6cookie/Npt1/Npt2/npt3/Nlen/Npt/npt0/ntype/Nzerom/a*sn",fread($this->sock,($minihead['size']-6))); + $allheader=array_merge($minihead,$headerinfo); + sEcho($allheader); + if($allheader['len']>0 && $allheader['len'] <= MAX_DIM_SIZE) + { + $left=$allheader['len']; + $stuff=""; + $nonin=0; + while(strlen($stuff) < $allheader['len'] && $nonin<3) + { + $stuffg=fread($this->sock,$left); + if(strlen($stuffg)<0) + { + $nonin++; + continue; + } + $left=$left - strlen($stuffg); + $stuff.=$stuffg; + } + $data=unpack("a*decoded",$stuff); + } + + else if($allheader['len'] > MAX_DIM_SIZE) + { + $data['decoded']="too big"; + } + + else + $data['decoded']=""; + $all=array_merge($allheader,$data); + + $this->lastReceived=$all; + $this->lastMessage=$all['decoded']; + + //$function=$this->DimInf . "(\$all);"; + //eval($function); + + return $all; + } + + function sendMessage($message,$sn) + { + //Make the "mini header" + $minihead=pack("a4n","ODC2",76); + $header=pack("nnna6NNnNNnnNa*",1,6,0,$this->cookie,0,0,0,strlen($message),0,0,96,0,$sn); + $bighead=$minihead . $header; + while(strlen($bighead)<76) + $bighead.=pack("c",0); + + $tosend=$bighead . pack("a*",$message); + $w=array($this->sock); + stream_select($r=NULL,$w,$e=NULL,NULL); + //Now send it all + fputs($this->sock,$tosend,strlen($tosend)); + } + function stuffToRead() + { + //$info=stream_get_meta_data($this->sock); + //sEcho($info); + $s=array($this->sock); + $changed=stream_select($s,$fds=NULL,$m=NULL,0,20000); + return ($changed>0); + } + + function close() + { + $this->connected=false; + return fclose($this->sock); + } + + function connect($ip,$port) + { + $this->sock=fsockopen($ip,$port,$en,$es,3); + if(!$this->sock) + { sEcho("Connection failed"); + $this->sock=null; + return false; + } + return true; + } +} + + +class FileSendConnect +{ + var $sock; + var $lastReceived; + var $lastMessage; + var $connected; + var $cookie; + var $tpye=3; + + + function FileSendConnect($ip,$port) + { + if(!$this->connect($ip,$port)) + { + sEcho("Connection failed constructor"); + $this->connected=false; + } + else + $this->connected=true; + + $this->lastMessage=""; + $this->lastReceived=""; + } + + function readDIM() + { + + if(!$this->stuffToRead()) + { + sEcho("Nothing to read"); + $this->lastMessage=$this->lastReceived=""; + return; + } + + $minihead=unpack("a4ver/nsize",fread($this->sock,6)); + if($minihead['size'] <=0) + return; + $headerinfo=unpack("nchan/nsix/nzero/a6cookie/Npt1/Npt2/npt3/Nlen/Npt/npt0/ntype/Nzerom/a*sn",fread($this->sock,($minihead['size']-6))); + $allheader=array_merge($minihead,$headerinfo); + sEcho($allheader); + if($allheader['len']>0) + $data=unpack("a*decoded",fread($this->sock,$allheader['len'])); + else + $data['decoded']=""; + $all=array_merge($allheader,$data); + + $this->lastReceived=$all; + $this->lastMessage=$all['decoded']; + + //$function=$this->DimInf . "(\$all);"; + //eval($function); + + return $all; + } + + function sendMessage($message,$sn) + { + //Make the "mini header" + $minihead=pack("a4n","ODC2",76); + $header=pack("nnna6NNnNNnnNa*",1,6,0,$this->cookie,0,0,0,strlen($message),0,0,96,0,$sn); + $bighead=$minihead . $header; + while(strlen($bighead)<76) + $bighead.=pack("c",0); + + $tosend=$bighead . pack("a*",$message); + + //Now send it all + fwrite($this->sock,$tosend,strlen($tosend)); + } + function stuffToRead() + { + //$info=stream_get_meta_data($this->sock); + //sEcho($info); + $s=array($this->sock); + $changed=stream_select($s,$fds=NULL,$m=NULL,1); + return ($changed>0); + } + + function close() + { + $this->connected=false; + fclose($this->sock); + unset($this->sock); + return true; + } + + function connect($ip,$port) + { + $this->sock=fsockopen($ip,$port,$en,$es,3); + if(!$this->sock) + { sEcho("Connection failed to" . $ip . ":" . $port); + $this->sock=null; + return false; + } + return true; + } +} diff --git a/plugins/BitlyUrl/BitlyUrlPlugin.php b/plugins/BitlyUrl/BitlyUrlPlugin.php index b4f80a50a..532e66fbc 100644 --- a/plugins/BitlyUrl/BitlyUrlPlugin.php +++ b/plugins/BitlyUrl/BitlyUrlPlugin.php @@ -33,8 +33,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php'; - class BitlyUrlPlugin extends UrlShortenerPlugin { public $shortenerName = 'bit.ly'; diff --git a/plugins/CacheLog/locale/nb/LC_MESSAGES/CacheLog.po b/plugins/CacheLog/locale/nb/LC_MESSAGES/CacheLog.po deleted file mode 100644 index e9622df88..000000000 --- a/plugins/CacheLog/locale/nb/LC_MESSAGES/CacheLog.po +++ /dev/null @@ -1,26 +0,0 @@ -# Translation of StatusNet - CacheLog to Norwegian (bokmål)‬ (‪Norsk (bokmål)‬) -# Expored from translatewiki.net -# -# Author: Nghtwlkr -# -- -# This file is distributed under the same license as the StatusNet package. -# -msgid "" -msgstr "" -"Project-Id-Version: StatusNet - CacheLog\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-10-27 23:43+0000\n" -"PO-Revision-Date: 2010-10-27 23:46:21+0000\n" -"Language-Team: Norwegian (bokmål)‬ <http://translatewiki.net/wiki/Portal:no>\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-POT-Import-Date: 2010-10-23 18:57:00+0000\n" -"X-Generator: MediaWiki 1.17alpha (r75590); Translate extension (2010-09-17)\n" -"X-Translation-Project: translatewiki.net at http://translatewiki.net\n" -"X-Language-Code: no\n" -"X-Message-Group: #out-statusnet-plugin-cachelog\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: CacheLogPlugin.php:106 -msgid "Log reads and writes to the cache." -msgstr "Logg leser og skriver til hurtiglageret." diff --git a/plugins/CasAuthentication/caslogin.php b/plugins/CasAuthentication/caslogin.php index 846774e7c..3301ce582 100644 --- a/plugins/CasAuthentication/caslogin.php +++ b/plugins/CasAuthentication/caslogin.php @@ -28,7 +28,7 @@ class CasloginAction extends Action $this->clientError(_m('Already logged in.')); } else { global $casSettings; - phpCAS::client(CAS_VERSION_2_0,$casSettings['server'],$casSettings['port'],$casSettings['path']); + phpCAS::client(CAS_VERSION_2_0,$casSettings['server'],$casSettings['port'],$casSettings['path'],false); phpCAS::setNoCasServerValidation(); phpCAS::handleLogoutRequests(); phpCAS::forceAuthentication(); diff --git a/plugins/CasAuthentication/extlib/CAS.php b/plugins/CasAuthentication/extlib/CAS.php index e75437419..62a617579 100644 --- a/plugins/CasAuthentication/extlib/CAS.php +++ b/plugins/CasAuthentication/extlib/CAS.php @@ -1,20 +1,46 @@ <?php
-// commented in 0.4.22-RC2 for Sylvain Derosiaux
-// error_reporting(E_ALL ^ E_NOTICE);
+/*
+ * Copyright © 2003-2010, The ESUP-Portail consortium & the JA-SIG Collaborative.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the ESUP-Portail consortium & the JA-SIG
+ * Collaborative nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior
+ * written permission.
+
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
//
// hack by Vangelis Haniotakis to handle the absence of $_SERVER['REQUEST_URI'] in IIS
//
if (!$_SERVER['REQUEST_URI']) {
- $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'].'?'.$_SERVER['QUERY_STRING'];
+ $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
}
//
// another one by Vangelis Haniotakis also to make phpCAS work with PHP5
//
-if (version_compare(PHP_VERSION,'5','>=')) {
- require_once(dirname(__FILE__).'/CAS/domxml-php4-to-php5.php');
+if (version_compare(PHP_VERSION, '5', '>=') && !(function_exists('domxml_new_doc'))) {
+ require_once (dirname(__FILE__) . '/CAS/domxml-php4-to-php5.php');
}
/**
@@ -35,24 +61,24 @@ if (version_compare(PHP_VERSION,'5','>=')) { /**
* phpCAS version. accessible for the user by phpCAS::getVersion().
*/
-define('PHPCAS_VERSION','1.1.0RC6');
+define('PHPCAS_VERSION', '1.1.2');
// ------------------------------------------------------------------------
// CAS VERSIONS
// ------------------------------------------------------------------------
- /**
- * @addtogroup public
- * @{
- */
+/**
+ * @addtogroup public
+ * @{
+ */
/**
* CAS version 1.0
*/
-define("CAS_VERSION_1_0",'1.0');
+define("CAS_VERSION_1_0", '1.0');
/*!
* CAS version 2.0
*/
-define("CAS_VERSION_2_0",'2.0');
+define("CAS_VERSION_2_0", '2.0');
// ------------------------------------------------------------------------
// SAML defines
@@ -71,143 +97,141 @@ define("SAML_XML_HEADER", '<?xml version="1.0" encoding="UTF-8"?>'); /**
* SOAP envelope for SAML POST
*/
-define ("SAML_SOAP_ENV", '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/>');
+define("SAML_SOAP_ENV", '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/>');
/**
* SOAP body for SAML POST
*/
-define ("SAML_SOAP_BODY", '<SOAP-ENV:Body>');
+define("SAML_SOAP_BODY", '<SOAP-ENV:Body>');
/**
* SAMLP request
*/
-define ("SAMLP_REQUEST", '<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" MajorVersion="1" MinorVersion="1" RequestID="_192.168.16.51.1024506224022" IssueInstant="2002-06-19T17:03:44.022Z">');
-define ("SAMLP_REQUEST_CLOSE", '</samlp:Request>');
+define("SAMLP_REQUEST", '<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" MajorVersion="1" MinorVersion="1" RequestID="_192.168.16.51.1024506224022" IssueInstant="2002-06-19T17:03:44.022Z">');
+define("SAMLP_REQUEST_CLOSE", '</samlp:Request>');
/**
* SAMLP artifact tag (for the ticket)
*/
-define ("SAML_ASSERTION_ARTIFACT", '<samlp:AssertionArtifact>');
+define("SAML_ASSERTION_ARTIFACT", '<samlp:AssertionArtifact>');
/**
* SAMLP close
*/
-define ("SAML_ASSERTION_ARTIFACT_CLOSE", '</samlp:AssertionArtifact>');
+define("SAML_ASSERTION_ARTIFACT_CLOSE", '</samlp:AssertionArtifact>');
/**
* SOAP body close
*/
-define ("SAML_SOAP_BODY_CLOSE", '</SOAP-ENV:Body>');
+define("SAML_SOAP_BODY_CLOSE", '</SOAP-ENV:Body>');
/**
* SOAP envelope close
*/
-define ("SAML_SOAP_ENV_CLOSE", '</SOAP-ENV:Envelope>');
+define("SAML_SOAP_ENV_CLOSE", '</SOAP-ENV:Envelope>');
/**
* SAML Attributes
*/
define("SAML_ATTRIBUTES", 'SAMLATTRIBS');
-
-
/** @} */
- /**
- * @addtogroup publicPGTStorage
- * @{
- */
+/**
+ * @addtogroup publicPGTStorage
+ * @{
+ */
// ------------------------------------------------------------------------
// FILE PGT STORAGE
// ------------------------------------------------------------------------
- /**
- * Default path used when storing PGT's to file
- */
-define("CAS_PGT_STORAGE_FILE_DEFAULT_PATH",'/tmp');
+/**
+ * Default path used when storing PGT's to file
+ */
+define("CAS_PGT_STORAGE_FILE_DEFAULT_PATH", '/tmp');
/**
* phpCAS::setPGTStorageFile()'s 2nd parameter to write plain text files
*/
-define("CAS_PGT_STORAGE_FILE_FORMAT_PLAIN",'plain');
+define("CAS_PGT_STORAGE_FILE_FORMAT_PLAIN", 'plain');
/**
* phpCAS::setPGTStorageFile()'s 2nd parameter to write xml files
*/
-define("CAS_PGT_STORAGE_FILE_FORMAT_XML",'xml');
+define("CAS_PGT_STORAGE_FILE_FORMAT_XML", 'xml');
/**
* Default format used when storing PGT's to file
*/
-define("CAS_PGT_STORAGE_FILE_DEFAULT_FORMAT",CAS_PGT_STORAGE_FILE_FORMAT_PLAIN);
+define("CAS_PGT_STORAGE_FILE_DEFAULT_FORMAT", CAS_PGT_STORAGE_FILE_FORMAT_PLAIN);
// ------------------------------------------------------------------------
// DATABASE PGT STORAGE
// ------------------------------------------------------------------------
- /**
- * default database type when storing PGT's to database
- */
-define("CAS_PGT_STORAGE_DB_DEFAULT_DATABASE_TYPE",'mysql');
+/**
+ * default database type when storing PGT's to database
+ */
+define("CAS_PGT_STORAGE_DB_DEFAULT_DATABASE_TYPE", 'mysql');
/**
* default host when storing PGT's to database
*/
-define("CAS_PGT_STORAGE_DB_DEFAULT_HOSTNAME",'localhost');
+define("CAS_PGT_STORAGE_DB_DEFAULT_HOSTNAME", 'localhost');
/**
* default port when storing PGT's to database
*/
-define("CAS_PGT_STORAGE_DB_DEFAULT_PORT",'');
+define("CAS_PGT_STORAGE_DB_DEFAULT_PORT", '');
/**
* default database when storing PGT's to database
*/
-define("CAS_PGT_STORAGE_DB_DEFAULT_DATABASE",'phpCAS');
+define("CAS_PGT_STORAGE_DB_DEFAULT_DATABASE", 'phpCAS');
/**
* default table when storing PGT's to database
*/
-define("CAS_PGT_STORAGE_DB_DEFAULT_TABLE",'pgt');
+define("CAS_PGT_STORAGE_DB_DEFAULT_TABLE", 'pgt');
/** @} */
// ------------------------------------------------------------------------
// SERVICE ACCESS ERRORS
// ------------------------------------------------------------------------
- /**
- * @addtogroup publicServices
- * @{
- */
+/**
+ * @addtogroup publicServices
+ * @{
+ */
/**
* phpCAS::service() error code on success
*/
-define("PHPCAS_SERVICE_OK",0);
+define("PHPCAS_SERVICE_OK", 0);
/**
* phpCAS::service() error code when the PT could not retrieve because
* the CAS server did not respond.
*/
-define("PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE",1);
+define("PHPCAS_SERVICE_PT_NO_SERVER_RESPONSE", 1);
/**
* phpCAS::service() error code when the PT could not retrieve because
* the response of the CAS server was ill-formed.
*/
-define("PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE",2);
+define("PHPCAS_SERVICE_PT_BAD_SERVER_RESPONSE", 2);
/**
* phpCAS::service() error code when the PT could not retrieve because
* the CAS server did not want to.
*/
-define("PHPCAS_SERVICE_PT_FAILURE",3);
+define("PHPCAS_SERVICE_PT_FAILURE", 3);
/**
* phpCAS::service() error code when the service was not available.
*/
-define("PHPCAS_SERVICE_NOT AVAILABLE",4);
+define("PHPCAS_SERVICE_NOT AVAILABLE", 4);
/** @} */
// ------------------------------------------------------------------------
// LANGUAGES
// ------------------------------------------------------------------------
- /**
- * @addtogroup publicLang
- * @{
- */
-
-define("PHPCAS_LANG_ENGLISH", 'english');
-define("PHPCAS_LANG_FRENCH", 'french');
-define("PHPCAS_LANG_GREEK", 'greek');
-define("PHPCAS_LANG_GERMAN", 'german');
-define("PHPCAS_LANG_JAPANESE", 'japanese');
-define("PHPCAS_LANG_SPANISH", 'spanish');
-define("PHPCAS_LANG_CATALAN", 'catalan');
+/**
+ * @addtogroup publicLang
+ * @{
+ */
+
+define("PHPCAS_LANG_ENGLISH", 'english');
+define("PHPCAS_LANG_FRENCH", 'french');
+define("PHPCAS_LANG_GREEK", 'greek');
+define("PHPCAS_LANG_GERMAN", 'german');
+define("PHPCAS_LANG_JAPANESE", 'japanese');
+define("PHPCAS_LANG_SPANISH", 'spanish');
+define("PHPCAS_LANG_CATALAN", 'catalan');
/** @} */
@@ -225,31 +249,31 @@ define("PHPCAS_LANG_DEFAULT", PHPCAS_LANG_ENGLISH); // ------------------------------------------------------------------------
// DEBUG
// ------------------------------------------------------------------------
- /**
- * @addtogroup publicDebug
- * @{
- */
+/**
+ * @addtogroup publicDebug
+ * @{
+ */
/**
* The default directory for the debug file under Unix.
*/
-define('DEFAULT_DEBUG_DIR','/tmp/');
+define('DEFAULT_DEBUG_DIR', '/tmp/');
/** @} */
// ------------------------------------------------------------------------
// MISC
// ------------------------------------------------------------------------
- /**
- * @addtogroup internalMisc
- * @{
- */
+/**
+ * @addtogroup internalMisc
+ * @{
+ */
/**
* This global variable is used by the interface class phpCAS.
*
* @hideinitializer
*/
-$GLOBALS['PHPCAS_CLIENT'] = null;
+$GLOBALS['PHPCAS_CLIENT'] = null;
/**
* This global variable is used to store where the initializer is called from
@@ -257,10 +281,12 @@ $GLOBALS['PHPCAS_CLIENT'] = null; *
* @hideinitializer
*/
-$GLOBALS['PHPCAS_INIT_CALL'] = array('done' => FALSE,
+$GLOBALS['PHPCAS_INIT_CALL'] = array (
+ 'done' => FALSE,
'file' => '?',
'line' => -1,
- 'method' => '?');
+ 'method' => '?'
+);
/**
* This global variable is used to store where the method checking
@@ -268,20 +294,24 @@ $GLOBALS['PHPCAS_INIT_CALL'] = array('done' => FALSE, *
* @hideinitializer
*/
-$GLOBALS['PHPCAS_AUTH_CHECK_CALL'] = array('done' => FALSE,
+$GLOBALS['PHPCAS_AUTH_CHECK_CALL'] = array (
+ 'done' => FALSE,
'file' => '?',
'line' => -1,
'method' => '?',
- 'result' => FALSE);
+ 'result' => FALSE
+);
/**
* This global variable is used to store phpCAS debug mode.
*
* @hideinitializer
*/
-$GLOBALS['PHPCAS_DEBUG'] = array('filename' => FALSE,
+$GLOBALS['PHPCAS_DEBUG'] = array (
+ 'filename' => FALSE,
'indent' => 0,
- 'unique_id' => '');
+ 'unique_id' => ''
+);
/** @} */
@@ -290,7 +320,7 @@ $GLOBALS['PHPCAS_DEBUG'] = array('filename' => FALSE, // ########################################################################
// include client class
-include_once(dirname(__FILE__).'/CAS/client.php');
+include_once (dirname(__FILE__) . '/CAS/client.php');
// ########################################################################
// INTERFACE CLASS
@@ -308,20 +338,17 @@ include_once(dirname(__FILE__).'/CAS/client.php'); * at the end of CAS/client.php).
*/
+class phpCAS {
-
-class phpCAS
-{
-
// ########################################################################
// INITIALIZATION
// ########################################################################
-
+
/**
* @addtogroup publicInit
* @{
*/
-
+
/**
* phpCAS client initializer.
* @note Only one of the phpCAS::client() and phpCAS::proxy functions should be
@@ -336,43 +363,41 @@ class phpCAS *
* @return a newly created CASClient object
*/
- function client($server_version,
- $server_hostname,
- $server_port,
- $server_uri,
- $start_session = true)
- {
+ function client($server_version, $server_hostname, $server_port, $server_uri, $start_session = true) {
global $PHPCAS_CLIENT, $PHPCAS_INIT_CALL;
-
- phpCAS::traceBegin();
- if ( is_object($PHPCAS_CLIENT) ) {
- phpCAS::error($PHPCAS_INIT_CALL['method'].'() has already been called (at '.$PHPCAS_INIT_CALL['file'].':'.$PHPCAS_INIT_CALL['line'].')');
+
+ phpCAS :: traceBegin();
+ if (is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error($PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . $PHPCAS_INIT_CALL['file'] . ':' . $PHPCAS_INIT_CALL['line'] . ')');
}
- if ( gettype($server_version) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_version (should be `string\')');
+ if (gettype($server_version) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_version (should be `string\')');
}
- if ( gettype($server_hostname) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_hostname (should be `string\')');
+ if (gettype($server_hostname) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_hostname (should be `string\')');
}
- if ( gettype($server_port) != 'integer' ) {
- phpCAS::error('type mismatched for parameter $server_port (should be `integer\')');
+ if (gettype($server_port) != 'integer') {
+ phpCAS :: error('type mismatched for parameter $server_port (should be `integer\')');
}
- if ( gettype($server_uri) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_uri (should be `string\')');
+ if (gettype($server_uri) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_uri (should be `string\')');
}
-
+
// store where the initializer is called from
- $dbg = phpCAS::backtrace();
- $PHPCAS_INIT_CALL = array('done' => TRUE,
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_INIT_CALL = array (
+ 'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
- 'method' => __CLASS__.'::'.__FUNCTION__);
-
+ 'method' => __CLASS__ . '::' . __FUNCTION__
+ );
+
// initialize the global object $PHPCAS_CLIENT
- $PHPCAS_CLIENT = new CASClient($server_version,FALSE/*proxy*/,$server_hostname,$server_port,$server_uri,$start_session);
- phpCAS::traceEnd();
- }
-
+ $PHPCAS_CLIENT = new CASClient($server_version, FALSE /*proxy*/
+ , $server_hostname, $server_port, $server_uri, $start_session);
+ phpCAS :: traceEnd();
+ }
+
/**
* phpCAS proxy initializer.
* @note Only one of the phpCAS::client() and phpCAS::proxy functions should be
@@ -387,110 +412,107 @@ class phpCAS *
* @return a newly created CASClient object
*/
- function proxy($server_version,
- $server_hostname,
- $server_port,
- $server_uri,
- $start_session = true)
- {
+ function proxy($server_version, $server_hostname, $server_port, $server_uri, $start_session = true) {
global $PHPCAS_CLIENT, $PHPCAS_INIT_CALL;
-
- phpCAS::traceBegin();
- if ( is_object($PHPCAS_CLIENT) ) {
- phpCAS::error($PHPCAS_INIT_CALL['method'].'() has already been called (at '.$PHPCAS_INIT_CALL['file'].':'.$PHPCAS_INIT_CALL['line'].')');
+
+ phpCAS :: traceBegin();
+ if (is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error($PHPCAS_INIT_CALL['method'] . '() has already been called (at ' . $PHPCAS_INIT_CALL['file'] . ':' . $PHPCAS_INIT_CALL['line'] . ')');
}
- if ( gettype($server_version) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_version (should be `string\')');
+ if (gettype($server_version) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_version (should be `string\')');
}
- if ( gettype($server_hostname) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_hostname (should be `string\')');
+ if (gettype($server_hostname) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_hostname (should be `string\')');
}
- if ( gettype($server_port) != 'integer' ) {
- phpCAS::error('type mismatched for parameter $server_port (should be `integer\')');
+ if (gettype($server_port) != 'integer') {
+ phpCAS :: error('type mismatched for parameter $server_port (should be `integer\')');
}
- if ( gettype($server_uri) != 'string' ) {
- phpCAS::error('type mismatched for parameter $server_uri (should be `string\')');
+ if (gettype($server_uri) != 'string') {
+ phpCAS :: error('type mismatched for parameter $server_uri (should be `string\')');
}
-
+
// store where the initialzer is called from
- $dbg = phpCAS::backtrace();
- $PHPCAS_INIT_CALL = array('done' => TRUE,
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_INIT_CALL = array (
+ 'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
- 'method' => __CLASS__.'::'.__FUNCTION__);
-
+ 'method' => __CLASS__ . '::' . __FUNCTION__
+ );
+
// initialize the global object $PHPCAS_CLIENT
- $PHPCAS_CLIENT = new CASClient($server_version,TRUE/*proxy*/,$server_hostname,$server_port,$server_uri,$start_session);
- phpCAS::traceEnd();
- }
-
+ $PHPCAS_CLIENT = new CASClient($server_version, TRUE /*proxy*/
+ , $server_hostname, $server_port, $server_uri, $start_session);
+ phpCAS :: traceEnd();
+ }
+
/** @} */
// ########################################################################
// DEBUGGING
// ########################################################################
-
+
/**
* @addtogroup publicDebug
* @{
*/
-
+
/**
* Set/unset debug mode
*
* @param $filename the name of the file used for logging, or FALSE to stop debugging.
*/
- function setDebug($filename='')
- {
+ function setDebug($filename = '') {
global $PHPCAS_DEBUG;
-
- if ( $filename != FALSE && gettype($filename) != 'string' ) {
- phpCAS::error('type mismatched for parameter $dbg (should be FALSE or the name of the log file)');
- }
-
- if ( empty($filename) ) {
- if ( preg_match('/^Win.*/',getenv('OS')) ) {
- if ( isset($_ENV['TMP']) ) {
- $debugDir = $_ENV['TMP'].'/';
- } else if ( isset($_ENV['TEMP']) ) {
- $debugDir = $_ENV['TEMP'].'/';
- } else {
- $debugDir = '';
- }
+
+ if ($filename != FALSE && gettype($filename) != 'string') {
+ phpCAS :: error('type mismatched for parameter $dbg (should be FALSE or the name of the log file)');
+ }
+
+ if (empty ($filename)) {
+ if (preg_match('/^Win.*/', getenv('OS'))) {
+ if (isset ($_ENV['TMP'])) {
+ $debugDir = $_ENV['TMP'] . '/';
+ } else
+ if (isset ($_ENV['TEMP'])) {
+ $debugDir = $_ENV['TEMP'] . '/';
+ } else {
+ $debugDir = '';
+ }
} else {
$debugDir = DEFAULT_DEBUG_DIR;
}
$filename = $debugDir . 'phpCAS.log';
}
-
- if ( empty($PHPCAS_DEBUG['unique_id']) ) {
- $PHPCAS_DEBUG['unique_id'] = substr(strtoupper(md5(uniqid(''))),0,4);
+
+ if (empty ($PHPCAS_DEBUG['unique_id'])) {
+ $PHPCAS_DEBUG['unique_id'] = substr(strtoupper(md5(uniqid(''))), 0, 4);
}
-
+
$PHPCAS_DEBUG['filename'] = $filename;
-
- phpCAS::trace('START ******************');
- }
-
+
+ phpCAS :: trace('START phpCAS-' . PHPCAS_VERSION . ' ******************');
+ }
+
/** @} */
/**
* @addtogroup internalDebug
* @{
*/
-
+
/**
* This method is a wrapper for debug_backtrace() that is not available
* in all PHP versions (>= 4.3.0 only)
*/
- function backtrace()
- {
- if ( function_exists('debug_backtrace') ) {
+ function backtrace() {
+ if (function_exists('debug_backtrace')) {
return debug_backtrace();
} else {
// poor man's hack ... but it does work ...
- return array();
- }
+ return array ();
}
-
+ }
+
/**
* Logs a string in debug mode.
*
@@ -498,20 +520,19 @@ class phpCAS *
* @private
*/
- function log($str)
- {
+ function log($str) {
$indent_str = ".";
global $PHPCAS_DEBUG;
-
- if ( $PHPCAS_DEBUG['filename'] ) {
- for ($i=0;$i<$PHPCAS_DEBUG['indent'];$i++) {
+
+ if ($PHPCAS_DEBUG['filename']) {
+ for ($i = 0; $i < $PHPCAS_DEBUG['indent']; $i++) {
$indent_str .= '| ';
}
- error_log($PHPCAS_DEBUG['unique_id'].' '.$indent_str.$str."\n",3,$PHPCAS_DEBUG['filename']);
- }
-
+ error_log($PHPCAS_DEBUG['unique_id'] . ' ' . $indent_str . $str . "\n", 3, $PHPCAS_DEBUG['filename']);
}
-
+
+ }
+
/**
* This method is used by interface methods to print an error and where the function
* was originally called from.
@@ -520,16 +541,15 @@ class phpCAS *
* @private
*/
- function error($msg)
- {
- $dbg = phpCAS::backtrace();
+ function error($msg) {
+ $dbg = phpCAS :: backtrace();
$function = '?';
$file = '?';
$line = '?';
- if ( is_array($dbg) ) {
- for ( $i=1; $i<sizeof($dbg); $i++) {
- if ( is_array($dbg[$i]) ) {
- if ( $dbg[$i]['class'] == __CLASS__ ) {
+ if (is_array($dbg)) {
+ for ($i = 1; $i < sizeof($dbg); $i++) {
+ if (is_array($dbg[$i])) {
+ if ($dbg[$i]['class'] == __CLASS__) {
$function = $dbg[$i]['function'];
$file = $dbg[$i]['file'];
$line = $dbg[$i]['line'];
@@ -537,77 +557,73 @@ class phpCAS }
}
}
- echo "<br />\n<b>phpCAS error</b>: <font color=\"FF0000\"><b>".__CLASS__."::".$function.'(): '.htmlentities($msg)."</b></font> in <b>".$file."</b> on line <b>".$line."</b><br />\n";
- phpCAS::trace($msg);
- phpCAS::traceExit();
- exit();
- }
-
+ echo "<br />\n<b>phpCAS error</b>: <font color=\"FF0000\"><b>" . __CLASS__ . "::" . $function . '(): ' . htmlentities($msg) . "</b></font> in <b>" . $file . "</b> on line <b>" . $line . "</b><br />\n";
+ phpCAS :: trace($msg);
+ phpCAS :: traceExit();
+ exit ();
+ }
+
/**
* This method is used to log something in debug mode.
*/
- function trace($str)
- {
- $dbg = phpCAS::backtrace();
- phpCAS::log($str.' ['.basename($dbg[1]['file']).':'.$dbg[1]['line'].']');
- }
-
+ function trace($str) {
+ $dbg = phpCAS :: backtrace();
+ phpCAS :: log($str . ' [' . basename($dbg[1]['file']) . ':' . $dbg[1]['line'] . ']');
+ }
+
/**
* This method is used to indicate the start of the execution of a function in debug mode.
*/
- function traceBegin()
- {
+ function traceBegin() {
global $PHPCAS_DEBUG;
-
- $dbg = phpCAS::backtrace();
+
+ $dbg = phpCAS :: backtrace();
$str = '=> ';
- if ( !empty($dbg[2]['class']) ) {
- $str .= $dbg[2]['class'].'::';
+ if (!empty ($dbg[2]['class'])) {
+ $str .= $dbg[2]['class'] . '::';
}
- $str .= $dbg[2]['function'].'(';
- if ( is_array($dbg[2]['args']) ) {
+ $str .= $dbg[2]['function'] . '(';
+ if (is_array($dbg[2]['args'])) {
foreach ($dbg[2]['args'] as $index => $arg) {
- if ( $index != 0 ) {
+ if ($index != 0) {
$str .= ', ';
}
- $str .= str_replace("\n","",var_export($arg,TRUE));
+ $str .= str_replace("\n", "", var_export($arg, TRUE));
}
}
- $str .= ') ['.basename($dbg[2]['file']).':'.$dbg[2]['line'].']';
- phpCAS::log($str);
- $PHPCAS_DEBUG['indent'] ++;
- }
-
+ $str .= ') [' . basename($dbg[2]['file']) . ':' . $dbg[2]['line'] . ']';
+ phpCAS :: log($str);
+ $PHPCAS_DEBUG['indent']++;
+ }
+
/**
* This method is used to indicate the end of the execution of a function in debug mode.
*
* @param $res the result of the function
*/
- function traceEnd($res='')
- {
+ function traceEnd($res = '') {
global $PHPCAS_DEBUG;
-
- $PHPCAS_DEBUG['indent'] --;
- $dbg = phpCAS::backtrace();
+
+ $PHPCAS_DEBUG['indent']--;
+ $dbg = phpCAS :: backtrace();
$str = '';
- $str .= '<= '.str_replace("\n","",var_export($res,TRUE));
- phpCAS::log($str);
- }
-
+ $str .= '<= ' . str_replace("\n", "", var_export($res, TRUE));
+ phpCAS :: log($str);
+ }
+
/**
* This method is used to indicate the end of the execution of the program
*/
- function traceExit()
- {
+ function traceExit() {
global $PHPCAS_DEBUG;
-
- phpCAS::log('exit()');
- while ( $PHPCAS_DEBUG['indent'] > 0 ) {
- phpCAS::log('-');
- $PHPCAS_DEBUG['indent'] --;
- }
+
+ phpCAS :: log('exit()');
+ while ($PHPCAS_DEBUG['indent'] > 0) {
+ phpCAS :: log('-');
+ $PHPCAS_DEBUG['indent']--;
}
-
+ }
+
/** @} */
// ########################################################################
// INTERNATIONALIZATION
@@ -616,7 +632,7 @@ class phpCAS * @addtogroup publicLang
* @{
*/
-
+
/**
* This method is used to set the language used by phpCAS.
* @note Can be called only once.
@@ -625,18 +641,17 @@ class phpCAS *
* @sa PHPCAS_LANG_FRENCH, PHPCAS_LANG_ENGLISH
*/
- function setLang($lang)
- {
+ function setLang($lang) {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( gettype($lang) != 'string' ) {
- phpCAS::error('type mismatched for parameter $lang (should be `string\')');
+ if (gettype($lang) != 'string') {
+ phpCAS :: error('type mismatched for parameter $lang (should be `string\')');
}
$PHPCAS_CLIENT->setLang($lang);
- }
-
+ }
+
/** @} */
// ########################################################################
// VERSION
@@ -645,17 +660,16 @@ class phpCAS * @addtogroup public
* @{
*/
-
+
/**
* This method returns the phpCAS version.
*
* @return the phpCAS version.
*/
- function getVersion()
- {
+ function getVersion() {
return PHPCAS_VERSION;
- }
-
+ }
+
/** @} */
// ########################################################################
// HTML OUTPUT
@@ -664,41 +678,39 @@ class phpCAS * @addtogroup publicOutput
* @{
*/
-
+
/**
* This method sets the HTML header used for all outputs.
*
* @param $header the HTML header.
*/
- function setHTMLHeader($header)
- {
+ function setHTMLHeader($header) {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( gettype($header) != 'string' ) {
- phpCAS::error('type mismatched for parameter $header (should be `string\')');
+ if (gettype($header) != 'string') {
+ phpCAS :: error('type mismatched for parameter $header (should be `string\')');
}
$PHPCAS_CLIENT->setHTMLHeader($header);
- }
-
+ }
+
/**
* This method sets the HTML footer used for all outputs.
*
* @param $footer the HTML footer.
*/
- function setHTMLFooter($footer)
- {
+ function setHTMLFooter($footer) {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( gettype($footer) != 'string' ) {
- phpCAS::error('type mismatched for parameter $footer (should be `string\')');
+ if (gettype($footer) != 'string') {
+ phpCAS :: error('type mismatched for parameter $footer (should be `string\')');
}
$PHPCAS_CLIENT->setHTMLFooter($footer);
- }
-
+ }
+
/** @} */
// ########################################################################
// PGT STORAGE
@@ -707,7 +719,7 @@ class phpCAS * @addtogroup publicPGTStorage
* @{
*/
-
+
/**
* This method is used to tell phpCAS to store the response of the
* CAS server to PGT requests onto the filesystem.
@@ -715,31 +727,29 @@ class phpCAS * @param $format the format used to store the PGT's (`plain' and `xml' allowed)
* @param $path the path where the PGT's should be stored
*/
- function setPGTStorageFile($format='',
- $path='')
- {
- global $PHPCAS_CLIENT,$PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
- }
- if ( !$PHPCAS_CLIENT->isProxy() ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ function setPGTStorageFile($format = '', $path = '') {
+ global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( $PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called before '.$PHPCAS_AUTH_CHECK_CALL['method'].'() (called at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].')');
+ if (!$PHPCAS_CLIENT->isProxy()) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( gettype($format) != 'string' ) {
- phpCAS::error('type mismatched for parameter $format (should be `string\')');
+ if ($PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called before ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() (called at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ')');
}
- if ( gettype($path) != 'string' ) {
- phpCAS::error('type mismatched for parameter $format (should be `string\')');
+ if (gettype($format) != 'string') {
+ phpCAS :: error('type mismatched for parameter $format (should be `string\')');
}
- $PHPCAS_CLIENT->setPGTStorageFile($format,$path);
- phpCAS::traceEnd();
+ if (gettype($path) != 'string') {
+ phpCAS :: error('type mismatched for parameter $format (should be `string\')');
}
-
+ $PHPCAS_CLIENT->setPGTStorageFile($format, $path);
+ phpCAS :: traceEnd();
+ }
+
/**
* This method is used to tell phpCAS to store the response of the
* CAS server to PGT requests into a database.
@@ -755,51 +765,44 @@ class phpCAS * @param $database the name of the database
* @param $table the name of the table storing the data
*/
- function setPGTStorageDB($user,
- $password,
- $database_type='',
- $hostname='',
- $port=0,
- $database='',
- $table='')
- {
- global $PHPCAS_CLIENT,$PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
- }
- if ( !$PHPCAS_CLIENT->isProxy() ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ function setPGTStorageDB($user, $password, $database_type = '', $hostname = '', $port = 0, $database = '', $table = '') {
+ global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( $PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called before '.$PHPCAS_AUTH_CHECK_CALL['method'].'() (called at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].')');
+ if (!$PHPCAS_CLIENT->isProxy()) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( gettype($user) != 'string' ) {
- phpCAS::error('type mismatched for parameter $user (should be `string\')');
+ if ($PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called before ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() (called at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ')');
}
- if ( gettype($password) != 'string' ) {
- phpCAS::error('type mismatched for parameter $password (should be `string\')');
+ if (gettype($user) != 'string') {
+ phpCAS :: error('type mismatched for parameter $user (should be `string\')');
}
- if ( gettype($database_type) != 'string' ) {
- phpCAS::error('type mismatched for parameter $database_type (should be `string\')');
+ if (gettype($password) != 'string') {
+ phpCAS :: error('type mismatched for parameter $password (should be `string\')');
}
- if ( gettype($hostname) != 'string' ) {
- phpCAS::error('type mismatched for parameter $hostname (should be `string\')');
+ if (gettype($database_type) != 'string') {
+ phpCAS :: error('type mismatched for parameter $database_type (should be `string\')');
}
- if ( gettype($port) != 'integer' ) {
- phpCAS::error('type mismatched for parameter $port (should be `integer\')');
+ if (gettype($hostname) != 'string') {
+ phpCAS :: error('type mismatched for parameter $hostname (should be `string\')');
}
- if ( gettype($database) != 'string' ) {
- phpCAS::error('type mismatched for parameter $database (should be `string\')');
+ if (gettype($port) != 'integer') {
+ phpCAS :: error('type mismatched for parameter $port (should be `integer\')');
}
- if ( gettype($table) != 'string' ) {
- phpCAS::error('type mismatched for parameter $table (should be `string\')');
+ if (gettype($database) != 'string') {
+ phpCAS :: error('type mismatched for parameter $database (should be `string\')');
}
- $PHPCAS_CLIENT->setPGTStorageDB($user,$password,$database_type,$hostname,$port,$database,$table);
- phpCAS::traceEnd();
+ if (gettype($table) != 'string') {
+ phpCAS :: error('type mismatched for parameter $table (should be `string\')');
}
-
+ $PHPCAS_CLIENT->setPGTStorageDB($user, $password, $database_type, $hostname, $port, $database, $table);
+ phpCAS :: traceEnd();
+ }
+
/** @} */
// ########################################################################
// ACCESS TO EXTERNAL SERVICES
@@ -808,7 +811,7 @@ class phpCAS * @addtogroup publicServices
* @{
*/
-
+
/**
* This method is used to access an HTTP[S] service.
*
@@ -822,33 +825,32 @@ class phpCAS * @return TRUE on success, FALSE otherwise (in this later case, $err_code
* gives the reason why it failed and $output contains an error message).
*/
- function serviceWeb($url,&$err_code,&$output)
- {
+ function serviceWeb($url, & $err_code, & $output) {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_CLIENT->isProxy() ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ if (!$PHPCAS_CLIENT->isProxy()) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called after the programmer is sure the user has been authenticated (by calling '.__CLASS__.'::checkAuthentication() or '.__CLASS__.'::forceAuthentication()');
+ if (!$PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called after the programmer is sure the user has been authenticated (by calling ' . __CLASS__ . '::checkAuthentication() or ' . __CLASS__ . '::forceAuthentication()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['result'] ) {
- phpCAS::error('authentication was checked (by '.$PHPCAS_AUTH_CHECK_CALL['method'].'() at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].') but the method returned FALSE');
+ if (!$PHPCAS_AUTH_CHECK_CALL['result']) {
+ phpCAS :: error('authentication was checked (by ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ') but the method returned FALSE');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
-
- $res = $PHPCAS_CLIENT->serviceWeb($url,$err_code,$output);
-
- phpCAS::traceEnd($res);
+
+ $res = $PHPCAS_CLIENT->serviceWeb($url, $err_code, $output);
+
+ phpCAS :: traceEnd($res);
return $res;
- }
-
+ }
+
/**
* This method is used to access an IMAP/POP3/NNTP service.
*
@@ -866,37 +868,36 @@ class phpCAS * @return an IMAP stream on success, FALSE otherwise (in this later case, $err_code
* gives the reason why it failed and $err_msg contains an error message).
*/
- function serviceMail($url,$service,$flags,&$err_code,&$err_msg,&$pt)
- {
+ function serviceMail($url, $service, $flags, & $err_code, & $err_msg, & $pt) {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_CLIENT->isProxy() ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ if (!$PHPCAS_CLIENT->isProxy()) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called after the programmer is sure the user has been authenticated (by calling '.__CLASS__.'::checkAuthentication() or '.__CLASS__.'::forceAuthentication()');
+ if (!$PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called after the programmer is sure the user has been authenticated (by calling ' . __CLASS__ . '::checkAuthentication() or ' . __CLASS__ . '::forceAuthentication()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['result'] ) {
- phpCAS::error('authentication was checked (by '.$PHPCAS_AUTH_CHECK_CALL['method'].'() at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].') but the method returned FALSE');
+ if (!$PHPCAS_AUTH_CHECK_CALL['result']) {
+ phpCAS :: error('authentication was checked (by ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ') but the method returned FALSE');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
-
- if ( gettype($flags) != 'integer' ) {
- phpCAS::error('type mismatched for parameter $flags (should be `integer\')');
+
+ if (gettype($flags) != 'integer') {
+ phpCAS :: error('type mismatched for parameter $flags (should be `integer\')');
}
-
- $res = $PHPCAS_CLIENT->serviceMail($url,$service,$flags,$err_code,$err_msg,$pt);
-
- phpCAS::traceEnd($res);
+
+ $res = $PHPCAS_CLIENT->serviceMail($url, $service, $flags, $err_code, $err_msg, $pt);
+
+ phpCAS :: traceEnd($res);
return $res;
- }
-
+ }
+
/** @} */
// ########################################################################
// AUTHENTICATION
@@ -905,7 +906,7 @@ class phpCAS * @addtogroup publicAuth
* @{
*/
-
+
/**
* Set the times authentication will be cached before really accessing the CAS server in gateway mode:
* - -1: check only once, and then never again (until you pree login)
@@ -914,150 +915,156 @@ class phpCAS *
* @param $n an integer.
*/
- function setCacheTimesForAuthRecheck($n)
- {
+ function setCacheTimesForAuthRecheck($n) {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( gettype($n) != 'integer' ) {
- phpCAS::error('type mismatched for parameter $header (should be `string\')');
+ if (gettype($n) != 'integer') {
+ phpCAS :: error('type mismatched for parameter $header (should be `string\')');
}
$PHPCAS_CLIENT->setCacheTimesForAuthRecheck($n);
- }
-
+ }
+
/**
* This method is called to check if the user is authenticated (use the gateway feature).
* @return TRUE when the user is authenticated; otherwise FALSE.
*/
- function checkAuthentication()
- {
+ function checkAuthentication() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
-
+
$auth = $PHPCAS_CLIENT->checkAuthentication();
-
+
// store where the authentication has been checked and the result
- $dbg = phpCAS::backtrace();
- $PHPCAS_AUTH_CHECK_CALL = array('done' => TRUE,
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_AUTH_CHECK_CALL = array (
+ 'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
- 'method' => __CLASS__.'::'.__FUNCTION__,
- 'result' => $auth );
- phpCAS::traceEnd($auth);
- return $auth;
- }
+ 'method' => __CLASS__ . '::' . __FUNCTION__,
+ 'result' => $auth
+ );
+ phpCAS :: traceEnd($auth);
+ return $auth;
+ }
/**
* This method is called to force authentication if the user was not already
* authenticated. If the user is not authenticated, halt by redirecting to
* the CAS server.
*/
- function forceAuthentication()
- {
+ function forceAuthentication() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
-
+
$auth = $PHPCAS_CLIENT->forceAuthentication();
-
+
// store where the authentication has been checked and the result
- $dbg = phpCAS::backtrace();
- $PHPCAS_AUTH_CHECK_CALL = array('done' => TRUE,
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_AUTH_CHECK_CALL = array (
+ 'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
- 'method' => __CLASS__.'::'.__FUNCTION__,
- 'result' => $auth );
-
- if ( !$auth ) {
- phpCAS::trace('user is not authenticated, redirecting to the CAS server');
+ 'method' => __CLASS__ . '::' . __FUNCTION__,
+ 'result' => $auth
+ );
+
+ if (!$auth) {
+ phpCAS :: trace('user is not authenticated, redirecting to the CAS server');
$PHPCAS_CLIENT->forceAuthentication();
} else {
- phpCAS::trace('no need to authenticate (user `'.phpCAS::getUser().'\' is already authenticated)');
+ phpCAS :: trace('no need to authenticate (user `' . phpCAS :: getUser() . '\' is already authenticated)');
}
-
- phpCAS::traceEnd();
- return $auth;
- }
-
+
+ phpCAS :: traceEnd();
+ return $auth;
+ }
+
/**
* This method is called to renew the authentication.
**/
function renewAuthentication() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before'.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
-
+
// store where the authentication has been checked and the result
- $dbg = phpCAS::backtrace();
- $PHPCAS_AUTH_CHECK_CALL = array('done' => TRUE, 'file' => $dbg[0]['file'], 'line' => $dbg[0]['line'], 'method' => __CLASS__.'::'.__FUNCTION__, 'result' => $auth );
-
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_AUTH_CHECK_CALL = array (
+ 'done' => TRUE,
+ 'file' => $dbg[0]['file'],
+ 'line' => $dbg[0]['line'],
+ 'method' => __CLASS__ . '::' . __FUNCTION__,
+ 'result' => $auth
+ );
+
$PHPCAS_CLIENT->renewAuthentication();
- phpCAS::traceEnd();
+ phpCAS :: traceEnd();
}
/**
* This method has been left from version 0.4.1 for compatibility reasons.
*/
- function authenticate()
- {
- phpCAS::error('this method is deprecated. You should use '.__CLASS__.'::forceAuthentication() instead');
- }
-
+ function authenticate() {
+ phpCAS :: error('this method is deprecated. You should use ' . __CLASS__ . '::forceAuthentication() instead');
+ }
+
/**
* This method is called to check if the user is authenticated (previously or by
* tickets given in the URL).
*
* @return TRUE when the user is authenticated.
*/
- function isAuthenticated()
- {
+ function isAuthenticated() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
-
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
-
+
// call the isAuthenticated method of the global $PHPCAS_CLIENT object
$auth = $PHPCAS_CLIENT->isAuthenticated();
-
+
// store where the authentication has been checked and the result
- $dbg = phpCAS::backtrace();
- $PHPCAS_AUTH_CHECK_CALL = array('done' => TRUE,
+ $dbg = phpCAS :: backtrace();
+ $PHPCAS_AUTH_CHECK_CALL = array (
+ 'done' => TRUE,
'file' => $dbg[0]['file'],
'line' => $dbg[0]['line'],
- 'method' => __CLASS__.'::'.__FUNCTION__,
- 'result' => $auth );
- phpCAS::traceEnd($auth);
+ 'method' => __CLASS__ . '::' . __FUNCTION__,
+ 'result' => $auth
+ );
+ phpCAS :: traceEnd($auth);
return $auth;
- }
-
+ }
+
/**
* Checks whether authenticated based on $_SESSION. Useful to avoid
* server calls.
* @return true if authenticated, false otherwise.
* @since 0.4.22 by Brendan Arnold
*/
- function isSessionAuthenticated ()
- {
+ function isSessionAuthenticated() {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
- }
- return($PHPCAS_CLIENT->isSessionAuthenticated());
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
-
+ return ($PHPCAS_CLIENT->isSessionAuthenticated());
+ }
+
/**
* This method returns the CAS user's login name.
* @warning should not be called only after phpCAS::forceAuthentication()
@@ -1065,21 +1072,20 @@ class phpCAS *
* @return the login name of the authenticated user
*/
- function getUser()
- {
+ function getUser() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()');
+ if (!$PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::forceAuthentication() or ' . __CLASS__ . '::isAuthenticated()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['result'] ) {
- phpCAS::error('authentication was checked (by '.$PHPCAS_AUTH_CHECK_CALL['method'].'() at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].') but the method returned FALSE');
+ if (!$PHPCAS_AUTH_CHECK_CALL['result']) {
+ phpCAS :: error('authentication was checked (by ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ') but the method returned FALSE');
}
return $PHPCAS_CLIENT->getUser();
- }
-
+ }
+
/**
* This method returns the CAS user's login name.
* @warning should not be called only after phpCAS::forceAuthentication()
@@ -1087,169 +1093,160 @@ class phpCAS *
* @return the login name of the authenticated user
*/
- function getAttributes()
- {
+ function getAttributes() {
global $PHPCAS_CLIENT, $PHPCAS_AUTH_CHECK_CALL;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['done'] ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()');
+ if (!$PHPCAS_AUTH_CHECK_CALL['done']) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::forceAuthentication() or ' . __CLASS__ . '::isAuthenticated()');
}
- if ( !$PHPCAS_AUTH_CHECK_CALL['result'] ) {
- phpCAS::error('authentication was checked (by '.$PHPCAS_AUTH_CHECK_CALL['method'].'() at '.$PHPCAS_AUTH_CHECK_CALL['file'].':'.$PHPCAS_AUTH_CHECK_CALL['line'].') but the method returned FALSE');
+ if (!$PHPCAS_AUTH_CHECK_CALL['result']) {
+ phpCAS :: error('authentication was checked (by ' . $PHPCAS_AUTH_CHECK_CALL['method'] . '() at ' . $PHPCAS_AUTH_CHECK_CALL['file'] . ':' . $PHPCAS_AUTH_CHECK_CALL['line'] . ') but the method returned FALSE');
}
return $PHPCAS_CLIENT->getAttributes();
+ }
+ /**
+ * Handle logout requests.
+ */
+ function handleLogoutRequests($check_client = true, $allowed_clients = false) {
+ global $PHPCAS_CLIENT;
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
- /**
- * Handle logout requests.
- */
- function handleLogoutRequests($check_client=true, $allowed_clients=false)
- {
- global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
- }
- return($PHPCAS_CLIENT->handleLogoutRequests($check_client, $allowed_clients));
- }
-
+ return ($PHPCAS_CLIENT->handleLogoutRequests($check_client, $allowed_clients));
+ }
+
/**
* This method returns the URL to be used to login.
* or phpCAS::isAuthenticated().
*
* @return the login name of the authenticated user
*/
- function getServerLoginURL()
- {
+ function getServerLoginURL() {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
return $PHPCAS_CLIENT->getServerLoginURL();
- }
-
+ }
+
/**
* Set the login URL of the CAS server.
* @param $url the login URL
* @since 0.4.21 by Wyman Chan
*/
- function setServerLoginURL($url='')
- {
+ function setServerLoginURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after
- '.__CLASS__.'::client()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after
+ ' . __CLASS__ . '::client()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be
- `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be
+ `string\')');
}
$PHPCAS_CLIENT->setServerLoginURL($url);
- phpCAS::traceEnd();
- }
-
-
+ phpCAS :: traceEnd();
+ }
+
/**
* Set the serviceValidate URL of the CAS server.
+ * Used only in CAS 1.0 validations
* @param $url the serviceValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
- function setServerServiceValidateURL($url='')
- {
+ function setServerServiceValidateURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after
- '.__CLASS__.'::client()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after
+ ' . __CLASS__ . '::client()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be
- `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be
+ `string\')');
}
$PHPCAS_CLIENT->setServerServiceValidateURL($url);
- phpCAS::traceEnd();
- }
-
-
- /**
+ phpCAS :: traceEnd();
+ }
+
+ /**
* Set the proxyValidate URL of the CAS server.
+ * Used for all CAS 2.0 validations
* @param $url the proxyValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
- function setServerProxyValidateURL($url='')
- {
+ function setServerProxyValidateURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after
- '.__CLASS__.'::client()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after
+ ' . __CLASS__ . '::client()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be
- `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be
+ `string\')');
}
$PHPCAS_CLIENT->setServerProxyValidateURL($url);
- phpCAS::traceEnd();
- }
-
- /**
+ phpCAS :: traceEnd();
+ }
+
+ /**
* Set the samlValidate URL of the CAS server.
* @param $url the samlValidate URL
* @since 1.1.0 by Joachim Fritschi
*/
- function setServerSamlValidateURL($url='')
- {
+ function setServerSamlValidateURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after
- '.__CLASS__.'::client()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after
+ ' . __CLASS__ . '::client()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be
- `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be
+ `string\')');
}
$PHPCAS_CLIENT->setServerSamlValidateURL($url);
- phpCAS::traceEnd();
- }
-
+ phpCAS :: traceEnd();
+ }
+
/**
* This method returns the URL to be used to login.
* or phpCAS::isAuthenticated().
*
* @return the login name of the authenticated user
*/
- function getServerLogoutURL()
- {
+ function getServerLogoutURL() {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should not be called before '.__CLASS__.'::client() or '.__CLASS__.'::proxy()');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should not be called before ' . __CLASS__ . '::client() or ' . __CLASS__ . '::proxy()');
}
return $PHPCAS_CLIENT->getServerLogoutURL();
- }
-
+ }
+
/**
* Set the logout URL of the CAS server.
* @param $url the logout URL
* @since 0.4.21 by Wyman Chan
*/
- function setServerLogoutURL($url='')
- {
+ function setServerLogoutURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after
- '.__CLASS__.'::client()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after
+ ' . __CLASS__ . '::client()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be
- `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be
+ `string\')');
}
$PHPCAS_CLIENT->setServerLogoutURL($url);
- phpCAS::traceEnd();
- }
-
+ phpCAS :: traceEnd();
+ }
+
/**
* This method is used to logout from CAS.
* @params $params an array that contains the optional url and service parameters that will be passed to the CAS server
@@ -1257,66 +1254,70 @@ class phpCAS */
function logout($params = "") {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
+ phpCAS :: traceBegin();
if (!is_object($PHPCAS_CLIENT)) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
- $parsedParams = array();
+ $parsedParams = array ();
if ($params != "") {
if (is_string($params)) {
- phpCAS::error('method `phpCAS::logout($url)\' is now deprecated, use `phpCAS::logoutWithUrl($url)\' instead');
+ phpCAS :: error('method `phpCAS::logout($url)\' is now deprecated, use `phpCAS::logoutWithUrl($url)\' instead');
}
if (!is_array($params)) {
- phpCAS::error('type mismatched for parameter $params (should be `array\')');
+ phpCAS :: error('type mismatched for parameter $params (should be `array\')');
}
foreach ($params as $key => $value) {
if ($key != "service" && $key != "url") {
- phpCAS::error('only `url\' and `service\' parameters are allowed for method `phpCAS::logout($params)\'');
+ phpCAS :: error('only `url\' and `service\' parameters are allowed for method `phpCAS::logout($params)\'');
}
$parsedParams[$key] = $value;
}
}
$PHPCAS_CLIENT->logout($parsedParams);
// never reached
- phpCAS::traceEnd();
+ phpCAS :: traceEnd();
}
-
+
/**
* This method is used to logout from CAS. Halts by redirecting to the CAS server.
* @param $service a URL that will be transmitted to the CAS server
*/
function logoutWithRedirectService($service) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
if (!is_string($service)) {
- phpCAS::error('type mismatched for parameter $service (should be `string\')');
+ phpCAS :: error('type mismatched for parameter $service (should be `string\')');
}
- $PHPCAS_CLIENT->logout(array("service" => $service));
+ $PHPCAS_CLIENT->logout(array (
+ "service" => $service
+ ));
// never reached
- phpCAS::traceEnd();
+ phpCAS :: traceEnd();
}
-
+
/**
* This method is used to logout from CAS. Halts by redirecting to the CAS server.
* @param $url a URL that will be transmitted to the CAS server
*/
function logoutWithUrl($url) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
if (!is_string($url)) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
- $PHPCAS_CLIENT->logout(array("url" => $url));
+ $PHPCAS_CLIENT->logout(array (
+ "url" => $url
+ ));
// never reached
- phpCAS::traceEnd();
+ phpCAS :: traceEnd();
}
-
+
/**
* This method is used to logout from CAS. Halts by redirecting to the CAS server.
* @param $service a URL that will be transmitted to the CAS server
@@ -1324,161 +1325,156 @@ class phpCAS */
function logoutWithRedirectServiceAndUrl($service, $url) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
if (!is_string($service)) {
- phpCAS::error('type mismatched for parameter $service (should be `string\')');
+ phpCAS :: error('type mismatched for parameter $service (should be `string\')');
}
if (!is_string($url)) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
- $PHPCAS_CLIENT->logout(array("service" => $service, "url" => $url));
+ $PHPCAS_CLIENT->logout(array (
+ "service" => $service,
+ "url" => $url
+ ));
// never reached
- phpCAS::traceEnd();
+ phpCAS :: traceEnd();
}
-
+
/**
* Set the fixed URL that will be used by the CAS server to transmit the PGT.
* When this method is not called, a phpCAS script uses its own URL for the callback.
*
* @param $url the URL
*/
- function setFixedCallbackURL($url='')
- {
+ function setFixedCallbackURL($url = '') {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( !$PHPCAS_CLIENT->isProxy() ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
+ if (!$PHPCAS_CLIENT->isProxy()) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
$PHPCAS_CLIENT->setCallbackURL($url);
- phpCAS::traceEnd();
- }
-
+ phpCAS :: traceEnd();
+ }
+
/**
* Set the fixed URL that will be set as the CAS service parameter. When this
* method is not called, a phpCAS script uses its own URL.
*
* @param $url the URL
*/
- function setFixedServiceURL($url)
- {
+ function setFixedServiceURL($url) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
- }
- if ( gettype($url) != 'string' ) {
- phpCAS::error('type mismatched for parameter $url (should be `string\')');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- $PHPCAS_CLIENT->setURL($url);
- phpCAS::traceEnd();
+ if (gettype($url) != 'string') {
+ phpCAS :: error('type mismatched for parameter $url (should be `string\')');
}
-
+ $PHPCAS_CLIENT->setURL($url);
+ phpCAS :: traceEnd();
+ }
+
/**
* Get the URL that is set as the CAS service parameter.
*/
- function getServiceURL()
- {
+ function getServiceURL() {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
- }
- return($PHPCAS_CLIENT->getURL());
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
-
+ return ($PHPCAS_CLIENT->getURL());
+ }
+
/**
* Retrieve a Proxy Ticket from the CAS server.
*/
- function retrievePT($target_service,&$err_code,&$err_msg)
- {
+ function retrievePT($target_service, & $err_code, & $err_msg) {
global $PHPCAS_CLIENT;
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::proxy()');
- }
- if ( gettype($target_service) != 'string' ) {
- phpCAS::error('type mismatched for parameter $target_service(should be `string\')');
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::proxy()');
}
- return($PHPCAS_CLIENT->retrievePT($target_service,$err_code,$err_msg));
+ if (gettype($target_service) != 'string') {
+ phpCAS :: error('type mismatched for parameter $target_service(should be `string\')');
}
-
+ return ($PHPCAS_CLIENT->retrievePT($target_service, $err_code, $err_msg));
+ }
+
/**
* Set the certificate of the CAS server.
*
* @param $cert the PEM certificate
*/
- function setCasServerCert($cert)
- {
+ function setCasServerCert($cert) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
- }
- if ( gettype($cert) != 'string' ) {
- phpCAS::error('type mismatched for parameter $cert (should be `string\')');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
- $PHPCAS_CLIENT->setCasServerCert($cert);
- phpCAS::traceEnd();
+ if (gettype($cert) != 'string') {
+ phpCAS :: error('type mismatched for parameter $cert (should be `string\')');
}
-
+ $PHPCAS_CLIENT->setCasServerCert($cert);
+ phpCAS :: traceEnd();
+ }
+
/**
* Set the certificate of the CAS server CA.
*
* @param $cert the CA certificate
*/
- function setCasServerCACert($cert)
- {
+ function setCasServerCACert($cert) {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
- }
- if ( gettype($cert) != 'string' ) {
- phpCAS::error('type mismatched for parameter $cert (should be `string\')');
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
- $PHPCAS_CLIENT->setCasServerCACert($cert);
- phpCAS::traceEnd();
+ if (gettype($cert) != 'string') {
+ phpCAS :: error('type mismatched for parameter $cert (should be `string\')');
}
-
+ $PHPCAS_CLIENT->setCasServerCACert($cert);
+ phpCAS :: traceEnd();
+ }
+
/**
* Set no SSL validation for the CAS server.
*/
- function setNoCasServerValidation()
- {
+ function setNoCasServerValidation() {
global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
- }
- $PHPCAS_CLIENT->setNoCasServerValidation();
- phpCAS::traceEnd();
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
-
+ $PHPCAS_CLIENT->setNoCasServerValidation();
+ phpCAS :: traceEnd();
+ }
+
/** @} */
-
- /**
- * Change CURL options.
- * CURL is used to connect through HTTPS to CAS server
- * @param $key the option key
- * @param $value the value to set
- */
- function setExtraCurlOption($key, $value)
- {
- global $PHPCAS_CLIENT;
- phpCAS::traceBegin();
- if ( !is_object($PHPCAS_CLIENT) ) {
- phpCAS::error('this method should only be called after '.__CLASS__.'::client() or'.__CLASS__.'::proxy()');
- }
- $PHPCAS_CLIENT->setExtraCurlOption($key, $value);
- phpCAS::traceEnd();
+
+ /**
+ * Change CURL options.
+ * CURL is used to connect through HTTPS to CAS server
+ * @param $key the option key
+ * @param $value the value to set
+ */
+ function setExtraCurlOption($key, $value) {
+ global $PHPCAS_CLIENT;
+ phpCAS :: traceBegin();
+ if (!is_object($PHPCAS_CLIENT)) {
+ phpCAS :: error('this method should only be called after ' . __CLASS__ . '::client() or' . __CLASS__ . '::proxy()');
}
+ $PHPCAS_CLIENT->setExtraCurlOption($key, $value);
+ phpCAS :: traceEnd();
+ }
}
@@ -1525,7 +1521,6 @@ class phpCAS /** @defgroup publicDebug Debugging
* @ingroup public */
-
/** @defgroup internal Implementation */
/** @defgroup internalAuthentication Authentication
@@ -1579,37 +1574,37 @@ class phpCAS /**
* @example example_simple.php
*/
- /**
- * @example example_proxy.php
- */
- /**
- * @example example_proxy2.php
- */
- /**
- * @example example_lang.php
- */
- /**
- * @example example_html.php
- */
- /**
- * @example example_file.php
- */
- /**
- * @example example_db.php
- */
- /**
- * @example example_service.php
- */
- /**
- * @example example_session_proxy.php
- */
- /**
- * @example example_session_service.php
- */
- /**
- * @example example_gateway.php
- */
-
-
-
+/**
+ * @example example_proxy.php
+ */
+/**
+ * @example example_proxy2.php
+ */
+/**
+ * @example example_lang.php
+ */
+/**
+ * @example example_html.php
+ */
+/**
+ * @example example_file.php
+ */
+/**
+ * @example example_db.php
+ */
+/**
+ * @example example_service.php
+ */
+/**
+ * @example example_session_proxy.php
+ */
+/**
+ * @example example_session_service.php
+ */
+/**
+ * @example example_gateway.php
+ */
+/**
+ * @example example_custom_urls.php
+ */
?>
diff --git a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-db.php b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-db.php index 5a589e4b2..1e316b6f6 100644 --- a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-db.php +++ b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-db.php @@ -1,4 +1,32 @@ <?php
+/*
+ * Copyright © 2003-2010, The ESUP-Portail consortium & the JA-SIG Collaborative.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the ESUP-Portail consortium & the JA-SIG
+ * Collaborative nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior
+ * written permission.
+
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
/**
* @file CAS/PGTStorage/pgt-db.php
diff --git a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-file.php b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-file.php index bc07485b8..983e557c5 100644 --- a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-file.php +++ b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-file.php @@ -1,5 +1,32 @@ <?php
+/*
+ * Copyright © 2003-2010, The ESUP-Portail consortium & the JA-SIG Collaborative.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the ESUP-Portail consortium & the JA-SIG
+ * Collaborative nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior
+ * written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
/**
* @file CAS/PGTStorage/pgt-file.php
* Basic class for PGT file storage
diff --git a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-main.php b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-main.php index cd9b49967..cf4c4ed0f 100644 --- a/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-main.php +++ b/plugins/CasAuthentication/extlib/CAS/PGTStorage/pgt-main.php @@ -1,5 +1,32 @@ <?php
-
+/*
+ * Copyright © 2003-2010, The ESUP-Portail consortium & the JA-SIG Collaborative.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the ESUP-Portail consortium & the JA-SIG
+ * Collaborative nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior
+ * written permission.
+
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
/**
* @file CAS/PGTStorage/pgt-main.php
* Basic class for PGT storage
diff --git a/plugins/CasAuthentication/extlib/CAS/client.php b/plugins/CasAuthentication/extlib/CAS/client.php index ad5a23f83..d38c24d36 100644 --- a/plugins/CasAuthentication/extlib/CAS/client.php +++ b/plugins/CasAuthentication/extlib/CAS/client.php @@ -1,5 +1,34 @@ <?php +/* + * Copyright © 2003-2010, The ESUP-Portail consortium & the JA-SIG Collaborative. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the ESUP-Portail consortium & the JA-SIG + * Collaborative nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior + * written permission. + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /** * @file CAS/client.php * Main class of the phpCAS library @@ -351,8 +380,8 @@ class CASClient { return $this->_server['login_url'] = $url; } - - + + /** * This method sets the serviceValidate URL of the CAS server. * @param $url the serviceValidate URL @@ -363,8 +392,8 @@ class CASClient { return $this->_server['service_validate_url'] = $url; } - - + + /** * This method sets the proxyValidate URL of the CAS server. * @param $url the proxyValidate URL @@ -375,8 +404,8 @@ class CASClient { return $this->_server['proxy_validate_url'] = $url; } - - + + /** * This method sets the samlValidate URL of the CAS server. * @param $url the samlValidate URL @@ -387,7 +416,7 @@ class CASClient { return $this->_server['saml_validate_url'] = $url; } - + /** * This method is used to retrieve the service validating URL of the CAS server. @@ -411,24 +440,24 @@ class CASClient return $this->_server['service_validate_url'].'?service='.urlencode($this->getURL()); } /** - * This method is used to retrieve the SAML validating URL of the CAS server. - * @return a URL. - * @private - */ + * This method is used to retrieve the SAML validating URL of the CAS server. + * @return a URL. + * @private + */ function getServerSamlValidateURL() - { - phpCAS::traceBegin(); - // the URL is build only when needed - if ( empty($this->_server['saml_validate_url']) ) { - switch ($this->getServerVersion()) { - case SAML_VERSION_1_1: - $this->_server['saml_validate_url'] = $this->getServerBaseURL().'samlValidate'; - break; + { + phpCAS::traceBegin(); + // the URL is build only when needed + if ( empty($this->_server['saml_validate_url']) ) { + switch ($this->getServerVersion()) { + case SAML_VERSION_1_1: + $this->_server['saml_validate_url'] = $this->getServerBaseURL().'samlValidate'; + break; } - } - phpCAS::traceEnd($this->_server['saml_validate_url'].'?TARGET='.urlencode($this->getURL())); - return $this->_server['saml_validate_url'].'?TARGET='.urlencode($this->getURL()); - } + } + phpCAS::traceEnd($this->_server['saml_validate_url'].'?TARGET='.urlencode($this->getURL())); + return $this->_server['saml_validate_url'].'?TARGET='.urlencode($this->getURL()); + } /** * This method is used to retrieve the proxy validating URL of the CAS server. * @return a URL. @@ -496,20 +525,20 @@ class CASClient { return $this->_server['logout_url'] = $url; } - + /** * An array to store extra curl options. */ var $_curl_options = array(); - + /** * This method is used to set additional user curl options. */ function setExtraCurlOption($key, $value) - { + { $this->_curl_options[$key] = $value; - } - + } + /** * This method checks to see if the request is secured via HTTPS * @return true if https, false otherwise @@ -556,45 +585,21 @@ class CASClient if (version_compare(PHP_VERSION,'5','>=') && ini_get('zend.ze1_compatibility_mode')) { phpCAS::error('phpCAS cannot support zend.ze1_compatibility_mode. Sorry.'); } + $this->_start_session = $start_session; + + if ($this->_start_session && session_id()) + { + phpCAS :: error("Another session was started before phpcas. Either disable the session" . + " handling for phpcas in the client() call or modify your application to leave" . + " session handling to phpcas"); + } // skip Session Handling for logout requests and if don't want it' - if ($start_session && !$this->isLogoutRequest()) { - phpCAS::trace("Starting session handling"); - // Check for Tickets from the CAS server - if (empty($_GET['ticket'])){ - phpCAS::trace("No ticket found"); - // only create a session if necessary - if (!isset($_SESSION)) { - phpCAS::trace("No session found, creating new session"); - session_start(); - } - }else{ - phpCAS::trace("Ticket found"); - // We have to copy any old data before renaming the session - if (isset($_SESSION)) { - phpCAS::trace("Old active session found, saving old data and destroying session"); - $old_session = $_SESSION; - session_destroy(); - }else{ - session_start(); - phpCAS::trace("Starting possible old session to copy variables"); - $old_session = $_SESSION; - session_destroy(); - } - // set up a new session, of name based on the ticket - $session_id = preg_replace('/[^\w]/','',$_GET['ticket']); - phpCAS::LOG("Session ID: " . $session_id); - session_id($session_id); - session_start(); - // restore old session vars - if(isset($old_session)){ - phpCAS::trace("Restoring old session vars"); - $_SESSION = $old_session; - } - } - }else{ - phpCAS::trace("Skipping session creation"); + if ($start_session && !$this->isLogoutRequest()) + { + phpCAS :: trace("Starting a new session"); + session_start(); } - + // are we in proxy mode ? $this->_proxy = $proxy; @@ -667,12 +672,8 @@ class CASClient } break; case CAS_VERSION_2_0: // check for a Service or Proxy Ticket - if (preg_match('/^ST-/', $ticket)) { - phpCAS::trace('ST \'' . $ticket . '\' found'); - $this->setST($ticket); - unset ($_GET['ticket']); - } else if (preg_match('/^PT-/', $ticket)) { - phpCAS::trace('PT \'' . $ticket . '\' found'); + if( preg_match('/^[SP]T-/',$ticket) ) { + phpCAS::trace('ST or PT \''.$ticket.'\' found'); $this->setPT($ticket); unset($_GET['ticket']); } else if ( !empty($ticket) ) { @@ -682,9 +683,9 @@ class CASClient break; case SAML_VERSION_1_1: // SAML just does Service Tickets if( preg_match('/^[SP]T-/',$ticket) ) { - phpCAS::trace('SA \''.$ticket.'\' found'); - $this->setSA($ticket); - unset($_GET['ticket']); + phpCAS::trace('SA \''.$ticket.'\' found'); + $this->setSA($ticket); + unset($_GET['ticket']); } else if ( !empty($ticket) ) { //ill-formed ticket, halt phpCAS::error('ill-formed ticket found in the URL (ticket=`'.htmlentities($ticket).'\')'); @@ -699,6 +700,57 @@ class CASClient // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // XX XX + // XX Session Handling XX + // XX XX + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + /** + * A variable to whether phpcas will use its own session handling. Default = true + * @hideinitializer + * @private + */ + var $_start_session = true; + + function setStartSession($session) + { + $this->_start_session = session; + } + + function getStartSession($session) + { + $this->_start_session = session; + } + + /** + * Renaming the session + */ + function renameSession($ticket) + { + phpCAS::traceBegin(); + if($this->_start_session){ + if (!empty ($this->_user)) + { + $old_session = $_SESSION; + session_destroy(); + // set up a new session, of name based on the ticket + $session_id = preg_replace('/[^\w]/', '', $ticket); + phpCAS :: trace("Session ID: ".$session_id); + session_id($session_id); + session_start(); + phpCAS :: trace("Restoring old session vars"); + $_SESSION = $old_session; + } else + { + phpCAS :: error('Session should only be renamed after successfull authentication'); + } + }else{ + phpCAS :: trace("Skipping session rename since phpCAS is not handling the session."); + } + phpCAS::traceEnd(); + } + + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // XX XX // XX AUTHENTICATION XX // XX XX // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @@ -743,8 +795,8 @@ class CASClient } return $this->_user; } - - + + /*********************************************************************************************************************** * Atrributes section @@ -760,23 +812,23 @@ class CASClient * @private */ var $_attributes = array(); - + function setAttributes($attributes) { $this->_attributes = $attributes; } - + function getAttributes() { if ( empty($this->_user) ) { // if no user is set, there shouldn't be any attributes also... phpCAS::error('this method should be used only after '.__CLASS__.'::forceAuthentication() or '.__CLASS__.'::isAuthenticated()'); } return $this->_attributes; } - + function hasAttributes() { return !empty($this->_attributes); } - + function hasAttribute($key) { return (is_array($this->_attributes) && array_key_exists($key, $this->_attributes)); } - + function getAttribute($key) { if($this->hasAttribute($key)) { return $this->_attributes[$key]; @@ -802,7 +854,7 @@ class CASClient } phpCAS::traceEnd(); } - + /** * This method is called to be sure that the user is authenticated. When not * authenticated, halt by redirecting to the CAS server; otherwise return TRUE. @@ -914,66 +966,73 @@ class CASClient */ function isAuthenticated() { - phpCAS::traceBegin(); - $res = FALSE; - $validate_url = ''; - - if ( $this->wasPreviouslyAuthenticated() ) { + phpCAS::traceBegin(); + $res = FALSE; + $validate_url = ''; + + if ( $this->wasPreviouslyAuthenticated() ) { + if($this->hasST() || $this->hasPT() || $this->hasSA()){ + // User has a additional ticket but was already authenticated + phpCAS::trace('ticket was present and will be discarded, use renewAuthenticate()'); + header('Location: '.$this->getURL()); + phpCAS::log( "Prepare redirect to remove ticket: ".$this->getURL() ); + }else{ // the user has already (previously during the session) been // authenticated, nothing to be done. phpCAS::trace('user was already authenticated, no need to look for tickets'); - $res = TRUE; } - else { - if ( $this->hasST() ) { - // if a Service Ticket was given, validate it - phpCAS::trace('ST `'.$this->getST().'\' is present'); - $this->validateST($validate_url,$text_response,$tree_response); // if it fails, it halts - phpCAS::trace('ST `'.$this->getST().'\' was validated'); - if ( $this->isProxy() ) { - $this->validatePGT($validate_url,$text_response,$tree_response); // idem - phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); - $_SESSION['phpCAS']['pgt'] = $this->getPGT(); - } - $_SESSION['phpCAS']['user'] = $this->getUser(); - $res = TRUE; - } - elseif ( $this->hasPT() ) { - // if a Proxy Ticket was given, validate it - phpCAS::trace('PT `'.$this->getPT().'\' is present'); - $this->validatePT($validate_url,$text_response,$tree_response); // note: if it fails, it halts - phpCAS::trace('PT `'.$this->getPT().'\' was validated'); - if ( $this->isProxy() ) { - $this->validatePGT($validate_url,$text_response,$tree_response); // idem - phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); - $_SESSION['phpCAS']['pgt'] = $this->getPGT(); - } - $_SESSION['phpCAS']['user'] = $this->getUser(); - $res = TRUE; - } - elseif ( $this->hasSA() ) { - // if we have a SAML ticket, validate it. - phpCAS::trace('SA `'.$this->getSA().'\' is present'); - $this->validateSA($validate_url,$text_response,$tree_response); // if it fails, it halts - phpCAS::trace('SA `'.$this->getSA().'\' was validated'); - $_SESSION['phpCAS']['user'] = $this->getUser(); - $_SESSION['phpCAS']['attributes'] = $this->getAttributes(); - $res = TRUE; - } - else { - // no ticket given, not authenticated - phpCAS::trace('no ticket found'); + $res = TRUE; + } + else { + if ( $this->hasST() ) { + // if a Service Ticket was given, validate it + phpCAS::trace('ST `'.$this->getST().'\' is present'); + $this->validateST($validate_url,$text_response,$tree_response); // if it fails, it halts + phpCAS::trace('ST `'.$this->getST().'\' was validated'); + if ( $this->isProxy() ) { + $this->validatePGT($validate_url,$text_response,$tree_response); // idem + phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); + $_SESSION['phpCAS']['pgt'] = $this->getPGT(); } - if ($res) { - // if called with a ticket parameter, we need to redirect to the app without the ticket so that CAS-ification is transparent to the browser (for later POSTS) - // most of the checks and errors should have been made now, so we're safe for redirect without masking error messages. - header('Location: '.$this->getURL()); - phpCAS::log( "Prepare redirect to : ".$this->getURL() ); + $_SESSION['phpCAS']['user'] = $this->getUser(); + $res = TRUE; + } + elseif ( $this->hasPT() ) { + // if a Proxy Ticket was given, validate it + phpCAS::trace('PT `'.$this->getPT().'\' is present'); + $this->validatePT($validate_url,$text_response,$tree_response); // note: if it fails, it halts + phpCAS::trace('PT `'.$this->getPT().'\' was validated'); + if ( $this->isProxy() ) { + $this->validatePGT($validate_url,$text_response,$tree_response); // idem + phpCAS::trace('PGT `'.$this->getPGT().'\' was validated'); + $_SESSION['phpCAS']['pgt'] = $this->getPGT(); } + $_SESSION['phpCAS']['user'] = $this->getUser(); + $res = TRUE; + } + elseif ( $this->hasSA() ) { + // if we have a SAML ticket, validate it. + phpCAS::trace('SA `'.$this->getSA().'\' is present'); + $this->validateSA($validate_url,$text_response,$tree_response); // if it fails, it halts + phpCAS::trace('SA `'.$this->getSA().'\' was validated'); + $_SESSION['phpCAS']['user'] = $this->getUser(); + $_SESSION['phpCAS']['attributes'] = $this->getAttributes(); + $res = TRUE; + } + else { + // no ticket given, not authenticated + phpCAS::trace('no ticket found'); + } + if ($res) { + // if called with a ticket parameter, we need to redirect to the app without the ticket so that CAS-ification is transparent to the browser (for later POSTS) + // most of the checks and errors should have been made now, so we're safe for redirect without masking error messages. + header('Location: '.$this->getURL()); + phpCAS::log( "Prepare redirect to : ".$this->getURL() ); } - - phpCAS::traceEnd($res); - return $res; + } + + phpCAS::traceEnd($res); + return $res; } /** @@ -1071,30 +1130,7 @@ class CASClient phpCAS::traceExit(); exit(); } - -// /** -// * This method is used to logout from CAS. -// * @param $url a URL that will be transmitted to the CAS server (to come back to when logged out) -// * @public -// */ -// function logout($url = "") { -// phpCAS::traceBegin(); -// $cas_url = $this->getServerLogoutURL(); -// // v0.4.14 sebastien.gougeon at univ-rennes1.fr -// // header('Location: '.$cas_url); -// if ( $url != "" ) { -// // Adam Moore 1.0.0RC2 -// $url = '?service=' . $url . '&url=' . $url; -// } -// header('Location: '.$cas_url . $url); -// session_unset(); -// session_destroy(); -// $this->printHTMLHeader($this->getString(CAS_STR_LOGOUT)); -// printf('<p>'.$this->getString(CAS_STR_SHOULD_HAVE_BEEN_REDIRECTED).'</p>',$cas_url); -// $this->printHTMLFooter(); -// phpCAS::traceExit(); -// exit(); -// } + /** * This method is used to logout from CAS. @@ -1114,7 +1150,7 @@ class CASClient } header('Location: '.$cas_url); phpCAS::log( "Prepare redirect to : ".$cas_url ); - + session_unset(); session_destroy(); @@ -1156,6 +1192,9 @@ class CASClient phpCAS::traceEnd(); return; } + if(!$this->_start_session){ + phpCAS::log("phpCAS can't handle logout requests if it does not manage the session."); + } phpCAS::log("Logout requested"); phpCAS::log("SAML REQUEST: ".$_POST['logoutRequest']); if ($check_client) { @@ -1177,7 +1216,7 @@ class CASClient } if (!$allowed) { phpCAS::error("Unauthorized logout request from client '".$client."'"); - printf("Unauthorized!"); + printf("Unauthorized!"); phpCAS::traceExit(); exit(); } @@ -1191,8 +1230,13 @@ class CASClient phpCAS::log("Ticket to logout: ".$ticket2logout); $session_id = preg_replace('/[^\w]/','',$ticket2logout); phpCAS::log("Session id: ".$session_id); - - // fix New session ID + + // destroy a possible application session created before phpcas + if(session_id()){ + session_unset(); + session_destroy(); + } + // fix session ID session_id($session_id); $_COOKIE[session_name()]=$session_id; $_GET[session_name()]=$session_id; @@ -1200,8 +1244,8 @@ class CASClient // Overwrite session session_start(); session_unset(); - session_destroy(); - printf("Disconnected!"); + session_destroy(); + printf("Disconnected!"); phpCAS::traceExit(); exit(); } @@ -1322,7 +1366,7 @@ class CASClient * This method is used to validate a ST; halt on failure, and sets $validate_url, * $text_reponse and $tree_response on success. These parameters are used later * by CASClient::validatePGT() for CAS proxies. - * + * Used for all CAS 1.0 validations * @param $validate_url the URL of the request to the CAS server. * @param $text_response the response of the CAS server, as is (XML text). * @param $tree_response the response of the CAS server, as a DOM XML tree. @@ -1338,7 +1382,7 @@ class CASClient $validate_url = $this->getServerServiceValidateURL().'&ticket='.$this->getST(); if ( $this->isProxy() ) { // pass the callback url for CAS proxies - $validate_url .= '&pgtUrl='.$this->getCallbackURL(); + $validate_url .= '&pgtUrl='.urlencode($this->getCallbackURL()); } // open and read the URL @@ -1434,156 +1478,160 @@ class CASClient } break; } + $this->renameSession($this->getST()); + // at this step, ST has been validated and $this->_user has been set, + phpCAS::traceEnd(TRUE); + return TRUE; + } + + // ######################################################################## + // SAML VALIDATION + // ######################################################################## + /** + * @addtogroup internalBasic + * @{ + */ + + /** + * This method is used to validate a SAML TICKET; halt on failure, and sets $validate_url, + * $text_reponse and $tree_response on success. These parameters are used later + * by CASClient::validatePGT() for CAS proxies. + * + * @param $validate_url the URL of the request to the CAS server. + * @param $text_response the response of the CAS server, as is (XML text). + * @param $tree_response the response of the CAS server, as a DOM XML tree. + * + * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). + * + * @private + */ + function validateSA($validate_url,&$text_response,&$tree_response) + { + phpCAS::traceBegin(); + + // build the URL to validate the ticket + $validate_url = $this->getServerSamlValidateURL(); + + // open and read the URL + if ( !$this->readURL($validate_url,''/*cookies*/,$headers,$text_response,$err_msg) ) { + phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'); + $this->authError('SA not validated', $validate_url, TRUE/*$no_response*/); + } + + phpCAS::trace('server version: '.$this->getServerVersion()); + // analyze the result depending on the version + switch ($this->getServerVersion()) { + case SAML_VERSION_1_1: + + // read the response of the CAS server into a DOM object + if ( !($dom = domxml_open_mem($text_response))) { + phpCAS::trace('domxml_open_mem() failed'); + $this->authError('SA not validated', + $validate_url, + FALSE/*$no_response*/, + TRUE/*$bad_response*/, + $text_response); + } + // read the root node of the XML tree + if ( !($tree_response = $dom->document_element()) ) { + phpCAS::trace('document_element() failed'); + $this->authError('SA not validated', + $validate_url, + FALSE/*$no_response*/, + TRUE/*$bad_response*/, + $text_response); + } + // insure that tag name is 'Envelope' + if ( $tree_response->node_name() != 'Envelope' ) { + phpCAS::trace('bad XML root node (should be `Envelope\' instead of `'.$tree_response->node_name().'\''); + $this->authError('SA not validated', + $validate_url, + FALSE/*$no_response*/, + TRUE/*$bad_response*/, + $text_response); + } + // check for the NameIdentifier tag in the SAML response + if ( sizeof($success_elements = $tree_response->get_elements_by_tagname("NameIdentifier")) != 0) { + phpCAS::trace('NameIdentifier found'); + $user = trim($success_elements[0]->get_content()); + phpCAS::trace('user = `'.$user.'`'); + $this->setUser($user); + $this->setSessionAttributes($text_response); + } else { + phpCAS::trace('no <NameIdentifier> tag found in SAML payload'); + $this->authError('SA not validated', + $validate_url, + FALSE/*$no_response*/, + TRUE/*$bad_response*/, + $text_response); + } + break; + } + $this->renameSession($this->getSA()); // at this step, ST has been validated and $this->_user has been set, phpCAS::traceEnd(TRUE); return TRUE; } - - // ######################################################################## - // SAML VALIDATION - // ######################################################################## - /** - * @addtogroup internalBasic - * @{ - */ - - /** - * This method is used to validate a SAML TICKET; halt on failure, and sets $validate_url, - * $text_reponse and $tree_response on success. These parameters are used later - * by CASClient::validatePGT() for CAS proxies. - * - * @param $validate_url the URL of the request to the CAS server. - * @param $text_response the response of the CAS server, as is (XML text). - * @param $tree_response the response of the CAS server, as a DOM XML tree. - * - * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). - * - * @private - */ - function validateSA($validate_url,&$text_response,&$tree_response) - { - phpCAS::traceBegin(); - - // build the URL to validate the ticket - $validate_url = $this->getServerSamlValidateURL(); - - // open and read the URL - if ( !$this->readURL($validate_url,''/*cookies*/,$headers,$text_response,$err_msg) ) { - phpCAS::trace('could not open URL \''.$validate_url.'\' to validate ('.$err_msg.')'); - $this->authError('SA not validated', $validate_url, TRUE/*$no_response*/); - } - - phpCAS::trace('server version: '.$this->getServerVersion()); - - // analyze the result depending on the version - switch ($this->getServerVersion()) { - case SAML_VERSION_1_1: - - // read the response of the CAS server into a DOM object - if ( !($dom = domxml_open_mem($text_response))) { - phpCAS::trace('domxml_open_mem() failed'); - $this->authError('SA not validated', - $validate_url, - FALSE/*$no_response*/, - TRUE/*$bad_response*/, - $text_response); - } - // read the root node of the XML tree - if ( !($tree_response = $dom->document_element()) ) { - phpCAS::trace('document_element() failed'); - $this->authError('SA not validated', - $validate_url, - FALSE/*$no_response*/, - TRUE/*$bad_response*/, - $text_response); - } - // insure that tag name is 'Envelope' - if ( $tree_response->node_name() != 'Envelope' ) { - phpCAS::trace('bad XML root node (should be `Envelope\' instead of `'.$tree_response->node_name().'\''); - $this->authError('SA not validated', - $validate_url, - FALSE/*$no_response*/, - TRUE/*$bad_response*/, - $text_response); - } - // check for the NameIdentifier tag in the SAML response - if ( sizeof($success_elements = $tree_response->get_elements_by_tagname("NameIdentifier")) != 0) { - phpCAS::trace('NameIdentifier found'); - $user = trim($success_elements[0]->get_content()); - phpCAS::trace('user = `'.$user.'`'); - $this->setUser($user); - $this->setSessionAttributes($text_response); - } else { - phpCAS::trace('no <NameIdentifier> tag found in SAML payload'); - $this->authError('SA not validated', - $validate_url, - FALSE/*$no_response*/, - TRUE/*$bad_response*/, - $text_response); - } - break; - } - - // at this step, ST has been validated and $this->_user has been set, - phpCAS::traceEnd(TRUE); - return TRUE; - } - - /** - * This method will parse the DOM and pull out the attributes from the SAML - * payload and put them into an array, then put the array into the session. - * - * @param $text_response the SAML payload. - * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). - * - * @private - */ - function setSessionAttributes($text_response) - { - phpCAS::traceBegin(); - - $result = FALSE; - - if (isset($_SESSION[SAML_ATTRIBUTES])) { - phpCAS::trace("session attrs already set."); //testbml - do we care? - } - - $attr_array = array(); - - if (($dom = domxml_open_mem($text_response))) { - $xPath = $dom->xpath_new_context(); - $xPath->xpath_register_ns('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol'); - $xPath->xpath_register_ns('saml', 'urn:oasis:names:tc:SAML:1.0:assertion'); - $nodelist = $xPath->xpath_eval("//saml:Attribute"); - $attrs = $nodelist->nodeset; - phpCAS::trace($text_response); - foreach($attrs as $attr){ - $xres = $xPath->xpath_eval("saml:AttributeValue", $attr); - $name = $attr->get_attribute("AttributeName"); - $value_array = array(); - foreach($xres->nodeset as $node){ - $value_array[] = $node->get_content(); - - } - phpCAS::trace("* " . $name . "=" . $value_array); - $attr_array[$name] = $value_array; - } - $_SESSION[SAML_ATTRIBUTES] = $attr_array; - // UGent addition... - foreach($attr_array as $attr_key => $attr_value) { - if(count($attr_value) > 1) { - $this->_attributes[$attr_key] = $attr_value; - } - else { - $this->_attributes[$attr_key] = $attr_value[0]; - } - } - $result = TRUE; - } - phpCAS::traceEnd($result); - return $result; - } + + /** + * This method will parse the DOM and pull out the attributes from the SAML + * payload and put them into an array, then put the array into the session. + * + * @param $text_response the SAML payload. + * @return bool TRUE when successfull and FALSE if no attributes a found + * + * @private + */ + function setSessionAttributes($text_response) + { + phpCAS::traceBegin(); + + $result = FALSE; + + if (isset($_SESSION[SAML_ATTRIBUTES])) { + phpCAS::trace("session attrs already set."); //testbml - do we care? + } + + $attr_array = array(); + + if (($dom = domxml_open_mem($text_response))) { + $xPath = $dom->xpath_new_context(); + $xPath->xpath_register_ns('samlp', 'urn:oasis:names:tc:SAML:1.0:protocol'); + $xPath->xpath_register_ns('saml', 'urn:oasis:names:tc:SAML:1.0:assertion'); + $nodelist = $xPath->xpath_eval("//saml:Attribute"); + if($nodelist){ + $attrs = $nodelist->nodeset; + foreach($attrs as $attr){ + $xres = $xPath->xpath_eval("saml:AttributeValue", $attr); + $name = $attr->get_attribute("AttributeName"); + $value_array = array(); + foreach($xres->nodeset as $node){ + $value_array[] = $node->get_content(); + } + $attr_array[$name] = $value_array; + } + $_SESSION[SAML_ATTRIBUTES] = $attr_array; + // UGent addition... + foreach($attr_array as $attr_key => $attr_value) { + if(count($attr_value) > 1) { + $this->_attributes[$attr_key] = $attr_value; + phpCAS::trace("* " . $attr_key . "=" . $attr_value); + } + else { + $this->_attributes[$attr_key] = $attr_value[0]; + phpCAS::trace("* " . $attr_key . "=" . $attr_value[0]); + } + } + $result = TRUE; + }else{ + phpCAS::trace("SAML Attributes are empty"); + $result = FALSE; + } + } + phpCAS::traceEnd($result); + return $result; + } /** @} */ @@ -2118,7 +2166,7 @@ class CASClient curl_setopt($ch, $key, $value); } } - + if ($this->_cas_server_cert == '' && $this->_cas_server_ca_cert == '' && !$this->_no_cas_server_validation) { phpCAS::error('one of the methods phpCAS::setCasServerCert(), phpCAS::setCasServerCACert() or phpCAS::setNoCasServerValidation() must be called.'); } @@ -2150,21 +2198,21 @@ class CASClient if ( is_array($cookies) ) { curl_setopt($ch,CURLOPT_COOKIE,implode(';',$cookies)); } - // add extra stuff if SAML - if ($this->hasSA()) { - $more_headers = array ("soapaction: http://www.oasis-open.org/committees/security", - "cache-control: no-cache", - "pragma: no-cache", - "accept: text/xml", - "connection: keep-alive", - "content-type: text/xml"); - - curl_setopt($ch, CURLOPT_HTTPHEADER, $more_headers); - curl_setopt($ch, CURLOPT_POST, 1); - $data = $this->buildSAMLPayload(); - //phpCAS::trace('SAML Payload: '.print_r($data, TRUE)); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - } + // add extra stuff if SAML + if ($this->hasSA()) { + $more_headers = array ("soapaction: http://www.oasis-open.org/committees/security", + "cache-control: no-cache", + "pragma: no-cache", + "accept: text/xml", + "connection: keep-alive", + "content-type: text/xml"); + + curl_setopt($ch, CURLOPT_HTTPHEADER, $more_headers); + curl_setopt($ch, CURLOPT_POST, 1); + $data = $this->buildSAMLPayload(); + //phpCAS::trace('SAML Payload: '.print_r($data, TRUE)); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } // perform the query $buf = curl_exec ($ch); //phpCAS::trace('CURL: Call completed. Response body is: \''.$buf.'\''); @@ -2185,39 +2233,39 @@ class CASClient phpCAS::traceEnd($res); return $res; - } - - /** - * This method is used to build the SAML POST body sent to /samlValidate URL. - * - * @return the SOAP-encased SAMLP artifact (the ticket). - * - * @private - */ - function buildSAMLPayload() - { - phpCAS::traceBegin(); - - //get the ticket - $sa = $this->getSA(); - //phpCAS::trace("SA: ".$sa); - - $body=SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST.SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE.SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE; - - phpCAS::traceEnd($body); - return ($body); - } - + } + + /** + * This method is used to build the SAML POST body sent to /samlValidate URL. + * + * @return the SOAP-encased SAMLP artifact (the ticket). + * + * @private + */ + function buildSAMLPayload() + { + phpCAS::traceBegin(); + + //get the ticket + $sa = $this->getSA(); + //phpCAS::trace("SA: ".$sa); + + $body=SAML_SOAP_ENV.SAML_SOAP_BODY.SAMLP_REQUEST.SAML_ASSERTION_ARTIFACT.$sa.SAML_ASSERTION_ARTIFACT_CLOSE.SAMLP_REQUEST_CLOSE.SAML_SOAP_BODY_CLOSE.SAML_SOAP_ENV_CLOSE; + + phpCAS::traceEnd($body); + return ($body); + } + /** * This method is the callback used by readURL method to request HTTP headers. */ var $_curl_headers = array(); function _curl_read_headers($ch, $header) - { + { $this->_curl_headers[] = $header; return strlen($header); - } - + } + /** * This method is used to access an HTTP[S] service. * @@ -2236,6 +2284,7 @@ class CASClient function serviceWeb($url,&$err_code,&$output) { phpCAS::traceBegin(); + $cookies = array(); // at first retrieve a PT $pt = $this->retrievePT($url,$err_code,$output); @@ -2248,7 +2297,8 @@ class CASClient $res = FALSE; } else { // add cookies if necessary - if ( is_array($_SESSION['phpCAS']['services'][$url]['cookies']) ) { + if ( isset($_SESSION['phpCAS']['services'][$url]['cookies']) && + is_array($_SESSION['phpCAS']['services'][$url]['cookies']) ) { foreach ( $_SESSION['phpCAS']['services'][$url]['cookies'] as $name => $val ) { $cookies[] = $name.'='.$val; } @@ -2400,29 +2450,29 @@ class CASClient function hasPT() { return !empty($this->_pt); } /** - * This method returns the SAML Ticket provided in the URL of the request. - * @return The SAML ticket. - * @private - */ - function getSA() - { return 'ST'.substr($this->_sa, 2); } - - /** - * This method stores the SAML Ticket. - * @param $sa The SAML Ticket. - * @private - */ - function setSA($sa) - { $this->_sa = $sa; } - - /** - * This method tells if a SAML Ticket was stored. - * @return TRUE if a SAML Ticket has been stored. - * @private - */ - function hasSA() - { return !empty($this->_sa); } - + * This method returns the SAML Ticket provided in the URL of the request. + * @return The SAML ticket. + * @private + */ + function getSA() + { return 'ST'.substr($this->_sa, 2); } + + /** + * This method stores the SAML Ticket. + * @param $sa The SAML Ticket. + * @private + */ + function setSA($sa) + { $this->_sa = $sa; } + + /** + * This method tells if a SAML Ticket was stored. + * @return TRUE if a SAML Ticket has been stored. + * @private + */ + function hasSA() + { return !empty($this->_sa); } + /** @} */ // ######################################################################## // PT VALIDATION @@ -2433,8 +2483,8 @@ class CASClient */ /** - * This method is used to validate a PT; halt on failure - * + * This method is used to validate a ST or PT; halt on failure + * Used for all CAS 2.0 validations * @return bool TRUE when successfull, halt otherwise by calling CASClient::authError(). * * @private @@ -2447,7 +2497,7 @@ class CASClient if ( $this->isProxy() ) { // pass the callback url for CAS proxies - $validate_url .= '&pgtUrl='.$this->getCallbackURL(); + $validate_url .= '&pgtUrl='.urlencode($this->getCallbackURL()); } // open and read the URL @@ -2514,6 +2564,7 @@ class CASClient $text_response); } + $this->renameSession($this->getPT()); // at this step, PT has been validated and $this->_user has been set, phpCAS::traceEnd(TRUE); @@ -2586,25 +2637,43 @@ class CASClient } } - $php_is_for_sissies = split("\?", $_SERVER['REQUEST_URI'], 2); - $final_uri .= $php_is_for_sissies[0]; - if(sizeof($php_is_for_sissies) > 1){ - $cgi_params = '?' . $php_is_for_sissies[1]; - } else { - $cgi_params = '?'; + $request_uri = explode('?', $_SERVER['REQUEST_URI'], 2); + $final_uri .= $request_uri[0]; + + if (isset($request_uri[1]) && $request_uri[1]) + { + $query_string = $this->removeParameterFromQueryString('ticket', $request_uri[1]); + + // If the query string still has anything left, append it to the final URI + if ($query_string !== '') + $final_uri .= "?$query_string"; + } - // remove the ticket if present in the CGI parameters - $cgi_params = preg_replace('/&ticket=[^&]*/','',$cgi_params); - $cgi_params = preg_replace('/\?ticket=[^&;]*/','?',$cgi_params); - $cgi_params = preg_replace('/\?%26/','?',$cgi_params); - $cgi_params = preg_replace('/\?&/','?',$cgi_params); - $cgi_params = preg_replace('/\?$/','',$cgi_params); - $final_uri .= $cgi_params; + + phpCAS::trace("Final URI: $final_uri"); $this->setURL($final_uri); } phpCAS::traceEnd($this->_url); return $this->_url; - } + } + + + + /** + * Removes a parameter from a query string + * + * @param string $parameterName + * @param string $queryString + * @return string + * + * @link http://stackoverflow.com/questions/1842681/regular-expression-to-remove-one-parameter-from-query-string + */ + function removeParameterFromQueryString($parameterName, $queryString) + { + $parameterName = preg_quote($parameterName); + return preg_replace("/&$parameterName(=[^&]*)?|^$parameterName(=[^&]*)?&?/", '', $queryString); + } + /** * This method sets the URL of the current request @@ -2641,7 +2710,7 @@ class CASClient phpCAS::traceBegin(); $this->printHTMLHeader($this->getString(CAS_STR_AUTHENTICATION_FAILED)); - printf($this->getString(CAS_STR_YOU_WERE_NOT_AUTHENTICATED),$this->getURL(),$_SERVER['SERVER_ADMIN']); + printf($this->getString(CAS_STR_YOU_WERE_NOT_AUTHENTICATED),htmlentities($this->getURL()),$_SERVER['SERVER_ADMIN']); phpCAS::trace('CAS URL: '.$cas_url); phpCAS::trace('Authentication failure: '.$failure); if ( $no_response ) { diff --git a/plugins/ClientSideShorten/ClientSideShortenPlugin.php b/plugins/ClientSideShorten/ClientSideShortenPlugin.php index 27a3a56f7..65e27a037 100644 --- a/plugins/ClientSideShorten/ClientSideShortenPlugin.php +++ b/plugins/ClientSideShorten/ClientSideShortenPlugin.php @@ -51,8 +51,10 @@ class ClientSideShortenPlugin extends Plugin } function onEndShowScripts($action){ - $action->inlineScript('var Notice_maxContent = ' . Notice::maxContent()); if (common_logged_in()) { + $user = common_current_user(); + $action->inlineScript('var maxNoticeLength = ' . User_urlshortener_prefs::maxNoticeLength($user)); + $action->inlineScript('var maxUrlLength = ' . User_urlshortener_prefs::maxUrlLength($user)); $action->script('plugins/ClientSideShorten/shorten.js'); } } diff --git a/plugins/ClientSideShorten/shorten.js b/plugins/ClientSideShorten/shorten.js index 856c7f05f..bdffb81e2 100644 --- a/plugins/ClientSideShorten/shorten.js +++ b/plugins/ClientSideShorten/shorten.js @@ -31,10 +31,21 @@ })(jQuery,'smartkeypress'); + function longestWordInString(string) + { + var words = string.split(/\s/); + var longestWord = 0; + for(var i=0;i<words.length;i++) + if(words[i].length > longestWord) longestWord = words[i].length; + return longestWord; + } + function shorten() { - $noticeDataText = $('#'+SN.C.S.NoticeDataText); - if(Notice_maxContent > 0 && $noticeDataText.val().length > Notice_maxContent){ + var $noticeDataText = $('#'+SN.C.S.NoticeDataText); + var noticeText = $noticeDataText.val(); + + if(noticeText.length > maxNoticeLength || longestWordInString(noticeText) > maxUrlLength) { var original = $noticeDataText.val(); shortenAjax = $.ajax({ url: $('address .url')[0].href+'/plugins/ClientSideShorten/shorten', diff --git a/plugins/Geonames/GeonamesPlugin.php b/plugins/Geonames/GeonamesPlugin.php index 310641ce6..d88014bb8 100644 --- a/plugins/Geonames/GeonamesPlugin.php +++ b/plugins/Geonames/GeonamesPlugin.php @@ -377,7 +377,7 @@ class GeonamesPlugin extends Plugin function getCache($attrs) { - $c = common_memcache(); + $c = Cache::instance(); if (empty($c)) { return null; @@ -392,7 +392,7 @@ class GeonamesPlugin extends Plugin function setCache($attrs, $loc) { - $c = common_memcache(); + $c = Cache::instance(); if (empty($c)) { return null; @@ -409,11 +409,11 @@ class GeonamesPlugin extends Plugin { $key = 'geonames:' . implode(',', array_keys($attrs)) . ':'. - common_keyize(implode(',', array_values($attrs))); + Cache::keyize(implode(',', array_values($attrs))); if ($this->cachePrefix) { return $this->cachePrefix . ':' . $key; } else { - return common_cache_key($key); + return Cache::key($key); } } diff --git a/plugins/Gravatar/GravatarPlugin.php b/plugins/Gravatar/GravatarPlugin.php index dd8ff7217..5fcc79fd5 100644 --- a/plugins/Gravatar/GravatarPlugin.php +++ b/plugins/Gravatar/GravatarPlugin.php @@ -183,7 +183,7 @@ class GravatarPlugin extends Plugin function gravatar_url($email, $size) { - $url = "http://www.gravatar.com/avatar.php?gravatar_id=". + $url = "https://secure.gravatar.com/avatar.php?gravatar_id=". md5(strtolower($email)). "&default=".urlencode(Avatar::defaultImage($size)). "&size=".$size; diff --git a/plugins/Imap/imapmanager.php b/plugins/Imap/imapmanager.php index a9e531e1a..2d27ac97a 100644 --- a/plugins/Imap/imapmanager.php +++ b/plugins/Imap/imapmanager.php @@ -63,12 +63,14 @@ class ImapManager extends IoManager } /** - * Tell the i/o master we need one instance for each supporting site - * being handled in this process. + * Tell the i/o master we need one instance globally. + * Since this is a plugin manager, the plugin class itself will + * create one instance per site. This prevents the IoMaster from + * making more instances. */ public static function multiSite() { - return IoManager::INSTANCE_PER_SITE; + return IoManager::GLOBAL_SINGLE_ONLY; } /** diff --git a/plugins/Irc/ChannelResponseChannel.php b/plugins/Irc/ChannelResponseChannel.php new file mode 100644 index 000000000..9d50b914a --- /dev/null +++ b/plugins/Irc/ChannelResponseChannel.php @@ -0,0 +1,61 @@ +<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Extend the IMChannel class to allow commands to send messages
+ * to a channel instead of PMing a user
+ *
+ * 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 Network
+ * @package StatusNet
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @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/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+class ChannelResponseChannel extends IMChannel {
+ protected $ircChannel;
+
+ /**
+ * Construct a ChannelResponseChannel
+ *
+ * @param IMplugin $imPlugin IMPlugin
+ * @param string $ircChannel IRC Channel to reply to
+ * @return ChannelResponseChannel
+ */
+ public function __construct($imPlugin, $ircChannel) {
+ $this->ircChannel = $ircChannel;
+ parent::__construct($imPlugin);
+ }
+
+ /**
+ * Send a message using the plugin
+ *
+ * @param User $user User
+ * @param string $text Message text
+ * @return void
+ */
+ public function output($user, $text) {
+ $text = $user->nickname.': ['.common_config('site', 'name') . '] ' . $text;
+ $this->imPlugin->sendMessage($this->ircChannel, $text);
+ }
+}
\ No newline at end of file diff --git a/plugins/Irc/Fake_Irc.php b/plugins/Irc/Fake_Irc.php new file mode 100644 index 000000000..d95ab0491 --- /dev/null +++ b/plugins/Irc/Fake_Irc.php @@ -0,0 +1,47 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Instead of sending IRC messages, retrieve the raw data that would be sent + * + * 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 Network + * @package StatusNet + * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com> + * @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/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class Fake_Irc extends Phergie_Driver_Streams { + public $would_be_sent = null; + + /** + * Store the components for sending a command + * + * @param string $command Command + * @param array $args Arguments + * @return void + */ + protected function send($command, $args = '') { + $this->would_be_sent = array('command' => $command, 'args' => $args); + } +}
\ No newline at end of file diff --git a/plugins/Irc/IrcPlugin.php b/plugins/Irc/IrcPlugin.php new file mode 100644 index 000000000..7a53e5cbf --- /dev/null +++ b/plugins/Irc/IrcPlugin.php @@ -0,0 +1,396 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * Send and receive notices using an IRC network + * + * PHP version 5 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category IM + * @package StatusNet + * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +// We bundle the Phergie library... +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phergie'); + +/** + * Plugin for IRC + * + * @category Plugin + * @package StatusNet + * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class IrcPlugin extends ImPlugin { + public $host = null; + public $port = null; + public $username = null; + public $realname = null; + public $nick = null; + public $password = null; + public $nickservidentifyregexp = null; + public $nickservpassword = null; + public $channels = null; + public $transporttype = null; + public $encoding = null; + public $pinginterval = null; + + public $regcheck = null; + public $unregregexp = null; + public $regregexp = null; + + public $transport = 'irc'; + protected $whiteList; + protected $fake_irc; + + /** + * Get the internationalized/translated display name of this IM service + * + * @return string Name of service + */ + public function getDisplayName() { + return _m('IRC'); + } + + /** + * Normalize a screenname for comparison + * + * @param string $screenname Screenname to normalize + * @return string An equivalent screenname in normalized form + */ + public function normalize($screenname) { + $screenname = str_replace(" ","", $screenname); + return strtolower($screenname); + } + + /** + * Get the screenname of the daemon that sends and receives messages + * + * @return string Screenname + */ + public function daemonScreenname() { + return $this->nick; + } + + /** + * Validate (ensure the validity of) a screenname + * + * @param string $screenname Screenname to validate + * @return boolean true if screenname is valid + */ + public function validate($screenname) { + if (preg_match('/\A[a-z0-9\-_]{1,1000}\z/i', $screenname)) { + return true; + } else { + return false; + } + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * @return boolean hook value; true means continue processing, false means stop. + */ + public function onAutoload($cls) { + $dir = dirname(__FILE__); + + switch ($cls) { + case 'IrcManager': + include_once $dir . '/'.strtolower($cls).'.php'; + return false; + case 'Fake_Irc': + case 'Irc_waiting_message': + case 'ChannelResponseChannel': + include_once $dir . '/'. $cls .'.php'; + return false; + default: + if (substr($cls, 0, 7) == 'Phergie') { + include_once str_replace('_', DIRECTORY_SEPARATOR, $cls) . '.php'; + return false; + } + return true; + } + } + + /* + * Start manager on daemon start + * + * @param array &$versions Array to insert manager into + * @return boolean + */ + public function onStartImDaemonIoManagers(&$classes) { + parent::onStartImDaemonIoManagers(&$classes); + $classes[] = new IrcManager($this); // handles sending/receiving + return true; + } + + /** + * Ensure the database table is present + * + */ + public function onCheckSchema() { + $schema = Schema::get(); + + // For storing messages while sessions become ready + $schema->ensureTable('irc_waiting_message', + array(new ColumnDef('id', 'integer', null, + false, 'PRI', null, null, true), + new ColumnDef('data', 'blob', null, false), + new ColumnDef('prioritise', 'tinyint', 1, false), + new ColumnDef('attempts', 'integer', null, false), + new ColumnDef('created', 'datetime', null, false), + new ColumnDef('claimed', 'datetime'))); + + return true; + } + + /** + * Get a microid URI for the given screenname + * + * @param string $screenname Screenname + * @return string microid URI + */ + public function microiduri($screenname) { + return 'irc:' . $screenname; + } + + /** + * Send a message to a given screenname + * + * @param string $screenname Screenname to send to + * @param string $body Text to send + * @return boolean true on success + */ + public function sendMessage($screenname, $body) { + $lines = explode("\n", $body); + foreach ($lines as $line) { + $this->fake_irc->doPrivmsg($screenname, $line); + $this->enqueueOutgoingRaw(array('type' => 'message', 'prioritise' => 0, 'data' => $this->fake_irc->would_be_sent)); + } + return true; + } + + /** + * Accept a queued input message. + * + * @return boolean true if processing completed, false if message should be reprocessed + */ + public function receiveRawMessage($data) { + if (strpos($data['source'], '#') === 0) { + $message = $data['message']; + $parts = explode(' ', $message, 2); + $command = $parts[0]; + if (in_array($command, $this->whiteList)) { + $this->handle_channel_incoming($data['sender'], $data['source'], $message); + } else { + $this->handleIncoming($data['sender'], $message); + } + } else { + $this->handleIncoming($data['sender'], $data['message']); + } + return true; + } + + /** + * Helper for handling incoming messages from a channel requiring response + * to the channel instead of via PM + * + * @param string $nick Screenname the message was sent from + * @param string $channel Channel the message originated from + * @param string $message Message text + * @param boolean true on success + */ + protected function handle_channel_incoming($nick, $channel, $notice_text) { + $user = $this->getUser($nick); + // For common_current_user to work + global $_cur; + $_cur = $user; + + if (!$user) { + $this->sendFromSite($nick, 'Unknown user; go to ' . + common_local_url('imsettings') . + ' to add your address to your account'); + common_log(LOG_WARNING, 'Message from unknown user ' . $nick); + return; + } + if ($this->handle_channel_command($user, $channel, $notice_text)) { + common_log(LOG_INFO, "Command message by $nick handled."); + return; + } else if ($this->isAutoreply($notice_text)) { + common_log(LOG_INFO, 'Ignoring auto reply from ' . $nick); + return; + } else if ($this->isOtr($notice_text)) { + common_log(LOG_INFO, 'Ignoring OTR from ' . $nick); + return; + } else { + common_log(LOG_INFO, 'Posting a notice from ' . $user->nickname); + $this->addNotice($nick, $user, $notice_text); + } + + $user->free(); + unset($user); + unset($_cur); + unset($message); + } + + /** + * Attempt to handle a message from a channel as a command + * + * @param User $user User the message is from + * @param string $channel Channel the message originated from + * @param string $body Message text + * @return boolean true if the message was a command and was executed, false if it was not a command + */ + protected function handle_channel_command($user, $channel, $body) { + $inter = new CommandInterpreter(); + $cmd = $inter->handle_command($user, $body); + if ($cmd) { + $chan = new ChannelResponseChannel($this, $channel); + $cmd->execute($chan); + return true; + } else { + return false; + } + } + + /** + * Send a confirmation code to a user + * + * @param string $screenname screenname sending to + * @param string $code the confirmation code + * @param User $user user sending to + * @return boolean success value + */ + public function sendConfirmationCode($screenname, $code, $user, $checked = false) { + $body = sprintf(_('User "%s" on %s has said that your %s screenname belongs to them. ' . + 'If that\'s true, you can confirm by clicking on this URL: ' . + '%s' . + ' . (If you cannot click it, copy-and-paste it into the ' . + 'address bar of your browser). If that user isn\'t you, ' . + 'or if you didn\'t request this confirmation, just ignore this message.'), + $user->nickname, common_config('site', 'name'), $this->getDisplayName(), common_local_url('confirmaddress', array('code' => $code))); + + if ($this->regcheck && !$checked) { + return $this->checked_sendConfirmationCode($screenname, $code, $user); + } else { + return $this->sendMessage($screenname, $body); + } + } + + /** + * Only sends the confirmation message if the nick is + * registered + * + * @param string $screenname Screenname sending to + * @param string $code The confirmation code + * @param User $user User sending to + * @return boolean true on succes + */ + public function checked_sendConfirmationCode($screenname, $code, $user) { + $this->fake_irc->doPrivmsg('NickServ', 'INFO '.$screenname); + $this->enqueueOutgoingRaw( + array( + 'type' => 'nickcheck', + 'prioritise' => 1, + 'data' => $this->fake_irc->would_be_sent, + 'nickdata' => + array( + 'screenname' => $screenname, + 'code' => $code, + 'user' => $user + ) + ) + ); + return true; + } + + /** + * Initialize plugin + * + * @return boolean + */ + public function initialize() { + if (!isset($this->host)) { + throw new Exception('must specify a host'); + } + if (!isset($this->username)) { + throw new Exception('must specify a username'); + } + if (!isset($this->realname)) { + throw new Exception('must specify a "real name"'); + } + if (!isset($this->nick)) { + throw new Exception('must specify a nickname'); + } + + if (!isset($this->port)) { + $this->port = 6667; + } + if (!isset($this->transporttype)) { + $this->transporttype = 'tcp'; + } + if (!isset($this->encoding)) { + $this->encoding = 'UTF-8'; + } + if (!isset($this->pinginterval)) { + $this->pinginterval = 120; + } + + if (!isset($this->regcheck)) { + $this->regcheck = true; + } + + $this->fake_irc = new Fake_Irc; + + /* + * Commands allowed to return output to a channel + */ + $this->whiteList = array('stats', 'last', 'get'); + + return true; + } + + /** + * Get plugin information + * + * @param array $versions Array to insert information into + * @return void + */ + public function onPluginVersion(&$versions) { + $versions[] = array('name' => 'IRC', + 'version' => STATUSNET_VERSION, + 'author' => 'Luke Fitzgerald', + 'homepage' => 'http://status.net/wiki/Plugin:IRC', + 'rawdescription' => + _m('The IRC plugin allows users to send and receive notices over an IRC network.')); + return true; + } +} diff --git a/plugins/Irc/Irc_waiting_message.php b/plugins/Irc/Irc_waiting_message.php new file mode 100644 index 000000000..59eec63d8 --- /dev/null +++ b/plugins/Irc/Irc_waiting_message.php @@ -0,0 +1,142 @@ +<?php
+/**
+ * Table Definition for irc_waiting_message
+ */
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+class Irc_waiting_message extends Memcached_DataObject {
+
+ public $__table = 'irc_waiting_message'; // table name
+ public $id; // int primary_key not_null auto_increment
+ public $data; // blob not_null
+ public $prioritise; // tinyint(1) not_null
+ public $attempts; // int not_null
+ public $created; // datetime() not_null
+ public $claimed; // datetime()
+
+ /* Static get */
+ public function staticGet($k, $v = null) {
+ return Memcached_DataObject::staticGet('Irc_waiting_message', $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+ public function table() {
+ return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'data' => DB_DATAOBJECT_BLOB + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'prioritise' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'created' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'claimed' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR);
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has, since it
+ * won't appear in StatusNet's own keys list. In most cases, this will
+ * simply reference your keyTypes() function.
+ *
+ * @return array list of key field names
+ */
+ public function keys() {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them. This key information is used to store and clear
+ * cached data, so be sure to list any key that will be used for static
+ * lookups.
+ *
+ * @return array associative array of key definitions, field name to type:
+ * 'K' for primary key: for compound keys, add an entry for each component;
+ * 'U' for unique keys: compound keys are not well supported here.
+ */
+ public function keyTypes() {
+ return array('id' => 'K');
+ }
+
+ /**
+ * Magic formula for non-autoincrementing integer primary keys
+ *
+ * If a table has a single integer column as its primary key, DB_DataObject
+ * assumes that the column is auto-incrementing and makes a sequence table
+ * to do this incrementation. Since we don't need this for our class, we
+ * overload this method and return the magic formula that DB_DataObject needs.
+ *
+ * @return array magic three-false array that stops auto-incrementing.
+ */
+ public function sequenceKey() {
+ return array(false, false, false);
+ }
+
+ /**
+ * Get the next item in the queue
+ *
+ * @return Irc_waiting_message Next message if there is one
+ */
+ public static function top() {
+ $wm = new Irc_waiting_message();
+
+ $wm->orderBy('prioritise DESC, created');
+ $wm->whereAdd('claimed is null');
+
+ $wm->limit(1);
+
+ $cnt = $wm->find(true);
+
+ if ($cnt) {
+ # XXX: potential race condition
+ # can we force it to only update if claimed is still null
+ # (or old)?
+ common_log(LOG_INFO, 'claiming IRC waiting message id = ' . $wm->id);
+ $orig = clone($wm);
+ $wm->claimed = common_sql_now();
+ $result = $wm->update($orig);
+ if ($result) {
+ common_log(LOG_INFO, 'claim succeeded.');
+ return $wm;
+ } else {
+ common_log(LOG_INFO, 'claim failed.');
+ }
+ }
+ $wm = null;
+ return null;
+ }
+
+ /**
+ * Increment the attempts count
+ *
+ * @return void
+ * @throws Exception
+ */
+ public function incAttempts() {
+ $orig = clone($this);
+ $this->attempts++;
+ $result = $this->update($orig);
+
+ if (!$result) {
+ throw Exception(sprintf(_m("Could not increment attempts count for %d"), $this->id));
+ }
+ }
+
+ /**
+ * Release a claimed item.
+ */
+ public function releaseClaim() {
+ // DB_DataObject doesn't let us save nulls right now
+ $sql = sprintf("UPDATE irc_waiting_message SET claimed=NULL WHERE id=%d", $this->id);
+ $this->query($sql);
+
+ $this->claimed = null;
+ $this->encache();
+ }
+}
diff --git a/plugins/Irc/README b/plugins/Irc/README new file mode 100644 index 000000000..0a5d9ea83 --- /dev/null +++ b/plugins/Irc/README @@ -0,0 +1,45 @@ +The IRC plugin allows users to send and receive notices over an IRC network. + +Installation +============ +add "addPlugin('irc', + array('setting'=>'value', 'setting2'=>'value2', ...);" +to the bottom of your config.php + +scripts/imdaemon.php included with StatusNet must be running. It will be started by +the plugin along with their other daemons when you run scripts/startdaemons.sh. +See the StatusNet README for more about queuing and daemons. + +Settings +======== +host*: Hostname of IRC server +port: Port of IRC server (defaults to 6667) +username*: Username of bot +realname*: Real name of bot +nick*: Nickname of bot +password: Password +nickservpassword: NickServ password for identification +nickservidentifyregexp: Override existing regexp matching request for identification from NickServ +channels: Channels for bot to idle in +transporttype: Set to 'ssl' to enable SSL +encoding: Set to change encoding +pinginterval: Set to change the number of seconds between pings (helps keep the connection open) + Defaults to 120 seconds +regcheck: Check user's nicknames are registered, enabled by default, set to false to disable +regregexp: Override existing regexp matching response from NickServ if nick checked is registered. + Must contain a capturing group catching the nick +unregregexp: Override existing regexp matching response from NickServ if nick checked is unregistered + Must contain a capturing group catching the nick + +* required + +Example +======= +addPlugin('irc', array( + 'host' => '...', + 'username' => '...', + 'realname' => '...', + 'nick' => '...', + 'channels' => array('#channel1', '#channel2') +)); + diff --git a/plugins/Irc/extlib/.gitignore b/plugins/Irc/extlib/.gitignore new file mode 100644 index 000000000..553fe8e25 --- /dev/null +++ b/plugins/Irc/extlib/.gitignore @@ -0,0 +1,2 @@ +Settings.php +*.db diff --git a/plugins/Irc/extlib/phergie/.gitignore b/plugins/Irc/extlib/phergie/.gitignore new file mode 100644 index 000000000..553fe8e25 --- /dev/null +++ b/plugins/Irc/extlib/phergie/.gitignore @@ -0,0 +1,2 @@ +Settings.php +*.db diff --git a/plugins/Irc/extlib/phergie/LICENSE b/plugins/Irc/extlib/phergie/LICENSE new file mode 100644 index 000000000..d7d23420a --- /dev/null +++ b/plugins/Irc/extlib/phergie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010, Phergie Development Team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of the Phergie Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/Irc/extlib/phergie/Phergie/Autoload.php b/plugins/Irc/extlib/phergie/Phergie/Autoload.php new file mode 100755 index 000000000..0004f44e2 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Autoload.php @@ -0,0 +1,81 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Autoloader for Phergie classes. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Autoload +{ + /** + * Constructor to add the base Phergie path to the include_path. + * + * @return void + */ + public function __construct() + { + $path = realpath(dirname(__FILE__) . '/..'); + $includePath = get_include_path(); + $includePathList = explode(PATH_SEPARATOR, $includePath); + if (!in_array($path, $includePathList)) { + self::addPath($path); + } + } + + /** + * Autoload callback for loading class files. + * + * @param string $class Class to load + * + * @return void + */ + public function load($class) + { + include str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php'; + } + + /** + * Registers an instance of this class as an autoloader. + * + * @return void + */ + public static function registerAutoloader() + { + spl_autoload_register(array(new self, 'load')); + } + + /** + * Add a path to the include path. + * + * @param string $path Path to add + * + * @return void + */ + public static function addPath($path) + { + set_include_path($path . PATH_SEPARATOR . get_include_path()); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Bot.php b/plugins/Irc/extlib/phergie/Phergie/Bot.php new file mode 100755 index 000000000..85e8a00fc --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Bot.php @@ -0,0 +1,390 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Composite class for other components to represent the bot. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Bot +{ + /** + * Current version of Phergie + */ + const VERSION = '2.0.1'; + + /** + * Current driver instance + * + * @var Phergie_Driver_Abstract + */ + protected $driver; + + /** + * Current configuration instance + * + * @var Phergie_Config + */ + protected $config; + + /** + * Current connection handler instance + * + * @var Phergie_Connection_Handler + */ + protected $connections; + + /** + * Current plugin handler instance + * + * @var Phergie_Plugin_Handler + */ + protected $plugins; + + /** + * Current event handler instance + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Current end-user interface instance + * + * @var Phergie_Ui_Abstract + */ + protected $ui; + + /** + * Current processor instance + * + * @var Phergie_Process_Abstract + */ + protected $processor; + + /** + * Returns a driver instance, creating one of the default class if + * none has been set. + * + * @return Phergie_Driver_Abstract + */ + public function getDriver() + { + if (empty($this->driver)) { + // Check if a driver has been defined in the configuration to use + // as the default + $config = $this->getConfig(); + if (isset($config['driver'])) { + $class = 'Phergie_Driver_' . ucfirst($config['driver']); + } else { + // Otherwise default to the Streams driver. + $class = 'Phergie_Driver_Streams'; + } + + $this->driver = new $class; + } + return $this->driver; + } + + /** + * Sets the driver instance to use. + * + * @param Phergie_Driver_Abstract $driver Driver instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setDriver(Phergie_Driver_Abstract $driver) + { + $this->driver = $driver; + return $this; + } + + /** + * Sets the configuration to use. + * + * @param Phergie_Config $config Configuration instance + * + * @return Phergie_Runner_Abstract Provides a fluent interface + */ + public function setConfig(Phergie_Config $config) + { + $this->config = $config; + return $this; + } + + /** + * Returns the entire configuration in use or the value of a specific + * configuration setting. + * + * @param string $index Optional index of a specific configuration + * setting for which the corresponding value should be returned + * @param mixed $default Value to return if no match is found for $index + * + * @return mixed Value corresponding to $index or the entire + * configuration if $index is not specified + */ + public function getConfig($index = null, $default = null) + { + if (empty($this->config)) { + $this->config = new Phergie_Config; + $this->config->read('Settings.php'); + } + if ($index !== null) { + if (isset($this->config[$index])) { + return $this->config[$index]; + } else { + return $default; + } + } + return $this->config; + } + + /** + * Returns a plugin handler instance, creating it if it does not already + * exist and using a default class if none has been set. + * + * @return Phergie_Plugin_Handler + */ + public function getPluginHandler() + { + if (empty($this->plugins)) { + $this->plugins = new Phergie_Plugin_Handler( + $this->getConfig(), + $this->getEventHandler() + ); + } + return $this->plugins; + } + + /** + * Sets the plugin handler instance to use. + * + * @param Phergie_Plugin_Handler $handler Plugin handler instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setPluginHandler(Phergie_Plugin_Handler $handler) + { + $this->plugins = $handler; + return $this; + } + + /** + * Returns an event handler instance, creating it if it does not already + * exist and using a default class if none has been set. + * + * @return Phergie_Event_Handler + */ + public function getEventHandler() + { + if (empty($this->events)) { + $this->events = new Phergie_Event_Handler; + } + return $this->events; + } + + /** + * Sets the event handler instance to use. + * + * @param Phergie_Event_Handler $handler Event handler instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setEventHandler(Phergie_Event_Handler $handler) + { + $this->events = $handler; + return $this; + } + + /** + * Returns a connection handler instance, creating it if it does not + * already exist and using a default class if none has been set. + * + * @return Phergie_Connection_Handler + */ + public function getConnectionHandler() + { + if (empty($this->connections)) { + $this->connections = new Phergie_Connection_Handler; + } + return $this->connections; + } + + /** + * Sets the connection handler instance to use. + * + * @param Phergie_Connection_Handler $handler Connection handler instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setConnectionHandler(Phergie_Connection_Handler $handler) + { + $this->connections = $handler; + return $this; + } + + /** + * Returns an end-user interface instance, creating it if it does not + * already exist and using a default class if none has been set. + * + * @return Phergie_Ui_Abstract + */ + public function getUi() + { + if (empty($this->ui)) { + $this->ui = new Phergie_Ui_Console; + } + return $this->ui; + } + + /** + * Sets the end-user interface instance to use. + * + * @param Phergie_Ui_Abstract $ui End-user interface instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setUi(Phergie_Ui_Abstract $ui) + { + $this->ui = $ui; + return $this; + } + + /** + * Returns a processer instance, creating one if none exists. + * + * @return Phergie_Process_Abstract + */ + public function getProcessor() + { + if (empty($this->processor)) { + $class = 'Phergie_Process_Standard'; + + $type = $this->getConfig('processor'); + if (!empty($type)) { + $class = 'Phergie_Process_' . ucfirst($type); + } + + $this->processor = new $class( + $this, + $this->getConfig('processor.options', array()) + ); + } + return $this->processor; + } + + /** + * Sets the processer instance to use. + * + * @param Phergie_Process_Abstract $processor Processer instance + * + * @return Phergie_Bot Provides a fluent interface + */ + public function setProcessor(Phergie_Process_Abstract $processor) + { + $this->processor = $processor; + return $this; + } + + /** + * Loads plugins into the plugin handler. + * + * @return void + */ + protected function loadPlugins() + { + $config = $this->getConfig(); + $plugins = $this->getPluginHandler(); + $ui = $this->getUi(); + + $plugins->setAutoload($config['plugins.autoload']); + foreach ($config['plugins'] as $name) { + try { + $plugin = $plugins->addPlugin($name); + $ui->onPluginLoad($name); + } catch (Phergie_Plugin_Exception $e) { + $ui->onPluginFailure($name, $e->getMessage()); + if (!empty($plugin)) { + $plugins->removePlugin($plugin); + } + } + } + } + + /** + * Configures and establishes connections to IRC servers. + * + * @return void + */ + protected function loadConnections() + { + $config = $this->getConfig(); + $driver = $this->getDriver(); + $connections = $this->getConnectionHandler(); + $plugins = $this->getPluginHandler(); + $ui = $this->getUi(); + + foreach ($config['connections'] as $data) { + $connection = new Phergie_Connection($data); + $connections->addConnection($connection); + + $ui->onConnect($data['host']); + $driver->setConnection($connection)->doConnect(); + $plugins->setConnection($connection); + $plugins->onConnect(); + } + } + + /** + * Establishes server connections and initiates an execution loop to + * continuously receive and process events. + * + * @return Phergie_Bot Provides a fluent interface + */ + public function run() + { + set_time_limit(0); + + $timezone = $this->getConfig('timezone', 'UTC'); + date_default_timezone_set($timezone); + + $ui = $this->getUi(); + $ui->setEnabled($this->getConfig('ui.enabled')); + + $this->loadPlugins(); + $this->loadConnections(); + + $processor = $this->getProcessor(); + + $connections = $this->getConnectionHandler(); + while (count($connections)) { + $processor->handleEvents(); + } + + $ui->onShutdown(); + + return $this; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Config.php b/plugins/Irc/extlib/phergie/Phergie/Config.php new file mode 100755 index 000000000..c182f2ac1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Config.php @@ -0,0 +1,186 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Reads from and writes to PHP configuration files and provides access to + * the settings they contain. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Config implements ArrayAccess +{ + /** + * Mapping of configuration file paths to an array of names of settings + * they contain + * + * @var array + */ + protected $files = array(); + + /** + * Mapping of setting names to their current corresponding values + * + * @var array + */ + protected $settings = array(); + + /** + * Includes a specified PHP configuration file and incorporates its + * return value (which should be an associative array) into the current + * configuration settings. + * + * @param string $file Path to the file to read + * + * @return Phergie_Config Provides a fluent interface + * @throws Phergie_Config_Exception + */ + public function read($file) + { + if (!(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' + && file_exists($file)) + && !is_executable($file) + ) { + throw new Phergie_Config_Exception( + 'Path "' . $file . '" does not reference an executable file', + Phergie_Config_Exception::ERR_FILE_NOT_EXECUTABLE + ); + } + + $settings = include $file; + if (!is_array($settings)) { + throw new Phergie_Config_Exception( + 'File "' . $file . '" does not return an array', + Phergie_Config_Exception::ERR_ARRAY_NOT_RETURNED + ); + } + + $this->files[$file] = array_keys($settings); + $this->settings += $settings; + + return $this; + } + + /** + * Merges an associative array of configuration setting values into the + * current configuration settings. + * + * @param array $settings Associative array of configuration setting + * values keyed by setting name + * + * @return Phergie_Config Provides a fluent interface + */ + public function readArray(array $settings) + { + $this->settings += $settings; + + return $this; + } + + /** + * Writes the values of the current configuration settings back to their + * originating files. + * + * @return Phergie_Config Provides a fluent interface + */ + public function write() + { + foreach ($this->files as $file => &$settings) { + $values = array(); + foreach ($settings as $setting) { + $values[$setting] = $this->settings[$setting]; + } + $source = '<?php' . PHP_EOL . PHP_EOL . + 'return ' . var_export($value, true) . ';'; + file_put_contents($file, $source); + } + } + + /** + * Checks to see if a configuration setting is assigned a value. + * + * @param string $offset Configuration setting name + * + * @return bool TRUE if the setting has a value, FALSE otherwise + * @see ArrayAccess::offsetExists() + */ + public function offsetExists($offset) + { + return isset($this->settings[$offset]); + } + + /** + * Returns the value of a configuration setting. + * + * @param string $offset Configuration setting name + * + * @return mixed Configuration setting value or NULL if it is not + * assigned a value + * @see ArrayAccess::offsetGet() + */ + public function offsetGet($offset) + { + if (isset($this->settings[$offset])) { + $value = &$this->settings[$offset]; + } else { + $value = null; + } + + return $value; + } + + /** + * Sets the value of a configuration setting. + * + * @param string $offset Configuration setting name + * @param mixed $value New setting value + * + * @return void + * @see ArrayAccess::offsetSet() + */ + public function offsetSet($offset, $value) + { + $this->settings[$offset] = $value; + } + + /** + * Removes the value set for a configuration setting. + * + * @param string $offset Configuration setting name + * + * @return void + * @see ArrayAccess::offsetUnset() + */ + public function offsetUnset($offset) + { + unset($this->settings[$offset]); + + foreach ($this->files as $file => $settings) { + $key = array_search($offset, $settings); + if ($key !== false) { + unset($this->files[$file][$key]); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Config/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Config/Exception.php new file mode 100644 index 000000000..fb646c10c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Config/Exception.php @@ -0,0 +1,44 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to configuration. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Config_Exception extends Phergie_Exception +{ + /** + * Error indicating that an attempt was made to read a configuration + * file that could not be executed + */ + const ERR_FILE_NOT_EXECUTABLE = 1; + + /** + * Error indicating that a read configuration file does not return an + * array + */ + const ERR_ARRAY_NOT_RETURNED = 2; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection.php b/plugins/Irc/extlib/phergie/Phergie/Connection.php new file mode 100755 index 000000000..746dec05f --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Connection.php @@ -0,0 +1,401 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Data structure for connection metadata. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Connection +{ + /** + * Host to which the client will connect + * + * @var string + */ + protected $host; + + /** + * Port on which the client will connect, defaults to the standard IRC + * port + * + * @var int + */ + protected $port; + + /** + * Transport for the connection, defaults to tcp but can be set to ssl + * or variations thereof to connect over SSL + * + * @var string + */ + protected $transport; + + /** + * Encoding method for the connection, defaults to ISO-8859-1 but can + * be set to UTF8 if necessary + * + * @var strng + */ + protected $encoding; + + /** + * Nick that the client will use + * + * @var string + */ + protected $nick; + + /** + * Username that the client will use + * + * @var string + */ + protected $username; + + /** + * Realname that the client will use + * + * @var string + */ + protected $realname; + + /** + * Password that the client will use + * + * @var string + */ + protected $password; + + /** + * Hostmask for the connection + * + * @var Phergie_Hostmask + */ + protected $hostmask; + + /** + * Constructor to initialize instance properties. + * + * @param array $options Optional associative array of property values + * to initialize + * + * @return void + */ + public function __construct(array $options = array()) + { + $this->transport = 'tcp'; + $this->encoding = 'ISO-8859-1'; + // @note this may need changed to something different, for broader support. + // @note also may need to make use of http://us.php.net/manual/en/function.stream-encoding.php + + $this->setOptions($options); + } + + /** + * Emits an error related to a required connection setting does not have + * value set for it. + * + * @param string $setting Name of the setting + * + * @return void + */ + protected function checkSetting($setting) + { + if (empty($this->$setting)) { + throw new Phergie_Connection_Exception( + 'Required connection setting "' . $setting . '" missing', + Phergie_Connection_Exception::ERR_REQUIRED_SETTING_MISSING + ); + } + } + + /** + * Returns a hostmask that uniquely identifies the connection. + * + * @return string + */ + public function getHostmask() + { + if (empty($this->hostmask)) { + $this->hostmask = new Phergie_Hostmask( + $this->getNick(), + $this->getUsername(), + $this->getHost() + ); + } + + return $this->hostmask; + } + + /** + * Sets the host to which the client will connect. + * + * @param string $host Hostname + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setHost($host) + { + if (empty($this->host)) { + $this->host = (string) $host; + } + + return $this; + } + + /** + * Returns the host to which the client will connect if it is set or + * emits an error if it is not set. + * + * @return string + */ + public function getHost() + { + $this->checkSetting('host'); + + return $this->host; + } + + /** + * Sets the port on which the client will connect. + * + * @param int $port Port + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setPort($port) + { + if (empty($this->port)) { + $this->port = (int) $port; + } + + return $this; + } + + /** + * Returns the port on which the client will connect. + * + * @return int + */ + public function getPort() + { + if (empty($this->port)) { + $this->port = 6667; + } + + return $this->port; + } + + /** + * Sets the transport for the connection to use. + * + * @param string $transport Transport (ex: tcp, ssl, etc.) + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setTransport($transport) + { + $this->transport = (string) $transport; + + if (!in_array($this->transport, stream_get_transports())) { + throw new Phergie_Connection_Exception( + 'Transport ' . $this->transport . ' is not supported', + Phergie_Connection_Exception::ERR_TRANSPORT_NOT_SUPPORTED + ); + } + + return $this; + } + + /** + * Returns the transport in use by the connection. + * + * @return string Transport (ex: tcp, ssl, etc.) + */ + public function getTransport() + { + return $this->transport; + } + + /** + * Sets the encoding for the connection to use. + * + * @param string $encoding Encoding to use (ex: ASCII, ISO-8859-1, UTF8, etc.) + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setEncoding($encoding) + { + $this->encoding = (string) $encoding; + + if (!in_array($this->encoding, mb_list_encodings())) { + throw new Phergie_Connection_Exception( + 'Encoding ' . $this->encoding . ' is not supported', + Phergie_Connection_Exception::ERR_ENCODING_NOT_SUPPORTED + ); + } + + return $this; + } + + /** + * Returns the encoding in use by the connection. + * + * @return string Encoding (ex: ASCII, ISO-8859-1, UTF8, etc.) + */ + public function getEncoding() + { + return $this->encoding; + } + + /** + * Sets the nick that the client will use. + * + * @param string $nick Nickname + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setNick($nick) + { + if (empty($this->nick)) { + $this->nick = (string) $nick; + } + + return $this; + } + + /** + * Returns the nick that the client will use. + * + * @return string + */ + public function getNick() + { + $this->checkSetting('nick'); + + return $this->nick; + } + + /** + * Sets the username that the client will use. + * + * @param string $username Username + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setUsername($username) + { + if (empty($this->username)) { + $this->username = (string) $username; + } + + return $this; + } + + /** + * Returns the username that the client will use. + * + * @return string + */ + public function getUsername() + { + $this->checkSetting('username'); + + return $this->username; + } + + /** + * Sets the realname that the client will use. + * + * @param string $realname Real name + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setRealname($realname) + { + if (empty($this->realname)) { + $this->realname = (string) $realname; + } + + return $this; + } + + /** + * Returns the realname that the client will use. + * + * @return string + */ + public function getRealname() + { + $this->checkSetting('realname'); + + return $this->realname; + } + + /** + * Sets the password that the client will use. + * + * @param string $password Password + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setPassword($password) + { + if (empty($this->password)) { + $this->password = (string) $password; + } + + return $this; + } + + /** + * Returns the password that the client will use. + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Sets multiple connection settings using an array. + * + * @param array $options Associative array of setting names mapped to + * corresponding values + * + * @return Phergie_Connection Provides a fluent interface + */ + public function setOptions(array $options) + { + foreach ($options as $option => $value) { + $method = 'set' . ucfirst($option); + if (method_exists($this, $method)) { + $this->$method($value); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php new file mode 100644 index 000000000..aec1cd8e0 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Connection/Exception.php @@ -0,0 +1,50 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to a connection to an IRC server. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Connection_Exception extends Phergie_Exception +{ + /** + * Error indicating that an operation was attempted requiring a value + * for a specific configuration setting, but none was set + */ + const ERR_REQUIRED_SETTING_MISSING = 1; + + /** + * Error indicating that a connection is configured to use a transport, + * but that transport is not supported by the current PHP installation + */ + const ERR_TRANSPORT_NOT_SUPPORTED = 2; + + /** + * Error indicating that a connection is configured to use an encoding, + * but that encoding is not supported by the current PHP installation + */ + const ERR_ENCODING_NOT_SUPPORTED = 3; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php new file mode 100644 index 000000000..e9aeddcd3 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Connection/Handler.php @@ -0,0 +1,130 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Handles connections initiated by the bot. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Connection_Handler implements Countable, IteratorAggregate +{ + /** + * Map of connections indexed by hostmask + * + * @var array + */ + protected $connections; + + /** + * Constructor to initialize storage for connections. + * + * @return void + */ + public function __construct() + { + $this->connections = array(); + } + + /** + * Adds a connection to the connection list. + * + * @param Phergie_Connection $connection Connection to add + * + * @return Phergie_Connection_Handler Provides a fluent interface + */ + public function addConnection(Phergie_Connection $connection) + { + $this->connections[(string) $connection->getHostmask()] = $connection; + return $this; + } + + /** + * Removes a connection from the connection list. + * + * @param Phergie_Connection|string $connection Instance or hostmask for + * the connection to remove + * + * @return Phergie_Connection_Handler Provides a fluent interface + */ + public function removeConnection($connection) + { + if ($connection instanceof Phergie_Connection) { + $hostmask = (string) $connection->getHostmask(); + } elseif (is_string($connection) + && isset($this->connections[$connection])) { + $hostmask = $connection; + } else { + return $this; + } + unset($this->connections[$hostmask]); + return $this; + } + + /** + * Returns the number of connections in the list. + * + * @return int Number of connections + */ + public function count() + { + return count($this->connections); + } + + /** + * Returns an iterator for the connection list. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->connections); + } + + /** + * Returns a list of specified connection objects. + * + * @param array|string $keys One or more hostmasks identifying the + * connections to return + * + * @return array List of Phergie_Connection objects corresponding to the + * specified hostmask(s) + */ + public function getConnections($keys) + { + $connections = array(); + + if (!is_array($keys)) { + $keys = array($keys); + } + + foreach ($keys as $key) { + if (isset($this->connections[$key])) { + $connections[] = $this->connections[$key]; + } + } + + return $connections; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php new file mode 100755 index 000000000..62736620d --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Driver/Abstract.php @@ -0,0 +1,301 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for drivers which handle issuing client commands to the IRC + * server and converting responses into usable data objects. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +abstract class Phergie_Driver_Abstract +{ + /** + * Currently active connection + * + * @var Phergie_Connection + */ + protected $connection; + + /** + * Sets the currently active connection. + * + * @param Phergie_Connection $connection Active connection + * + * @return Phergie_Driver_Abstract Provides a fluent interface + */ + public function setConnection(Phergie_Connection $connection) + { + $this->connection = $connection; + + return $this; + } + + /** + * Returns the currently active connection. + * + * @return Phergie_Connection + * @throws Phergie_Driver_Exception + */ + public function getConnection() + { + if (empty($this->connection)) { + throw new Phergie_Driver_Exception( + 'Operation requires an active connection, but none is set', + Phergie_Driver_Exception::ERR_NO_ACTIVE_CONNECTION + ); + } + + return $this->connection; + } + + /** + * Returns an event if one has been received from the server. + * + * @return Phergie_Event_Interface|null Event instance if an event has + * been received, NULL otherwise + */ + public abstract function getEvent(); + + /** + * Initiates a connection with the server. + * + * @return void + */ + public abstract function doConnect(); + + /** + * Terminates the connection with the server. + * + * @param string $reason Reason for connection termination (optional) + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_1_6 + */ + public abstract function doQuit($reason = null); + + /** + * Joins a channel. + * + * @param string $channels Comma-delimited list of channels to join + * @param string $keys Optional comma-delimited list of channel keys + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_1 + */ + public abstract function doJoin($channels, $keys = null); + + /** + * Leaves a channel. + * + * @param string $channels Comma-delimited list of channels to leave + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_2 + */ + public abstract function doPart($channels); + + /** + * Invites a user to an invite-only channel. + * + * @param string $nick Nick of the user to invite + * @param string $channel Name of the channel + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_7 + */ + public abstract function doInvite($nick, $channel); + + /** + * Obtains a list of nicks of users in specified channels. + * + * @param string $channels Comma-delimited list of one or more channels + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_5 + */ + public abstract function doNames($channels); + + /** + * Obtains a list of channel names and topics. + * + * @param string $channels Comma-delimited list of one or more channels + * to which the response should be restricted + * (optional) + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_6 + */ + public abstract function doList($channels = null); + + /** + * Retrieves or changes a channel topic. + * + * @param string $channel Name of the channel + * @param string $topic New topic to assign (optional) + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_4 + */ + public abstract function doTopic($channel, $topic = null); + + /** + * Retrieves or changes a channel or user mode. + * + * @param string $target Channel name or user nick + * @param string $mode New mode to assign (optional) + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_3 + */ + public abstract function doMode($target, $mode = null); + + /** + * Changes the client nick. + * + * @param string $nick New nick to assign + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_1_2 + */ + public abstract function doNick($nick); + + /** + * Retrieves information about a nick. + * + * @param string $nick Nick + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_5_2 + */ + public abstract function doWhois($nick); + + /** + * Sends a message to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the message to send + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_4_1 + */ + public abstract function doPrivmsg($target, $text); + + /** + * Sends a notice to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the notice to send + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_4_2 + */ + public abstract function doNotice($target, $text); + + /** + * Kicks a user from a channel. + * + * @param string $nick Nick of the user + * @param string $channel Channel name + * @param string $reason Reason for the kick (optional) + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_2_8 + */ + public abstract function doKick($nick, $channel, $reason = null); + + /** + * Responds to a server test of client responsiveness. + * + * @param string $daemon Daemon from which the original request originates + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html#c4_6_3 + */ + public abstract function doPong($daemon); + + /** + * Sends a CTCP ACTION (/me) command to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the action to perform + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.4 + */ + public abstract function doAction($target, $text); + + /** + * Sends a CTCP PING request to a user. + * + * @param string $nick User nick + * @param string $hash Hash to use in the handshake + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.2 + */ + public abstract function doPing($nick, $hash); + + /** + * Sends a CTCP VERSION request or response to a user. + * + * @param string $nick User nick + * @param string $version Version string to send for a response + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.1 + */ + public abstract function doVersion($nick, $version = null); + + /** + * Sends a CTCP TIME request to a user. + * + * @param string $nick User nick + * @param string $time Time string to send for a response + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.6 + */ + public abstract function doTime($nick, $time = null); + + /** + * Sends a CTCP FINGER request to a user. + * + * @param string $nick User nick + * @param string $finger Finger string to send for a response + * + * @return void + * @link http://www.irchelp.org/irchelp/rfc/ctcpspec.html + */ + public abstract function doFinger($nick, $finger = null); + + /** + * Sends a raw command to the server. + * + * @param string $command Command string to send + * + * @return void + */ + public abstract function doRaw($command); +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php new file mode 100755 index 000000000..5873b2cb9 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Driver/Exception.php @@ -0,0 +1,59 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to driver operations. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Driver_Exception extends Phergie_Exception +{ + /** + * Error indicating that an operation was requested requiring an active + * connection before one had been set + */ + const ERR_NO_ACTIVE_CONNECTION = 1; + + /** + * Error indicating that an operation was requested requiring an active + * connection where one had been set but not initiated + */ + const ERR_NO_INITIATED_CONNECTION = 2; + + /** + * Error indicating that an attempt to initiate a connection failed + */ + const ERR_CONNECTION_ATTEMPT_FAILED = 3; + + /** + * Error indicating that an attempt to send data via a connection failed + */ + const ERR_CONNECTION_WRITE_FAILED = 4; + + /** + * Error indicating that an attempt to read data via a connection failed + */ + const ERR_CONNECTION_READ_FAILED = 5; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php new file mode 100644 index 000000000..84c85a01c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Driver/Statusnet.php @@ -0,0 +1,66 @@ +<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ *
+ * 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/>.
+ *
+ * Extends the Streams driver (Phergie_Driver_Streams) to give external access
+ * to the socket resources and send method
+ *
+ * @category Phergie
+ * @package Phergie_Driver_Statusnet
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class Phergie_Driver_Statusnet extends Phergie_Driver_Streams {
+ /**
+ * Handles construction of command strings and their transmission to the
+ * server.
+ *
+ * @param string $command Command to send
+ * @param string|array $args Optional string or array of sequential
+ * arguments
+ *
+ * @return string Command string that was sent
+ * @throws Phergie_Driver_Exception
+ */
+ public function send($command, $args = '') {
+ return parent::send($command, $args);
+ }
+
+ public function forceQuit() {
+ try {
+ // Send a QUIT command to the server
+ $this->send('QUIT', 'Reconnecting');
+ } catch (Phergie_Driver_Exception $e){}
+
+ // Terminate the socket connection
+ fclose($this->socket);
+
+ // Remove the socket from the internal socket list
+ unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
+ }
+
+ /**
+ * Returns the array of sockets
+ *
+ * @return array Array of socket resources
+ */
+ public function getSockets() {
+ return $this->sockets;
+ }
+}
\ No newline at end of file diff --git a/plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php b/plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php new file mode 100755 index 000000000..73c0230c7 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php @@ -0,0 +1,729 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Driver that uses the sockets wrapper of the streams extension for + * communicating with the server and handles formatting and parsing of + * events using PHP. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Driver_Streams extends Phergie_Driver_Abstract +{ + /** + * Socket handlers + * + * @var array + */ + protected $sockets = array(); + + /** + * Reference to the currently active socket handler + * + * @var resource + */ + protected $socket; + + /** + * Amount of time in seconds to wait to receive an event each time the + * socket is polled + * + * @var float + */ + protected $timeout = 0.1; + + /** + * Handles construction of command strings and their transmission to the + * server. + * + * @param string $command Command to send + * @param string|array $args Optional string or array of sequential + * arguments + * + * @return string Command string that was sent + * @throws Phergie_Driver_Exception + */ + protected function send($command, $args = '') + { + $connection = $this->getConnection(); + $encoding = $connection->getEncoding(); + + // Require an open socket connection to continue + if (empty($this->socket)) { + throw new Phergie_Driver_Exception( + 'doConnect() must be called first', + Phergie_Driver_Exception::ERR_NO_INITIATED_CONNECTION + ); + } + + // Add the command + $buffer = strtoupper($command); + + // Add arguments + if (!empty($args)) { + + // Apply formatting if arguments are passed in as an array + if (is_array($args)) { + $end = count($args) - 1; + $args[$end] = ':' . $args[$end]; + $args = implode(' ', $args); + } else { + $args = ':' . $args; + } + + $buffer .= ' ' . $args; + } + + // Transmit the command over the socket connection + $attempts = $written = 0; + $temp = $buffer . "\r\n"; + $is_multibyte = !substr($encoding, 0, 8) === 'ISO-8859' && $encoding !== 'ASCII' && $encoding !== 'CP1252'; + $length = ($is_multibyte) ? mb_strlen($buffer, '8bit') : strlen($buffer); + while (true) { + $written += (int) fwrite($this->socket, $temp); + if ($written < $length) { + $temp = substr($temp, $written); + $attempts++; + if ($attempts == 3) { + throw new Phergie_Driver_Exception( + 'Unable to write to socket', + Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED + ); + } + } else { + break; + } + } + + // Return the command string that was transmitted + return $buffer; + } + + /** + * Overrides the parent class to set the currently active socket handler + * when the active connection is changed. + * + * @param Phergie_Connection $connection Active connection + * + * @return Phergie_Driver_Streams Provides a fluent interface + */ + public function setConnection(Phergie_Connection $connection) + { + // Set the active socket handler + $hostmask = (string) $connection->getHostmask(); + if (!empty($this->sockets[$hostmask])) { + $this->socket = $this->sockets[$hostmask]; + } + + // Set the active connection + return parent::setConnection($connection); + } + + /** + * Returns a list of hostmasks corresponding to sockets with data to read. + * + * @param int $sec Length of time to wait for new data (seconds) + * @param int $usec Length of time to wait for new data (microseconds) + * + * @return array List of hostmasks or an empty array if none were found + * to have data to read + */ + public function getActiveReadSockets($sec = 0, $usec = 200000) + { + $read = $this->sockets; + $write = null; + $error = null; + $active = array(); + + if (count($this->sockets) > 0) { + $number = stream_select($read, $write, $error, $sec, $usec); + if ($number > 0) { + foreach ($read as $item) { + $active[] = array_search($item, $this->sockets); + } + } + } + + return $active; + } + + /** + * Sets the amount of time to wait for a new event each time the socket + * is polled. + * + * @param float $timeout Amount of time in seconds + * + * @return Phergie_Driver_Streams Provides a fluent interface + */ + public function setTimeout($timeout) + { + $timeout = (float) $timeout; + if ($timeout) { + $this->timeout = $timeout; + } + return $this; + } + + /** + * Returns the amount of time to wait for a new event each time the + * socket is polled. + * + * @return float Amount of time in seconds + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Supporting method to parse event argument strings where the last + * argument may contain a colon. + * + * @param string $args Argument string to parse + * @param int $count Optional maximum number of arguments + * + * @return array Array of argument values + */ + protected function parseArguments($args, $count = -1) + { + return preg_split('/ :?/S', $args, $count); + } + + /** + * Listens for an event on the current connection. + * + * @return Phergie_Event_Interface|null Event instance if an event was + * received, NULL otherwise + */ + public function getEvent() + { + // Check the socket is still active + if (feof($this->socket)) { + throw new Phergie_Driver_Exception( + 'EOF detected on socket', + Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED + ); + } + + // Check for a new event on the current connection + $buffer = fgets($this->socket, 512); + + // If no new event was found, return NULL + if (empty($buffer)) { + return null; + } + + // Strip the trailing newline from the buffer + $buffer = rtrim($buffer); + + // If the event is from the server... + if (substr($buffer, 0, 1) != ':') { + + // Parse the command and arguments + list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null); + $hostmask = new Phergie_Hostmask(null, null, $this->connection->getHost()); + + } else { + // If the event could be from the server or a user... + + // Parse the server hostname or user hostmask, command, and arguments + list($prefix, $cmd, $args) + = array_pad(explode(' ', ltrim($buffer, ':'), 3), 3, null); + if (strpos($prefix, '@') !== false) { + $hostmask = Phergie_Hostmask::fromString($prefix); + } else { + $hostmask = new Phergie_Hostmask(null, null, $prefix); + } + } + + // Parse the event arguments depending on the event type + $cmd = strtolower($cmd); + switch ($cmd) { + case 'names': + case 'nick': + case 'quit': + case 'ping': + case 'join': + case 'error': + $args = array(ltrim($args, ':')); + break; + + case 'privmsg': + case 'notice': + $args = $this->parseArguments($args, 2); + list($source, $ctcp) = $args; + if (substr($ctcp, 0, 1) === "\001" && substr($ctcp, -1) === "\001") { + $ctcp = substr($ctcp, 1, -1); + $reply = ($cmd == 'notice'); + list($cmd, $args) = array_pad(explode(' ', $ctcp, 2), 2, null); + $cmd = strtolower($cmd); + switch ($cmd) { + case 'version': + case 'time': + case 'finger': + if ($reply) { + $args = $ctcp; + } + break; + case 'ping': + if ($reply) { + $cmd .= 'Response'; + } else { + $cmd = 'ctcpPing'; + } + break; + case 'action': + $args = array($source, $args); + break; + + default: + $cmd = 'ctcp'; + if ($reply) { + $cmd .= 'Response'; + } + $args = array($source, $args); + break; + } + } + break; + + case 'oper': + case 'topic': + case 'mode': + $args = $this->parseArguments($args); + break; + + case 'part': + case 'kill': + case 'invite': + $args = $this->parseArguments($args, 2); + break; + + case 'kick': + $args = $this->parseArguments($args, 3); + break; + + // Remove the target from responses + default: + $args = substr($args, strpos($args, ' ') + 1); + break; + } + + // Create, populate, and return an event object + if (ctype_digit($cmd)) { + $event = new Phergie_Event_Response; + $event + ->setCode($cmd) + ->setDescription($args); + } else { + $event = new Phergie_Event_Request; + $event + ->setType($cmd) + ->setArguments($args); + if (isset($hostmask)) { + $event->setHostmask($hostmask); + } + } + $event->setRawData($buffer); + return $event; + } + + /** + * Initiates a connection with the server. + * + * @return void + */ + public function doConnect() + { + // Listen for input indefinitely + set_time_limit(0); + + // Get connection information + $connection = $this->getConnection(); + $hostname = $connection->getHost(); + $port = $connection->getPort(); + $password = $connection->getPassword(); + $username = $connection->getUsername(); + $nick = $connection->getNick(); + $realname = $connection->getRealname(); + $transport = $connection->getTransport(); + + // Establish and configure the socket connection + $remote = $transport . '://' . $hostname . ':' . $port; + $this->socket = @stream_socket_client($remote, $errno, $errstr); + if (!$this->socket) { + throw new Phergie_Driver_Exception( + 'Unable to connect: socket error ' . $errno . ' ' . $errstr, + Phergie_Driver_Exception::ERR_CONNECTION_ATTEMPT_FAILED + ); + } + + $seconds = (int) $this->timeout; + $microseconds = ($this->timeout - $seconds) * 1000000; + stream_set_timeout($this->socket, $seconds, $microseconds); + + // Send the password if one is specified + if (!empty($password)) { + $this->send('PASS', $password); + } + + // Send user information + $this->send( + 'USER', + array( + $username, + $hostname, + $hostname, + $realname + ) + ); + + $this->send('NICK', $nick); + + // Add the socket handler to the internal array for socket handlers + $this->sockets[(string) $connection->getHostmask()] = $this->socket; + } + + /** + * Terminates the connection with the server. + * + * @param string $reason Reason for connection termination (optional) + * + * @return void + */ + public function doQuit($reason = null) + { + // Send a QUIT command to the server + $this->send('QUIT', $reason); + + // Terminate the socket connection + fclose($this->socket); + + // Remove the socket from the internal socket list + unset($this->sockets[(string) $this->getConnection()->getHostmask()]); + } + + /** + * Joins a channel. + * + * @param string $channels Comma-delimited list of channels to join + * @param string $keys Optional comma-delimited list of channel keys + * + * @return void + */ + public function doJoin($channels, $keys = null) + { + $args = array($channels); + + if (!empty($keys)) { + $args[] = $keys; + } + + $this->send('JOIN', $args); + } + + /** + * Leaves a channel. + * + * @param string $channels Comma-delimited list of channels to leave + * + * @return void + */ + public function doPart($channels) + { + $this->send('PART', $channels); + } + + /** + * Invites a user to an invite-only channel. + * + * @param string $nick Nick of the user to invite + * @param string $channel Name of the channel + * + * @return void + */ + public function doInvite($nick, $channel) + { + $this->send('INVITE', array($nick, $channel)); + } + + /** + * Obtains a list of nicks of usrs in currently joined channels. + * + * @param string $channels Comma-delimited list of one or more channels + * + * @return void + */ + public function doNames($channels) + { + $this->send('NAMES', $channels); + } + + /** + * Obtains a list of channel names and topics. + * + * @param string $channels Comma-delimited list of one or more channels + * to which the response should be restricted + * (optional) + * + * @return void + */ + public function doList($channels = null) + { + $this->send('LIST', $channels); + } + + /** + * Retrieves or changes a channel topic. + * + * @param string $channel Name of the channel + * @param string $topic New topic to assign (optional) + * + * @return void + */ + public function doTopic($channel, $topic = null) + { + $args = array($channel); + + if (!empty($topic)) { + $args[] = $topic; + } + + $this->send('TOPIC', $args); + } + + /** + * Retrieves or changes a channel or user mode. + * + * @param string $target Channel name or user nick + * @param string $mode New mode to assign (optional) + * + * @return void + */ + public function doMode($target, $mode = null) + { + $args = array($target); + + if (!empty($mode)) { + $args[] = $mode; + } + + $this->send('MODE', $args); + } + + /** + * Changes the client nick. + * + * @param string $nick New nick to assign + * + * @return void + */ + public function doNick($nick) + { + $this->send('NICK', $nick); + } + + /** + * Retrieves information about a nick. + * + * @param string $nick Nick + * + * @return void + */ + public function doWhois($nick) + { + $this->send('WHOIS', $nick); + } + + /** + * Sends a message to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the message to send + * + * @return void + */ + public function doPrivmsg($target, $text) + { + $this->send('PRIVMSG', array($target, $text)); + } + + /** + * Sends a notice to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the notice to send + * + * @return void + */ + public function doNotice($target, $text) + { + $this->send('NOTICE', array($target, $text)); + } + + /** + * Kicks a user from a channel. + * + * @param string $nick Nick of the user + * @param string $channel Channel name + * @param string $reason Reason for the kick (optional) + * + * @return void + */ + public function doKick($nick, $channel, $reason = null) + { + $args = array($nick, $channel); + + if (!empty($reason)) { + $args[] = $response; + } + + $this->send('KICK', $args); + } + + /** + * Responds to a server test of client responsiveness. + * + * @param string $daemon Daemon from which the original request originates + * + * @return void + */ + public function doPong($daemon) + { + $this->send('PONG', $daemon); + } + + /** + * Sends a CTCP ACTION (/me) command to a nick or channel. + * + * @param string $target Channel name or user nick + * @param string $text Text of the action to perform + * + * @return void + */ + public function doAction($target, $text) + { + $buffer = rtrim('ACTION ' . $text); + + $this->doPrivmsg($target, chr(1) . $buffer . chr(1)); + } + + /** + * Sends a CTCP response to a user. + * + * @param string $nick User nick + * @param string $command Command to send + * @param string|array $args String or array of sequential arguments + * (optional) + * + * @return void + */ + protected function doCtcp($nick, $command, $args = null) + { + if (is_array($args)) { + $args = implode(' ', $args); + } + + $buffer = rtrim(strtoupper($command) . ' ' . $args); + + $this->doNotice($nick, chr(1) . $buffer . chr(1)); + } + + /** + * Sends a CTCP PING request or response (they are identical) to a user. + * + * @param string $nick User nick + * @param string $hash Hash to use in the handshake + * + * @return void + */ + public function doPing($nick, $hash) + { + $this->doCtcp($nick, 'PING', $hash); + } + + /** + * Sends a CTCP VERSION request or response to a user. + * + * @param string $nick User nick + * @param string $version Version string to send for a response + * + * @return void + */ + public function doVersion($nick, $version = null) + { + if ($version) { + $this->doCtcp($nick, 'VERSION', $version); + } else { + $this->doCtcp($nick, 'VERSION'); + } + } + + /** + * Sends a CTCP TIME request to a user. + * + * @param string $nick User nick + * @param string $time Time string to send for a response + * + * @return void + */ + public function doTime($nick, $time = null) + { + if ($time) { + $this->doCtcp($nick, 'TIME', $time); + } else { + $this->doCtcp($nick, 'TIME'); + } + } + + /** + * Sends a CTCP FINGER request to a user. + * + * @param string $nick User nick + * @param string $finger Finger string to send for a response + * + * @return void + */ + public function doFinger($nick, $finger = null) + { + if ($finger) { + $this->doCtcp($nick, 'FINGER', $finger); + } else { + $this->doCtcp($nick, 'FINGER'); + } + } + + /** + * Sends a raw command to the server. + * + * @param string $command Command string to send + * + * @return void + */ + public function doRaw($command) + { + $this->send('RAW', $command); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php new file mode 100644 index 000000000..54b035dc0 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Abstract.php @@ -0,0 +1,62 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for events. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +abstract class Phergie_Event_Abstract +{ + /** + * Event type, used for determining the callback to execute in response + * + * @var string + */ + protected $type; + + /** + * Returns the event type. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the event type. + * + * @param string $type Event type + * + * @return Phergie_Event_Abstract Implements a fluent interface + */ + public function setType($type) + { + $this->type = (string) $type; + return $this; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Command.php b/plugins/Irc/extlib/phergie/Phergie/Event/Command.php new file mode 100644 index 000000000..5940636ba --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Command.php @@ -0,0 +1,62 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Event originating from a plugin for the bot. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Event_Command extends Phergie_Event_Request +{ + /** + * Reference to the plugin instance that created the event + * + * @var Phergie_Plugin_Abstract + */ + protected $plugin; + + /** + * Stores a reference to the plugin instance that created the event. + * + * @param Phergie_Plugin_Abstract $plugin Plugin instance + * + * @return Phergie_Event_Command Provides a fluent interface + */ + public function setPlugin(Phergie_Plugin_Abstract $plugin) + { + $this->plugin = $plugin; + return $this; + } + + /** + * Returns a reference to the plugin instance that created the event. + * + * @return Phergie_Plugin_Abstract + */ + public function getPlugin() + { + return $this->plugin; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Event/Exception.php new file mode 100644 index 000000000..6b094a810 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Exception.php @@ -0,0 +1,38 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to outgoing events. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Event_Exception extends Phergie_Exception +{ + /** + * Error indicating that an attempt was made to create an event of an + * unknown type + */ + const ERR_UNKNOWN_EVENT_TYPE = 1; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Event/Handler.php new file mode 100644 index 000000000..e308df8a5 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Handler.php @@ -0,0 +1,190 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Handles events initiated by plugins. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Event_Handler implements IteratorAggregate, Countable +{ + /** + * Current queue of events + * + * @var array + */ + protected $events; + + /** + * Constructor to initialize the event queue. + * + * @return void + */ + public function __construct() + { + $this->events = array(); + } + + /** + * Adds an event to the queue. + * + * @param Phergie_Plugin_Abstract $plugin Plugin originating the event + * @param string $type Event type, corresponding to a + * Phergie_Event_Command::TYPE_* constant + * @param array $args Optional event arguments + * + * @return Phergie_Event_Handler Provides a fluent interface + */ + public function addEvent(Phergie_Plugin_Abstract $plugin, $type, + array $args = array() + ) { + if (!defined('Phergie_Event_Command::TYPE_' . strtoupper($type))) { + throw new Phergie_Event_Exception( + 'Unknown event type "' . $type . '"', + Phergie_Event_Exception::ERR_UNKNOWN_EVENT_TYPE + ); + } + + $event = new Phergie_Event_Command; + $event + ->setPlugin($plugin) + ->setType($type) + ->setArguments($args); + + $this->events[] = $event; + + return $this; + } + + /** + * Returns the current event queue. + * + * @return array Enumerated array of Phergie_Event_Command objects + */ + public function getEvents() + { + return $this->events; + } + + /** + * Clears the event queue. + * + * @return Phergie_Event_Handler Provides a fluent interface + */ + public function clearEvents() + { + $this->events = array(); + return $this; + } + + /** + * Replaces the current event queue with a given queue of events. + * + * @param array $events Ordered list of objects of the class + * Phergie_Event_Command + * + * @return Phergie_Event_Handler Provides a fluent interface + */ + public function replaceEvents(array $events) + { + $this->events = $events; + return $this; + } + + /** + * Returns whether an event of the given type exists in the queue. + * + * @param string $type Event type from Phergie_Event_Request::TYPE_* + * constants + * + * @return bool TRUE if an event of the specified type exists in the + * queue, FALSE otherwise + */ + public function hasEventOfType($type) + { + foreach ($this->events as $event) { + if ($event->getType() == $type) { + return true; + } + } + return false; + } + + /** + * Returns a list of events of a specified type. + * + * @param string $type Event type from Phergie_Event_Request::TYPE_* + * constants + * + * @return array Array containing event instances of the specified type + * or an empty array if no such events were found + */ + public function getEventsOfType($type) + { + $events = array(); + foreach ($this->events as $event) { + if ($event->getType() == $type) { + $events[] = $event; + } + } + return $events; + } + + /** + * Removes a single event from the event queue. + * + * @param Phergie_Event_Command $event Event to remove + * + * @return Phergie_Event_Handler Provides a fluent interface + */ + public function removeEvent(Phergie_Event_Command $event) + { + $key = array_search($event, $this->events); + if ($key !== false) { + unset($this->events[$key]); + } + return $this; + } + + /** + * Returns an iterator for the current event queue. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->events); + } + + /** + * Returns the number of events in the event queue + * + * @return int number of queued events + */ + public function count() + { + return count($this->events); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Request.php b/plugins/Irc/extlib/phergie/Phergie/Event/Request.php new file mode 100755 index 000000000..647b5acb8 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Request.php @@ -0,0 +1,468 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Autonomous event originating from a user or the server. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + * @link http://www.irchelp.org/irchelp/rfc/chapter4.html + */ +class Phergie_Event_Request + extends Phergie_Event_Abstract + implements ArrayAccess +{ + /** + * Nick message event type + */ + const TYPE_NICK = 'nick'; + + /** + * Whois message event type + */ + const TYPE_WHOIS = 'whois'; + + /** + * Quit command event type + */ + const TYPE_QUIT = 'quit'; + + /** + * Join message event type + */ + const TYPE_JOIN = 'join'; + + /** + * Kick message event type + */ + const TYPE_KICK = 'kick'; + + /** + * Part message event type + */ + const TYPE_PART = 'part'; + + /** + * Invite message event type + */ + const TYPE_INVITE = 'invite'; + + /** + * Mode message event type + */ + const TYPE_MODE = 'mode'; + + /** + * Topic message event type + */ + const TYPE_TOPIC = 'topic'; + + /** + * Private message command event type + */ + const TYPE_PRIVMSG = 'privmsg'; + + /** + * Notice message event type + */ + const TYPE_NOTICE = 'notice'; + + /** + * Pong message event type + */ + const TYPE_PONG = 'pong'; + + /** + * CTCP ACTION command event type + */ + const TYPE_ACTION = 'action'; + + /** + * CTCP PING command event type + */ + const TYPE_PING = 'ping'; + + /** + * CTCP TIME command event type + */ + const TYPE_TIME = 'time'; + + /** + * CTCP VERSION command event type + */ + const TYPE_VERSION = 'version'; + + /** + * RAW message event type + */ + const TYPE_RAW = 'raw'; + + /** + * Mapping of event types to their named parameters + * + * @var array + */ + protected static $map = array( + + self::TYPE_QUIT => array( + 'message' => 0 + ), + + self::TYPE_JOIN => array( + 'channel' => 0 + ), + + self::TYPE_KICK => array( + 'channel' => 0, + 'user' => 1, + 'comment' => 2 + ), + + self::TYPE_PART => array( + 'channel' => 0, + 'message' => 1 + ), + + self::TYPE_INVITE => array( + 'nickname' => 0, + 'channel' => 1 + ), + + self::TYPE_MODE => array( + 'target' => 0, + 'mode' => 1, + 'limit' => 2, + 'user' => 3, + 'banmask' => 4 + ), + + self::TYPE_TOPIC => array( + 'channel' => 0, + 'topic' => 1 + ), + + self::TYPE_PRIVMSG => array( + 'receiver' => 0, + 'text' => 1 + ), + + self::TYPE_NOTICE => array( + 'nickname' => 0, + 'text' => 1 + ), + + self::TYPE_ACTION => array( + 'target' => 0, + 'action' => 1 + ), + + self::TYPE_RAW => array( + 'message' => 0 + ) + + ); + + /** + * Hostmask representing the originating user, if applicable + * + * @var Phergie_Hostmask + */ + protected $hostmask; + + /** + * Arguments included with the message + * + * @var array + */ + protected $arguments; + + /** + * Raw data sent by the server + * + * @var string + */ + protected $rawData; + + /** + * Sets the hostmask representing the originating user. + * + * @param Phergie_Hostmask $hostmask User hostmask + * + * @return Phergie_Event_Request Provides a fluent interface + */ + public function setHostmask(Phergie_Hostmask $hostmask) + { + $this->hostmask = $hostmask; + return $this; + } + + /** + * Returns the hostmask representing the originating user. + * + * @return Phergie_Event_Request|null Hostmask or NULL if none was set + */ + public function getHostmask() + { + return $this->hostmask; + } + + /** + * Sets the arguments for the request. + * + * @param array $arguments Request arguments + * + * @return Phergie_Event_Request Provides a fluent interface + */ + public function setArguments($arguments) + { + $this->arguments = $arguments; + return $this; + } + + /** + * Sets the value of a single argument for the request. + * + * @param mixed $argument Integer position (starting from 0) or the + * equivalent string name of the argument from self::$map + * @param string $value Value to assign to the argument + * + * @return Phergie_Event_Request Provides a fluent interface + */ + public function setArgument($argument, $value) + { + $argument = $this->resolveArgument($argument); + if ($argument !== null) { + $this->arguments[$argument] = (string) $value; + } + return $this; + } + + /** + * Returns the arguments for the request. + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Resolves an argument specification to an integer position. + * + * @param mixed $argument Integer position (starting from 0) or the + * equivalent string name of the argument from self::$map + * + * @return int|null Integer position of the argument or NULL if no + * corresponding argument was found + */ + protected function resolveArgument($argument) + { + if (isset($this->arguments[$argument])) { + return $argument; + } else { + $argument = strtolower($argument); + if (isset(self::$map[$this->type][$argument]) + && isset($this->arguments[self::$map[$this->type][$argument]]) + ) { + return self::$map[$this->type][$argument]; + } + } + return null; + } + + /** + * Returns a single specified argument for the request. + * + * @param mixed $argument Integer position (starting from 0) or the + * equivalent string name of the argument from self::$map + * + * @return string|null Argument value or NULL if none is set + */ + public function getArgument($argument) + { + $argument = $this->resolveArgument($argument); + if ($argument !== null) { + return $this->arguments[$argument]; + } + return null; + } + + /** + * Sets the raw buffer for the event. + * + * @param string $buffer Raw event buffer + * + * @return Phergie_Event_Request Provides a fluent interface + */ + public function setRawData($buffer) + { + $this->rawData = $buffer; + return $this; + } + + /** + * Returns the raw buffer sent from the server for the event. + * + * @return string + */ + public function getRawData() + { + return $this->rawData; + } + + /** + * Returns the nick of the user who originated the event. + * + * @return string + */ + public function getNick() + { + return $this->hostmask->getNick(); + } + + /** + * Returns the channel name if the event occurred in a channel or the + * user nick if the event was a private message directed at the bot by a + * user. + * + * @return string + */ + public function getSource() + { + if (substr($this->arguments[0], 0, 1) == '#') { + return $this->arguments[0]; + } + return $this->hostmask->getNick(); + } + + /** + * Returns whether or not the event occurred within a channel. + * + * @return TRUE if the event is in a channel, FALSE otherwise + */ + public function isInChannel() + { + return (substr($this->getSource(), 0, 1) == '#'); + } + + /** + * Returns whether or not the event originated from a user. + * + * @return TRUE if the event is from a user, FALSE otherwise + */ + public function isFromUser() + { + if (empty($this->hostmask)) { + return false; + } + $username = $this->hostmask->getUsername(); + return !empty($username); + } + + /** + * Returns whether or not the event originated from the server. + * + * @return TRUE if the event is from the server, FALSE otherwise + */ + public function isFromServer() + { + $username = $this->hostmask->getUsername(); + return empty($username); + } + + /** + * Provides access to named parameters via virtual "getter" methods. + * + * @param string $name Name of the method called + * @param array $arguments Arguments passed to the method (should always + * be empty) + * + * @return mixed Method return value + */ + public function __call($name, array $arguments) + { + if (!count($arguments) && substr($name, 0, 3) == 'get') { + return $this->getArgument(substr($name, 3)); + } + } + + /** + * Checks to see if an event argument is assigned a value. + * + * @param string|int $offset Argument name or position beginning from 0 + * + * @return bool TRUE if the argument has a value, FALSE otherwise + * @see ArrayAccess::offsetExists() + */ + public function offsetExists($offset) + { + return ($this->resolveArgument($offset) !== null); + } + + /** + * Returns the value of an event argument. + * + * @param string|int $offset Argument name or position beginning from 0 + * + * @return string|null Argument value or NULL if none is set + * @see ArrayAccess::offsetGet() + */ + public function offsetGet($offset) + { + return $this->getArgument($offset); + } + + /** + * Sets the value of an event argument. + * + * @param string|int $offset Argument name or position beginning from 0 + * @param string $value New argument value + * + * @return void + * @see ArrayAccess::offsetSet() + */ + public function offsetSet($offset, $value) + { + $offset = $this->resolveArgument($offset); + if ($offset !== null) { + $this->arguments[$offset] = $value; + } + } + + /** + * Removes the value set for an event argument. + * + * @param string|int $offset Argument name or position beginning from 0 + * + * @return void + * @see ArrayAccess::offsetUnset() + */ + public function offsetUnset($offset) + { + if ($offset = $this->resolveArgument($offset)) { + unset($this->arguments[$offset]); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Event/Response.php b/plugins/Irc/extlib/phergie/Phergie/Event/Response.php new file mode 100755 index 000000000..097e2535e --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Event/Response.php @@ -0,0 +1,953 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Event originating from the server in response to an event sent by the + * current client. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + * @link http://www.irchelp.org/irchelp/rfc/chapter6.html + */ +class Phergie_Event_Response extends Phergie_Event_Abstract +{ + /** + * <nickname> No such nick/channel + * + * Used to indicate the nickname parameter supplied to a command is currently + * unused. + */ + const ERR_NOSUCHNICK = '401'; + + /** + * <server name> No such server + * + * Used to indicate the server name given currently doesn't exist. + */ + const ERR_NOSUCHSERVER = '402'; + + /** + * <channel name> No such channel + * + * Used to indicate the given channel name is invalid. + */ + const ERR_NOSUCHCHANNEL = '403'; + + /** + * <channel name> Cannot send to channel + * + * Sent to a user who is either (a) not on a channel which is mode +n or (b) not + * a chanop (or mode +v) on a channel which has mode +m set and is trying to send + * a PRIVMSG message to that channel. + */ + const ERR_CANNOTSENDTOCHAN = '404'; + + /** + * <channel name> You have joined too many channels + * + * Sent to a user when they have joined the maximum number of allowed channels + * and they try to join another channel. + */ + const ERR_TOOMANYCHANNELS = '405'; + + /** + * <nickname> There was no such nickname + * + * Returned by WHOWAS to indicate there is no history information for that + * nickname. + */ + const ERR_WASNOSUCHNICK = '406'; + + /** + * <target> Duplicate recipients. No message delivered + * + * Returned to a client which is attempting to send PRIVMSG/NOTICE using the + * user@host destination format and for a user@host which has several + * occurrences. + */ + const ERR_TOOMANYTARGETS = '407'; + + /** + * No origin specified + * + * PING or PONG message missing the originator parameter which is required since + * these commands must work without valid prefixes. + */ + const ERR_NOORIGIN = '409'; + + /** + * No recipient given (<command>) + */ + const ERR_NORECIPIENT = '411'; + + /** + * No text to send + */ + const ERR_NOTEXTTOSEND = '412'; + + /** + * <mask> No toplevel domain specified + */ + const ERR_NOTOPLEVEL = '413'; + + /** + * <mask> Wildcard in toplevel domain + * + * 412 - 414 are returned by PRIVMSG to indicate that the message wasn't + * delivered for some reason. ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that + * are returned when an invalid use of "PRIVMSG $<server>" or "PRIVMSG #<host>" + * is attempted. + */ + const ERR_WILDTOPLEVEL = '414'; + + /** + * <command> Unknown command + * + * Returned to a registered client to indicate that the command sent is unknown + * by the server. + */ + const ERR_UNKNOWNCOMMAND = '421'; + + /** + * MOTD File is missing + * + * Server's MOTD file could not be opened by the server. + */ + const ERR_NOMOTD = '422'; + + /** + * <server> No administrative info available + * + * Returned by a server in response to an ADMIN message when there is an error in + * finding the appropriate information. + */ + const ERR_NOADMININFO = '423'; + + /** + * File error doing <file op> on <file> + * + * Generic error message used to report a failed file operation during the + * processing of a message. + */ + const ERR_FILEERROR = '424'; + + /** + * No nickname given + * + * Returned when a nickname parameter expected for a command and isn't found. + */ + const ERR_NONICKNAMEGIVEN = '431'; + + /** + * <nick> Erroneus nickname + * + * Returned after receiving a NICK message which contains characters which do not + * fall in the defined set. See section x.x.x for details on valid nicknames. + */ + const ERR_ERRONEUSNICKNAME = '432'; + + /** + * <nick> Nickname is already in use + * + * Returned when a NICK message is processed that results in an attempt to change + * to a currently existing nickname. + */ + const ERR_NICKNAMEINUSE = '433'; + + /** + * <nick> Nickname collision KILL + * + * Returned by a server to a client when it detects a nickname collision + * (registered of a NICK that already exists by another server). + */ + const ERR_NICKCOLLISION = '436'; + + /** + * <nick> <channel> They aren't on that channel + * + * Returned by the server to indicate that the target user of the command is not + * on the given channel. + */ + const ERR_USERNOTINCHANNEL = '441'; + + /** + * <channel> You're not on that channel + * + * Returned by the server whenever a client tries to perform a channel effecting + * command for which the client isn't a member. + */ + const ERR_NOTONCHANNEL = '442'; + + /** + * <user> <channel> is already on channel + * + * Returned when a client tries to invite a user to a channel they are already + * on. + */ + const ERR_USERONCHANNEL = '443'; + + /** + * <user> User not logged in + * + * Returned by the summon after a SUMMON command for a user was unable to be + * performed since they were not logged in. + */ + const ERR_NOLOGIN = '444'; + + /** + * SUMMON has been disabled + * + * Returned as a response to the SUMMON command. Must be returned by any server + * which does not implement it. + */ + const ERR_SUMMONDISABLED = '445'; + + /** + * USERS has been disabled + * + * Returned as a response to the USERS command. Must be returned by any server + * which does not implement it. + */ + const ERR_USERSDISABLED = '446'; + + /** + * You have not registered + * + * Returned by the server to indicate that the client must be registered before + * the server will allow it to be parsed in detail. + */ + const ERR_NOTREGISTERED = '451'; + + /** + * <command> Not enough parameters + * + * Returned by the server by numerous commands to indicate to the client that it + * didn't supply enough parameters. + */ + const ERR_NEEDMOREPARAMS = '461'; + + /** + * You may not reregister + * + * Returned by the server to any link which tries to change part of the + * registered details (such as password or user details from second USER + * message). + */ + const ERR_ALREADYREGISTRED = '462'; + + /** + * Your host isn't among the privileged + * + * Returned to a client which attempts to register with a server which does not + * been setup to allow connections from the host the attempted connection is + * tried. + */ + const ERR_NOPERMFORHOST = '463'; + + /** + * Password incorrect + * + * Returned to indicate a failed attempt at registering a connection for which a + * password was required and was either not given or incorrect. + */ + const ERR_PASSWDMISMATCH = '464'; + + /** + * You are banned from this server + * + * Returned after an attempt to connect and register yourself with a server which + * has been setup to explicitly deny connections to you. + */ + const ERR_YOUREBANNEDCREEP = '465'; + + /** + * <channel> Channel key already set + */ + const ERR_KEYSET = '467'; + + /** + * <channel> Cannot join channel (+l) + */ + const ERR_CHANNELISFULL = '471'; + + /** + * <char> is unknown mode char to me + */ + const ERR_UNKNOWNMODE = '472'; + + /** + * <channel> Cannot join channel (+i) + */ + const ERR_INVITEONLYCHAN = '473'; + + /** + * <channel> Cannot join channel (+b) + */ + const ERR_BANNEDFROMCHAN = '474'; + + /** + * <channel> Cannot join channel (+k) + */ + const ERR_BADCHANNELKEY = '475'; + + /** + * Permission Denied- You're not an IRC operator + * + * Any command requiring operator privileges to operate must return this error to + * indicate the attempt was unsuccessful. + */ + const ERR_NOPRIVILEGES = '481'; + + /** + * <channel> You're not channel operator + * + * Any command requiring 'chanop' privileges (such as MODE messages) must return + * this error if the client making the attempt is not a chanop on the specified + * channel. + */ + const ERR_CHANOPRIVSNEEDED = '482'; + + /** + * You cant kill a server! + * + * Any attempts to use the KILL command on a server are to be refused and this + * error returned directly to the client. + */ + const ERR_CANTKILLSERVER = '483'; + + /** + * No O-lines for your host + * + * If a client sends an OPER message and the server has not been configured to + * allow connections from the client's host as an operator, this error must be + * returned. + */ + const ERR_NOOPERHOST = '491'; + + /** + * Unknown MODE flag + * + * Returned by the server to indicate that a MODE message was sent with a + * nickname parameter and that the a mode flag sent was not recognized. + */ + const ERR_UMODEUNKNOWNFLAG = '501'; + + /** + * Cant change mode for other users + * + * Error sent to any user trying to view or change the user mode for a user other + * than themselves. + */ + const ERR_USERSDONTMATCH = '502'; + + /** + * Dummy reply number. Not used. + */ + const RPL_NONE = '300'; + + /** + * [<reply>{<space><reply>}] + * + * Reply format used by USERHOST to list replies to the query list. The reply + * string is composed as follows <reply> = <nick>['*'] '=' <'+'|'-'><hostname> + * The '*' indicates whether the client has registered as an Operator. The '-' or + * '+' characters represent whether the client has set an AWAY message or not + * respectively. + */ + const RPL_USERHOST = '302'; + + /** + * [<nick> {<space><nick>}] + * + * Reply format used by ISON to list replies to the query list. + */ + const RPL_ISON = '303'; + + /** + * <nick> <away message> + */ + const RPL_AWAY = '301'; + + /** + * You are no longer marked as being away + */ + const RPL_UNAWAY = '305'; + + /** + * You have been marked as being away + * + * These replies are used with the AWAY command (if allowed). RPL_AWAY is sent to + * any client sending a PRIVMSG to a client which is away. RPL_AWAY is only sent + * by the server to which the client is connected. Replies RPL_UNAWAY and + * RPL_NOWAWAY are sent when the client removes and sets an AWAY message. + */ + const RPL_NOWAWAY = '306'; + + /** + * <nick> <user> <host> * <real name> + */ + const RPL_WHOISUSER = '311'; + + /** + * <nick> <server> <server info> + */ + const RPL_WHOISSERVER = '312'; + + /** + * <nick> is an IRC operator + */ + const RPL_WHOISOPERATOR = '313'; + + /** + * <nick> <integer> seconds idle + */ + const RPL_WHOISIDLE = '317'; + + /** + * <nick> End of /WHOIS list + */ + const RPL_ENDOFWHOIS = '318'; + + /** + * <nick> {[@|+]<channel><space>} + * + * Replies 311 - 313, 317 - 319 are all replies generated in response to a WHOIS + * message. Given that there are enough parameters present, the answering server + * must either formulate a reply out of the above numerics (if the query nick is + * found) or return an error reply. The '*' in RPL_WHOISUSER is there as the + * literal character and not as a wild card. For each reply set, only + * RPL_WHOISCHANNELS may appear more than once (for long lists of channel names). + * The '@' and '+' characters next to the channel name indicate whether a client + * is a channel operator or has been granted permission to speak on a moderated + * channel. The RPL_ENDOFWHOIS reply is used to mark the end of processing a + * WHOIS message. + */ + const RPL_WHOISCHANNELS = '319'; + + /** + * <nick> <user> <host> * <real name> + */ + const RPL_WHOWASUSER = '314'; + + /** + * <nick> End of WHOWAS + * + * When replying to a WHOWAS message, a server must use the replies + * RPL_WHOWASUSER, RPL_WHOISSERVER or ERR_WASNOSUCHNICK for each nickname in the + * presented list. At the end of all reply batches, there must be RPL_ENDOFWHOWAS + * (even if there was only one reply and it was an error). + */ + const RPL_ENDOFWHOWAS = '369'; + + /** + * Channel Users Name + */ + const RPL_LISTSTART = '321'; + + /** + * <channel> <# visible> <topic> + */ + const RPL_LIST = '322'; + + /** + * End of /LIST + * + * Replies RPL_LISTSTART, RPL_LIST, RPL_LISTEND mark the start, actual replies + * with data and end of the server's response to a LIST command. If there are no + * channels available to return, only the start and end reply must be sent. + */ + const RPL_LISTEND = '323'; + + /** + * <channel> <mode> <mode params> + */ + const RPL_CHANNELMODEIS = '324'; + + /** + * <channel> No topic is set + */ + const RPL_NOTOPIC = '331'; + + /** + * <channel> <topic> + * + * When sending a TOPIC message to determine the channel topic, one of two + * replies is sent. If the topic is set, RPL_TOPIC is sent back else RPL_NOTOPIC. + */ + const RPL_TOPIC = '332'; + + /** + * <channel> <nick> + * + * Returned by the server to indicate that the attempted INVITE message was + * successful and is being passed onto the end client. + */ + const RPL_INVITING = '341'; + + /** + * <user> Summoning user to IRC + * + * Returned by a server answering a SUMMON message to indicate that it is + * summoning that user. + */ + const RPL_SUMMONING = '342'; + + /** + * <version>.<debuglevel> <server> <comments> + * + * Reply by the server showing its version details. The <version> is the version + * of the software being used (including any patchlevel revisions) and the + * <debuglevel> is used to indicate if the server is running in "debug mode". The + * "comments" field may contain any comments about the version or further version + * details. + */ + const RPL_VERSION = '351'; + + /** + * <channel> <user> <host> <server> <nick> <H|G>[*][@|+] <hopcount> <real name> + */ + const RPL_WHOREPLY = '352'; + + /** + * <name> End of /WHO list + * + * The RPL_WHOREPLY and RPL_ENDOFWHO pair are used to answer a WHO message. The + * RPL_WHOREPLY is only sent if there is an appropriate match to the WHO query. + * If there is a list of parameters supplied with a WHO message, a RPL_ENDOFWHO + * must be sent after processing each list item with <name> being the item. + */ + const RPL_ENDOFWHO = '315'; + + /** + * <channel> [[@|+]<nick> [[@|+]<nick> [...]]] + */ + const RPL_NAMREPLY = '353'; + + /** + * <channel> End of /NAMES list + * + * To reply to a NAMES message, a reply pair consisting of RPL_NAMREPLY and + * RPL_ENDOFNAMES is sent by the server back to the client. If there is no + * channel found as in the query, then only RPL_ENDOFNAMES is returned. The + * exception to this is when a NAMES message is sent with no parameters and all + * visible channels and contents are sent back in a series of RPL_NAMEREPLY + * messages with a RPL_ENDOFNAMES to mark the end. + */ + const RPL_ENDOFNAMES = '366'; + + /** + * <mask> <server> <hopcount> <server info> + */ + const RPL_LINKS = '364'; + + /** + * <mask> End of /LINKS list + * + * In replying to the LINKS message, a server must send replies back using the + * RPL_LINKS numeric and mark the end of the list using an RPL_ENDOFLINKS reply.v + */ + const RPL_ENDOFLINKS = '365'; + + /** + * <channel> <banid> + */ + const RPL_BANLIST = '367'; + + /** + * <channel> End of channel ban list + * + * When listing the active 'bans' for a given channel, a server is required to + * send the list back using the RPL_BANLIST and RPL_ENDOFBANLIST messages. A + * separate RPL_BANLIST is sent for each active banid. After the banids have been + * listed (or if none present) a RPL_ENDOFBANLIST must be sent. + */ + const RPL_ENDOFBANLIST = '368'; + + /** + * <string> + */ + const RPL_INFO = '371'; + + /** + * End of /INFO list + * + * A server responding to an INFO message is required to send all its 'info' in a + * series of RPL_INFO messages with a RPL_ENDOFINFO reply to indicate the end of + * the replies. + */ + const RPL_ENDOFINFO = '374'; + + /** + * - <server> Message of the day - + */ + const RPL_MOTDSTART = '375'; + + /** + * - <text> + */ + const RPL_MOTD = '372'; + + /** + * End of /MOTD command + * + * When responding to the MOTD message and the MOTD file is found, the file is + * displayed line by line, with each line no longer than 80 characters, using + * RPL_MOTD format replies. These should be surrounded by a RPL_MOTDSTART (before + * the RPL_MOTDs) and an RPL_ENDOFMOTD (after). + */ + const RPL_ENDOFMOTD = '376'; + + /** + * You are now an IRC operator + * + * RPL_YOUREOPER is sent back to a client which has just successfully issued an + * OPER message and gained operator status. + */ + const RPL_YOUREOPER = '381'; + + /** + * <config file> Rehashing + * + * If the REHASH option is used and an operator sends a REHASH message, an + * RPL_REHASHING is sent back to the operator. + */ + const RPL_REHASHING = '382'; + + /** + * <server> <string showing server's local time> + * + * When replying to the TIME message, a server must send the reply using the + * RPL_TIME format above. The string showing the time need only contain the + * correct day and time there. There is no further requirement for the time + * string. + */ + const RPL_TIME = '391'; + + /** + * UserID Terminal Host + */ + const RPL_USERSSTART = '392'; + + /** + * %-8s %-9s %-8s + */ + const RPL_USERS = '393'; + + /** + * End of users + */ + const RPL_ENDOFUSERS = '394'; + + /** + * Nobody logged in + * + * If the USERS message is handled by a server, the replies RPL_USERSTART, + * RPL_USERS, RPL_ENDOFUSERS and RPL_NOUSERS are used. RPL_USERSSTART must be + * sent first, following by either a sequence of RPL_USERS or a single + * RPL_NOUSER. Following this is RPL_ENDOFUSERS. + */ + const RPL_NOUSERS = '395'; + + /** + * Link <version & debug level> <destination> <next server> + */ + const RPL_TRACELINK = '200'; + + /** + * Try. <class> <server> + */ + const RPL_TRACECONNECTING = '201'; + + /** + * H.S. <class> <server> + */ + const RPL_TRACEHANDSHAKE = '202'; + + /** + * ???? <class> [<client IP address in dot form>] + */ + const RPL_TRACEUNKNOWN = '203'; + + /** + * Oper <class> <nick> + */ + const RPL_TRACEOPERATOR = '204'; + + /** + * User <class> <nick> + */ + const RPL_TRACEUSER = '205'; + + /** + * Serv <class> <int>S <int>C <server> <nick!user|*!*>@<host|server> + */ + const RPL_TRACESERVER = '206'; + + /** + * <newtype> 0 <client name> + */ + const RPL_TRACENEWTYPE = '208'; + + /** + * File <logfile> <debug level> + * + * The RPL_TRACE* are all returned by the server in response to the TRACE + * message. How many are returned is dependent on the the TRACE message and + * whether it was sent by an operator or not. There is no predefined order for + * which occurs first. Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and + * RPL_TRACEHANDSHAKE are all used for connections which have not been fully + * established and are either unknown, still attempting to connect or in the + * process of completing the 'server handshake'. RPL_TRACELINK is sent by any + * server which handles a TRACE message and has to pass it on to another server. + * The list of RPL_TRACELINKs sent in response to a TRACE command traversing the + * IRC network should reflect the actual connectivity of the servers themselves + * along that path. RPL_TRACENEWTYPE is to be used for any connection which does + * not fit in the other categories but is being displayed anyway. + */ + const RPL_TRACELOG = '261'; + + /** + * <linkname> <sendq> <sent messages> <sent bytes> <received messages> <received + * bytes> <time open> + */ + const RPL_STATSLINKINFO = '211'; + + /** + * <command> <count> + */ + const RPL_STATSCOMMANDS = '212'; + + /** + * C <host> * <name> <port> <class> + */ + const RPL_STATSCLINE = '213'; + + /** + * N <host> * <name> <port> <class> + */ + const RPL_STATSNLINE = '214'; + + /** + * I <host> * <host> <port> <class> + */ + const RPL_STATSILINE = '215'; + + /** + * K <host> * <username> <port> <class> + */ + const RPL_STATSKLINE = '216'; + + /** + * Y <class> <ping frequency> <connect frequency> <max sendq> + */ + const RPL_STATSYLINE = '218'; + + /** + * <stats letter> End of /STATS report + */ + const RPL_ENDOFSTATS = '219'; + + /** + * L <hostmask> * <servername> <maxdepth> + */ + const RPL_STATSLLINE = '241'; + + /** + * Server Up %d days %d%02d%02d + */ + const RPL_STATSUPTIME = '242'; + + /** + * O <hostmask> * <name> + */ + const RPL_STATSOLINE = '243'; + + /** + * H <hostmask> * <servername> + */ + const RPL_STATSHLINE = '244'; + + /** + * <user mode string> + * + * To answer a query about a client's own mode, RPL_UMODEIS is sent back. + */ + const RPL_UMODEIS = '221'; + + /** + * There are <integer> users and <integer> invisible on <integer> servers + */ + const RPL_LUSERCLIENT = '251'; + + /** + * <integer> operator(s) online + */ + const RPL_LUSEROP = '252'; + + /** + * <integer> unknown connection(s) + */ + const RPL_LUSERUNKNOWN = '253'; + + /** + * <integer> channels formed + */ + const RPL_LUSERCHANNELS = '254'; + + /** + * I have <integer> clients and <integer> servers + * + * In processing an LUSERS message, the server sends a set of replies from + * RPL_LUSERCLIENT, RPL_LUSEROP, RPL_USERUNKNOWN, RPL_LUSERCHANNELS and + * RPL_LUSERME. When replying, a server must send back RPL_LUSERCLIENT and + * RPL_LUSERME. The other replies are only sent back if a non-zero count is found + * for them. + */ + const RPL_LUSERME = '255'; + + /** + * <server> Administrative info + */ + const RPL_ADMINME = '256'; + + /** + * <admin info> + */ + const RPL_ADMINLOC1 = '257'; + + /** + * <admin info> + */ + const RPL_ADMINLOC2 = '258'; + + /** + * <admin info> + * + * When replying to an ADMIN message, a server is expected to use replies + * RLP_ADMINME through to RPL_ADMINEMAIL and provide a text message with each. + * For RPL_ADMINLOC1 a description of what city, state and country the server is + * in is expected, followed by details of the university and department + * (RPL_ADMINLOC2) and finally the administrative contact for the server (an + * email address here is required) in RPL_ADMINEMAIL. + */ + const RPL_ADMINEMAIL = '259'; + + /** + * Reply code sent by the server, which can be compared to the ERR_* and + * RPL_* constants + * + * @var string + */ + protected $code; + + /** + * Reply code description sent by the server. + * + * @var string + */ + protected $description; + + /** + * Raw data sent by the server + * + * @var string + */ + protected $rawData; + + /** + * Event type + * + * @var string + */ + protected $type = 'response'; + + /** + * Sets the reply code sent by the server. + * + * @param string $code Reply code + * + * @return Phergie_Event_Response Provides a fluent interface + */ + public function setCode($code) + { + $this->code = $code; + return $this; + } + + /** + * Returns the reply code sent by the server. + * + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * Sets the reply code description sent by the server. + * + * @param string $description Reply code description + * + * @return Phergie_Event_Response Provides a fluent interface + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Returns the reply code description sent by the server. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the raw buffer for the given event + * + * @param string $buffer Raw event buffer + * + * @return Phergie_Event_Response Provides a fluent interface + */ + public function setRawData($buffer) + { + $this->rawData = $buffer; + return $this; + } + + /** + * Returns the raw buffer that was sent from the server for that event + * + * @return string + */ + public function getRawData() + { + return $this->rawData; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Exception.php new file mode 100755 index 000000000..f4d71e531 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Exception.php @@ -0,0 +1,33 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for all Phergie-related exceptions. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Exception extends Exception +{ +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Hostmask.php b/plugins/Irc/extlib/phergie/Phergie/Hostmask.php new file mode 100755 index 000000000..b13842f53 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Hostmask.php @@ -0,0 +1,217 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Data structure for a hostmask. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Hostmask +{ + /** + * Host + * + * @var string + */ + protected $host; + + /** + * Nick + * + * @var string + */ + protected $nick; + + /** + * Username + * + * @var string + */ + protected $username; + + /** + * Regular expression used to parse a hostmask + * + * @var string + */ + protected static $regex = '/^([^!@]+)!(?:[ni]=)?([^@]+)@([^ ]+)/'; + + /** + * Constructor to initialize components of the hostmask. + * + * @param string $nick Nick component + * @param string $username Username component + * @param string $host Host component + * + * @return void + */ + public function __construct($nick, $username, $host) + { + $this->nick = $nick; + $this->username = $username; + $this->host = $host; + } + + /** + * Returns whether a given string appears to be a valid hostmask. + * + * @param string $string Alleged hostmask string + * + * @return bool TRUE if the string appears to be a valid hostmask, FALSE + * otherwise + */ + public static function isValid($string) + { + return (preg_match(self::$regex, $string) > 0); + } + + /** + * Parses a string containing the entire hostmask into a new instance of + * this class. + * + * @param string $hostmask Entire hostmask including the nick, username, + * and host components + * + * @return Phergie_Hostmask New instance populated with data parsed from + * the provided hostmask string + * @throws Phergie_Hostmask_Exception + */ + public static function fromString($hostmask) + { + if (preg_match(self::$regex, $hostmask, $match)) { + list(, $nick, $username, $host) = $match; + return new self($nick, $username, $host); + } + + throw new Phergie_Hostmask_Exception( + 'Invalid hostmask specified: "' . $hostmask . '"', + Phergie_Hostmask_Exception::ERR_INVALID_HOSTMASK + ); + } + + /** + * Sets the hostname. + * + * @param string $host Hostname + * + * @return Phergie_Hostmask Provides a fluent interface + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * Returns the hostname. + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Sets the username of the user. + * + * @param string $username Username + * + * @return Phergie_Hostmask Provides a fluent interface + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * Returns the username of the user. + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Sets the nick of the user. + * + * @param string $nick User nick + * + * @return Phergie_Hostmask Provides a fluent interface + */ + public function setNick($nick) + { + $this->nick = $nick; + + return $this; + } + + /** + * Returns the nick of the user. + * + * @return string + */ + public function getNick() + { + return $this->nick; + } + + /** + * Returns the hostmask for the originating server or user. + * + * @return string + */ + public function __toString() + { + return $this->nick . '!' . $this->username . '@' . $this->host; + } + + /** + * Returns whether a given hostmask matches a given pattern. + * + * @param string $pattern Pattern using conventions of a ban mask where + * represents a wildcard + * @param string $hostmask Optional hostmask to match against, if not + * the current hostmask instance + * + * @return bool TRUE if the hostmask matches the pattern, FALSE otherwise + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3 Examples + */ + public function matches($pattern, $hostmask = null) + { + if (!$hostmask) { + $hostmask = (string) $this; + } + + $pattern = str_replace('*', '.*', $pattern); + + return (preg_match('#^' . $pattern . '$#', $hostmask) > 0); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php new file mode 100644 index 000000000..590f02004 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Hostmask/Exception.php @@ -0,0 +1,37 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to hostmask handling. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Hostmask_Exception extends Phergie_Exception +{ + /** + * Error indicating that an invalid hostmask string was specified + */ + const ERR_INVALID_HOSTMASK = 1; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php new file mode 100755 index 000000000..b7105ecdb --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Abstract.php @@ -0,0 +1,605 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for plugins to provide event handler stubs and commonly needed + * functionality. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +abstract class Phergie_Plugin_Abstract +{ + /** + * Current configuration handler + * + * @var Phergie_Config + */ + protected $config; + + /** + * Plugin handler used to provide access to other plugins + * + * @var Phergie_Plugin_Handler + */ + protected $plugins; + + /** + * Current event handler instance for outgoing events + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Current connection instance + * + * @var Phergie_Connection + */ + protected $connection; + + /** + * Current incoming event being handled + * + * @var Phergie_Event_Request|Phergie_Event_Response + */ + protected $event; + + /** + * Plugin short name + * + * @var string + */ + protected $name; + + /** + * Returns the short name for the plugin based on its class name. + * + * @return string + */ + public function getName() + { + if (empty($this->name)) { + $this->name = substr(strrchr(get_class($this), '_'), 1); + } + return $this->name; + } + + /** + * Sets the short name for the plugin. + * + * @param string $name Plugin short name + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setName($name) + { + $this->name = (string) $name; + return $this; + } + + /** + * Indicates that the plugin failed to load due to an unsatisfied + * runtime requirement, such as a missing dependency. + * + * @param string $message Error message to provide more information + * about the reason for the failure + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + * @throws Phergie_Plugin_Exception Always + */ + protected function fail($message) + { + throw new Phergie_Plugin_Exception( + $message, + Phergie_Plugin_Exception::ERR_REQUIREMENT_UNSATISFIED + ); + } + + /** + * Sets the current configuration handler. + * + * @param Phergie_Config $config Configuration handler + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setConfig(Phergie_Config $config) + { + $this->config = $config; + return $this; + } + + /** + * Returns the current configuration handler or the value of a single + * setting from it. + * + * @param string $name Optional name of a setting for which the value + * should be returned instead of the entire configuration handler + * @param mixed $default Optional default value to return if no value + * is set for the setting indicated by $name + * + * @return Phergie_Config|mixed Configuration handler or value of the + * setting specified by $name + * @throws Phergie_Plugin_Exception No configuration handler has been set + */ + public function getConfig($name = null, $default = null) + { + if (empty($this->config)) { + throw new Phergie_Plugin_Exception( + 'Configuration handler cannot be accessed before one is set', + Phergie_Plugin_Exception::ERR_NO_CONFIG_HANDLER + ); + } + if (!is_null($name)) { + if (!isset($this->config[$name])) { + return $default; + } + return $this->config[$name]; + } + return $this->config; + } + + /** + * Sets the current plugin handler. + * + * @param Phergie_Plugin_Handler $handler Plugin handler + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setPluginHandler(Phergie_Plugin_Handler $handler) + { + $this->plugins = $handler; + return $this; + } + + /** + * Returns the current plugin handler. + * + * @return Phergie_Plugin_Handler + * @throws Phergie_Plugin_Exception No plugin handler has been set + */ + public function getPluginHandler() + { + if (empty($this->plugins)) { + throw new Phergie_Plugin_Exception( + 'Plugin handler cannot be accessed before one is set', + Phergie_Plugin_Exception::ERR_NO_PLUGIN_HANDLER + ); + } + return $this->plugins; + } + + /** + * Sets the current event handler. + * + * @param Phergie_Event_Handler $handler Event handler + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setEventHandler(Phergie_Event_Handler $handler) + { + $this->events = $handler; + return $this; + } + + /** + * Returns the current event handler. + * + * @return Phergie_Event_Handler + * @throws Phergie_Plugin_Exception No event handler has been set + */ + public function getEventHandler() + { + if (empty($this->events)) { + throw new Phergie_Plugin_Exception( + 'Event handler cannot be accessed before one is set', + Phergie_Plugin_Exception::ERR_NO_EVENT_HANDLER + ); + } + return $this->events; + } + + /** + * Sets the current connection. + * + * @param Phergie_Connection $connection Connection + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setConnection(Phergie_Connection $connection) + { + $this->connection = $connection; + return $this; + } + + /** + * Returns the current event connection. + * + * @return Phergie_Connection + * @throws Phergie_Plugin_Exception No connection has been set + */ + public function getConnection() + { + if (empty($this->connection)) { + throw new Phergie_Plugin_Exception( + 'Connection cannot be accessed before one is set', + Phergie_Plugin_Exception::ERR_NO_CONNECTION + ); + } + return $this->connection; + } + + /** + * Sets the current incoming event to be handled. + * + * @param Phergie_Event_Request|Phergie_Event_Response $event Event + * + * @return Phergie_Plugin_Abstract Provides a fluent interface + */ + public function setEvent($event) + { + $this->event = $event; + return $this; + } + + /** + * Returns the current incoming event to be handled. + * + * @return Phergie_Event_Request|Phergie_Event_Response + */ + public function getEvent() + { + if (empty($this->event)) { + throw new Phergie_Plugin_Exception( + 'Event cannot be accessed before one is set', + Phergie_Plugin_Exception::ERR_NO_EVENT + ); + } + return $this->event; + } + + /** + * Provides do* methods with signatures identical to those of + * Phergie_Driver_Abstract but that queue up events to be dispatched + * later. + * + * @param string $name Name of the method called + * @param array $args Arguments passed in the call + * + * @return mixed + */ + public function __call($name, array $args) + { + $subcmd = substr($name, 0, 2); + if ($subcmd == 'do') { + $type = strtolower(substr($name, 2)); + $this->getEventHandler()->addEvent($this, $type, $args); + } else if ($subcmd != 'on') { + throw new Phergie_Plugin_Exception( + 'Called invalid method ' . $name . ' in ' . get_class($this), + Phergie_Plugin_Exception::ERR_INVALID_CALL + ); + } + } + + /** + * Handler for when the plugin is initially loaded - useful for checking + * runtime dependencies or performing any setup necessary for the plugin + * to function properly such as initializing a database. + * + * @return void + */ + public function onLoad() + { + } + + /** + * Handler for when the bot initially connects to a server. + * + * @return void + */ + public function onConnect() + { + } + + /** + * Handler for each tick, a single iteration of the continuous loop + * executed by the bot to receive, handle, and send events - useful for + * repeated execution of tasks on a time interval. + * + * @return void + */ + public function onTick() + { + } + + /** + * Handler for when any event is received but has not yet been dispatched + * to the plugin handler method specific to its event type. + * + * @return bool|null|void FALSE to short-circuit further event + * processing, TRUE or NULL otherwise + */ + public function preEvent() + { + } + + /** + * Handler for after plugin processing of an event has concluded but + * before any events triggered in response by plugins are sent to the + * server - useful for modifying outgoing events before they are sent. + * + * @return void + */ + public function preDispatch() + { + } + + /** + * Handler for after any events triggered by plugins in response to a + * received event are sent to the server. + * + * @return void + */ + public function postDispatch() + { + } + + /** + * Handler for when the server prompts the client for a nick. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_2 + */ + public function onNick() + { + } + + /** + * Handler for when a user obtains operator privileges. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_5 + */ + public function onOper() + { + } + + /** + * Handler for when the client session is about to be terminated. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_1_6 + */ + public function onQuit() + { + } + + /** + * Handler for when a user joins a channel. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_1 + */ + public function onJoin() + { + } + + /** + * Handler for when a user leaves a channel. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_2 + */ + public function onPart() + { + } + + /** + * Handler for when a user or channel mode is changed. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3 + */ + public function onMode() + { + } + + /** + * Handler for when a channel topic is viewed or changed. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_4 + */ + public function onTopic() + { + } + + /** + * Handler for when a message is received from a channel or user. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_4_1 + */ + public function onPrivmsg() + { + } + + /** + * Handler for when the bot receives a CTCP ACTION request. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.4 + */ + public function onAction() + { + } + + /** + * Handler for when a notice is received. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_4_2 + */ + public function onNotice() + { + } + + /** + * Handler for when a user is kicked from a channel. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_8 + */ + public function onKick() + { + } + + /** + * Handler for when the bot receives a ping event from a server, at + * which point it is expected to respond with a pong request within + * a short period else the server may terminate its connection. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_2 + */ + public function onPing() + { + } + + /** + * Handler for when the bot receives a CTCP TIME request. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.6 + */ + public function onTime() + { + } + + /** + * Handler for when the bot receives a CTCP VERSION request. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.1 + */ + public function onVersion() + { + } + + /** + * Handler for when the bot receives a CTCP PING request. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.2 + */ + public function onCtcpPing() + { + } + + /** + * Handler for when the bot receives a CTCP request of an unknown type. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html + */ + public function onCtcp() + { + } + + /** + * Handler for when a reply is received for a CTCP PING request sent by + * the bot. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.2 + */ + public function onPingReply() + { + } + + /** + * Handler for when a reply is received for a CTCP TIME request sent by + * the bot. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.6 + */ + public function onTimeReply() + { + } + + /** + * Handler for when a reply is received for a CTCP VERSION request sent + * by the bot. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html#4.1 + */ + public function onVersionReply() + { + } + + /** + * Handler for when a reply received for a CTCP request of an unknown + * type. + * + * @return void + * @link http://www.invlogic.com/irc/ctcp.html + */ + public function onCtcpReply() + { + } + + /** + * Handler for when the bot receives a kill request from a server. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_1 + */ + public function onKill() + { + } + + /** + * Handler for when the bot receives an invitation to join a channel. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_7 + */ + public function onInvite() + { + } + + /** + * Handler for when a server response is received to a command issued by + * the bot. + * + * @return void + * @link http://irchelp.org/irchelp/rfc/chapter6.html + */ + public function onResponse() + { + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php new file mode 100755 index 000000000..e209e32e1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Acl.php @@ -0,0 +1,186 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Acl + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Acl + */ + +/** + * Provides an access control system to limit reponses to events based on + * the users who originate them. + * + * Configuration settings: + * acl.whitelist - mapping of user hostmask patterns (optionally by host) to + * plugins and methods where those plugins and methods will + * only be accessible to those users (i.e. and inaccessible + * to other users) + * acl.blacklist - mapping of user hostmasks (optionally by host) to plugins + * and methods where where those plugins and methods will be + * inaccessible to those users but accessible to other users + * acl.ops - TRUE to automatically give access to whitelisted plugins + * and methods to users with ops for events they initiate in + * channels where they have ops + * + * The whitelist and blacklist settings are formatted like so: + * <code> + * 'acl.whitelist' => array( + * 'hostname1' => array( + * 'pattern1' => array( + * 'plugins' => array( + * 'ShortPluginName' + * ), + * 'methods' => array( + * 'methodName' + * ) + * ), + * ) + * ), + * </code> + * + * The hostname array dimension is optional; if not used, rules will be + * applied across all connections. The pattern is a user hostmask pattern + * where asterisks (*) are used for wildcards. Plugins and methods do not + * need to be set to empty arrays if they are not used; simply exclude them. + * + * @category Phergie + * @package Phergie_Plugin_Acl + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Acl + * @uses Phergie_Plugin_UserInfo pear.phergie.org + */ +class Phergie_Plugin_Acl extends Phergie_Plugin_Abstract +{ + /** + * Checks for permission settings and removes the plugin if none are set. + * + * @return void + */ + public function onLoad() + { + $this->plugins->getPlugin('UserInfo'); + + if (!$this->getConfig('acl.blacklist') + && !$this->getConfig('acl.whitelist') + ) { + $this->plugins->removePlugin($this); + } + } + + /** + * Applies a set of rules to a plugin handler iterator. + * + * @param Phergie_Plugin_Iterator $iterator Iterator to receive rules + * @param array $rules Associate array containing + * either a 'plugins' key pointing to an array containing plugin + * short names to filter, a 'methods' key pointing to an array + * containing method names to filter, or both + * + * @return void + */ + protected function applyRules(Phergie_Plugin_Iterator $iterator, array $rules) + { + if (!empty($rules['plugins'])) { + $iterator->addPluginFilter($rules['plugins']); + } + if (!empty($rules['methods'])) { + $iterator->addMethodFilter($rules['methods']); + } + } + + /** + * Checks permission settings and short-circuits event processing for + * blacklisted users. + * + * @return void + */ + public function preEvent() + { + // Ignore server responses + if ($this->event instanceof Phergie_Event_Response) { + return; + } + + // Ignore server-initiated events + if (!$this->event->isFromUser()) { + return; + } + + // Get the iterator used to filter plugins when processing events + $iterator = $this->plugins->getIterator(); + + // Get configuration setting values + $whitelist = $this->getConfig('acl.whitelist', array()); + $blacklist = $this->getConfig('acl.blacklist', array()); + $ops = $this->getConfig('acl.ops', false); + + // Support host-specific lists + $host = $this->connection->getHost(); + foreach (array('whitelist', 'blacklist') as $var) { + foreach ($$var as $pattern => $rules) { + $regex = '/^' . str_replace('*', '.*', $pattern) . '$/i'; + if (preg_match($regex, $host)) { + ${$var} = ${$var}[$pattern]; + break; + } + } + } + + // Get information on the user initiating the current event + $hostmask = $this->event->getHostmask(); + $isOp = $ops + && $this->event->isInChannel() + && $this->plugins->userInfo->isOp( + $this->event->getNick(), + $this->event->getSource() + ); + + // Filter whitelisted commands if the user is not on the whitelist + if (!$isOp) { + $whitelisted = false; + foreach ($whitelist as $pattern => $rules) { + if ($hostmask->matches($pattern)) { + $whitelisted = true; + } + } + if (!$whitelisted) { + foreach ($whitelist as $pattern => $rules) { + $this->applyRules($iterator, $rules); + } + } + } + + // Filter blacklisted commands if the user is on the blacklist + $blacklisted = false; + foreach ($blacklist as $pattern => $rules) { + if ($hostmask->matches($pattern)) { + $this->applyRules($iterator, $rules); + break; + } + } + } + + /** + * Clears filters on the plugin handler iterator. + * + * @return void + */ + public function postDispatch() + { + $this->plugins->getIterator()->clearFilters(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php new file mode 100755 index 000000000..16d5f9b9c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/AltNick.php @@ -0,0 +1,95 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_AltNick + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AltNick + */ + +/** + * Handles switching to alternate nicks in cases where the primary nick is + * not available for use. + * + * @category Phergie + * @package Phergie_Plugin_AltNick + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AltNick + * @uses extension spl + */ +class Phergie_Plugin_AltNick extends Phergie_Plugin_Abstract +{ + /** + * Iterator for the alternate nick list + * + * @var ArrayIterator + */ + protected $iterator; + + /** + * Initializes instance variables. + * + * @return void + */ + public function onConnect() + { + if ($this->config['altnick.nicks']) { + if (is_string($this->config['altnick.nicks'])) { + $this->config['altnick.nicks'] + = array($this->config['altnick.nicks']); + } + $this->iterator = new ArrayIterator($this->config['altnick.nicks']); + } + } + + /** + * Switches to alternate nicks as needed when nick collisions occur. + * + * @return void + */ + public function onResponse() + { + // If no alternate nick list was found, return + if (empty($this->iterator)) { + return; + } + + // If the response event indicates that the nick set is in use... + $code = $this->getEvent()->getCode(); + if ($code == Phergie_Event_Response::ERR_NICKNAMEINUSE) { + + // Attempt to move to the next nick in the alternate nick list + $this->iterator->next(); + + // If another nick is available... + if ($this->iterator->valid()) { + + // Switch to the new nick + $altNick = $this->iterator->current(); + $this->doNick($altNick); + + // Update the connection to reflect the nick change + $this->getConnection()->setNick($altNick); + + } else { + // If no other nicks are available... + + // Terminate the connection + $this->doQuit('All specified alternate nicks are in use'); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php new file mode 100755 index 000000000..ed4030a28 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/AudioScrobbler.php @@ -0,0 +1,191 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_AudioScrobbler + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AudioScrobbler + */ + +/** + * Provides commands to look up information on tracks played by specific + * users on the Last.fm and Libre.fm services. + * + * TODO: Make the "nick-binding" use an SQLite database instead of having them + * hard-coded in to the config file. + * + * Configuration settings: + * "audioscrobbler.lastfm_api_key": API given by last.fm (string). + * "audioscrobbler.librefm_api_key": API key given by libre.fm (string). + * + * @category Phergie + * @package Phergie_Plugin_AudioScrobbler + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AudioScrobbler + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + * @uses extension simplexml + */ +class Phergie_Plugin_AudioScrobbler extends Phergie_Plugin_Abstract +{ + /** + * Last.FM API entry point + * + * @var string + */ + protected $lastfmUrl = 'http://ws.audioscrobbler.com/2.0/'; + + /** + * Libre.FM API entry point + * + * @var string + */ + protected $librefmUrl = 'http://alpha.dev.libre.fm/2.0/'; + + /** + * Scrobbler query string for user.getRecentTracks + * + * @var string + */ + protected $query = '?method=user.getrecenttracks&user=%s&api_key=%s'; + + /** + * HTTP plugin + * + * @var Phergie_Plugin_Http + */ + protected $http; + + /** + * Check for dependencies. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('simplexml')) { + $this->fail('SimpleXML php extension is required'); + } + + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $this->http = $plugins->getPlugin('Http'); + } + + /** + * Command function to get a user's status on last.fm. + * + * @param string $user User identifier + * + * @return void + */ + public function onCommandLastfm($user = null) + { + if ($key = $this->config['audioscrobbler.lastfm_api_key']) { + $scrobbled = $this->getScrobbled($user, $this->lastfmUrl, $key); + if ($scrobbled) { + $this->doPrivmsg($this->getEvent()->getSource(), $scrobbled); + } + } + } + + /** + * Command function to get a user's status on libre.fm. + * + * @param string $user User identifier + * + * @return void + */ + public function onCommandLibrefm($user = null) + { + if ($key = $this->config['audioscrobbler.librefm_api_key']) { + $scrobbled = $this->getScrobbled($user, $this->librefmUrl, $key); + if ($scrobbled) { + $this->doPrivmsg($this->getEvent()->getSource(), $scrobbled); + } + } + } + + /** + * Simple Scrobbler API function to get a formatted string of the most + * recent track played by a user. + * + * @param string $user Username to look up + * @param string $url Base URL of the scrobbler service + * @param string $key Scrobbler service API key + * + * @return string Formatted string of the most recent track played + */ + public function getScrobbled($user, $url, $key) + { + $event = $this->getEvent(); + $user = $user ? $user : $event->getNick(); + $url = sprintf($url . $this->query, urlencode($user), urlencode($key)); + + $response = $this->http->get($url); + if ($response->isError()) { + $this->doNotice( + $event->getNick(), + 'Can\'t find status for ' . $user . ': HTTP ' . + $response->getCode() . ' ' . $response->getMessage() + ); + return false; + } + + $xml = $response->getContent(); + if ($xml->error) { + $this->doNotice( + $event->getNick(), + 'Can\'t find status for ' . $user . ': API ' . $xml->error + ); + return false; + } + + $recenttracks = $xml->recenttracks; + $track = $recenttracks->track[0]; + + // If the user exists but has not scrobbled anything, the result will + // be empty. + if (empty($track->name) && empty($track->artist)) { + $this->doNotice( + $event->getNick(), + 'Can\'t find track information for ' . $recenttracks['user'] + ); + return false; + } + + if (isset($track['nowplaying'])) { + $msg = sprintf( + '%s is listening to %s by %s', + $recenttracks['user'], + $track->name, + $track->artist + ); + } else { + $msg = sprintf( + '%s, %s was listening to %s by %s', + date('j M Y, H:i', (int) $track->date['uts']), + $recenttracks['user'], + $track->name, + $track->artist + ); + } + if ($track->streamable == 1) { + $msg .= ' - ' . $track->url; + } + return $msg; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php new file mode 100755 index 000000000..a6d1adfbb --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/AutoJoin.php @@ -0,0 +1,69 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_AutoJoin + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AutoJoin + */ + +/** + * Automates the process of having the bot join one or more channels upon + * connection to the server. + * + * The configuration setting autojoin.channels is used to determine which + * channels to join. This setting can point to a comma-delimited string or + * enumerated array containing a single list of channels or an associative + * array keyed by hostname where each value is a comma-delimited string or + * enumerated array containing a list of channels to join on the server + * corresponding to that hostname. + * + * @category Phergie + * @package Phergie_Plugin_AutoJoin + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_AutoJoin + */ +class Phergie_Plugin_AutoJoin extends Phergie_Plugin_Abstract +{ + /** + * Intercepts the end of the "message of the day" response and responds by + * joining the channels specified in the configuration file. + * + * @return void + */ + public function onResponse() + { + switch ($this->getEvent()->getCode()) { + case Phergie_Event_Response::RPL_ENDOFMOTD: + case Phergie_Event_Response::ERR_NOMOTD: + if ($channels = $this->config['autojoin.channels']) { + if (is_array($channels)) { + // Support autojoin.channels being in these formats: + // 'hostname' => array('#channel1', '#channel2', ... ) + $host = $this->getConnection()->getHost(); + if (isset($channels[$host])) { + $channels = $channels[$host]; + } + if (is_array($channels)) { + $channels = implode(',', $channels); + } + } + $this->doJoin($channels); + } + $this->getPluginHandler()->removePlugin($this); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php new file mode 100644 index 000000000..7213cdee8 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer.php @@ -0,0 +1,82 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Beer + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Beer + */ + +/** + * Processes requests to serve users beer. + * + * @category Phergie + * @package Phergie_Plugin_Beer + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Beer + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Beer extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user a beer. + * + * @param string $request Request including the target and an optional + * suggestion of what beer to serve + * + * @return void + */ + public function onCommandBeer($request) + { + $format = $this->getConfig( + 'beer.format', + 'throws %target% %article% %item%.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Beer/beer.db', + 'beer', + $format, + $request + ); + } + + /** + * Adds a "booze" alias for the "beer" command. + * + * @param string $request Request including the target and an optional + * suggestion of what beer to serve + * + * @return void + */ + public function onCommandBooze($request) + { + $this->onCommandBeer($request); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php new file mode 100644 index 000000000..c7921e519 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Beer/db.php @@ -0,0 +1,81 @@ +<?php + +if (!defined('__DIR__')) { + define('__DIR__', dirname(__FILE__)); +} + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/beer.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE beer (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX beer_name ON beer (name)'); +$insert = $db->prepare('INSERT INTO beer (name, link) VALUES (:name, :link)'); + +// Get raw beerme.com data set +echo 'Downloading beerme.com data set', PHP_EOL; +$file = __DIR__ . '/beerlist.txt'; +if (!file_exists($file)) { + copy('http://beerme.com/beerlist.php', $file); +} +$contents = file_get_contents($file); + +// Extract data from data set +echo 'Processing beerme.com data', PHP_EOL; +$contents = tidy_repair_string($contents); +libxml_use_internal_errors(true); +$doc = new DOMDocument; +$doc->loadHTML($contents); +libxml_clear_errors(); +$xpath = new DOMXPath($doc); +$beers = $xpath->query('//table[@class="beerlist"]/tr/td[1]'); +$db->beginTransaction(); +foreach ($beers as $beer) { + $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $beer->textContent); + $name = preg_replace('/\h*\v+\h*/', '', $name); + $link = 'http://beerme.com' . $beer->childNodes->item(1)->getAttribute('href'); + $insert->execute(array($name, $link)); +} +$db->commit(); + +// Clean up +echo 'Cleaning up', PHP_EOL; +unlink($file); + +// Get and decompress openbeerdb.com data set +$archive = __DIR__ . '/beers.zip'; +if (!file_exists($archive)) { + echo 'Downloading openbeerdb.com data set', PHP_EOL; + copy('http://openbeerdb.googlecode.com/files/beers.zip', $archive); +} + +echo 'Decompressing openbeerdb.com data set', PHP_EOL; +$zip = new ZipArchive; +$zip->open($archive); +$zip->extractTo(__DIR__, 'beers/beers.csv'); +$zip->close(); +$file = __DIR__ . '/beers/beers.csv'; + +// Extract data from data set +echo 'Processing openbeerdb.com data', PHP_EOL; +$fp = fopen($file, 'r'); +$columns = fgetcsv($fp, 0, '|'); +$db->beginTransaction(); +while ($line = fgetcsv($fp, 0, '|')) { + $line = array_combine($columns, $line); + $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $line['name']); + $name = preg_replace('/\h*\v+\h*/', '', $name); + $link = null; + $insert->execute(array($name, $link)); +} +$db->commit(); +fclose($fp); + +// Clean up +echo 'Cleaning up', PHP_EOL; +unlink($file); +unlink($archive); +rmdir(__DIR__ . '/beers'); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php new file mode 100644 index 000000000..16c671f68 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/BeerScore.php @@ -0,0 +1,156 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_BeerScore + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_BeerScore + */ + +/** + * Handles incoming requests for beer scores. + * + * @category Phergie + * @package Phergie_Plugin_BeerScore + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_BeerScore + * @uses Phergie_Plugin_Http pear.phergie.org + */ +class Phergie_Plugin_BeerScore extends Phergie_Plugin_Abstract +{ + /** + * Score result type + * + * @const string + */ + const TYPE_SCORE = 'SCORE'; + + /** + * Search result type + * + * @const string + */ + const TYPE_SEARCH = 'SEARCH'; + + /** + * Refine result type + * + * @const type + */ + const TYPE_REFINE = 'REFINE'; + + /** + * Base API URL + * + * @const string + */ + const API_BASE_URL = 'http://caedmon.net/beerscore/'; + + /** + * HTTP plugin + * + * @var Phergie_Plugin_Http + */ + protected $http; + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->http = $this->getPluginHandler()->getPlugin('Http'); + } + + /** + * Handles beerscore commands. + * + * @param string $searchstring String to use in seaching for beer scores + * + * @return void + */ + public function onCommandBeerscore($searchstring) + { + $event = $this->getEvent(); + $target = $event->getNick(); + $source = $event->getSource(); + + $apiurl = self::API_BASE_URL . rawurlencode($searchstring); + $response = $this->http->get($apiurl); + + if ($response->isError()) { + $this->doNotice($target, 'Score not found (or failed to contact API)'); + return; + } + + $result = $response->getContent(); + switch ($result->type) { + case self::TYPE_SCORE: + // small enough number to get scores + foreach ($result->beer as $beer) { + if ($beer->score === -1) { + $score = '(not rated)'; + } else { + $score = $beer->score; + } + $str + = "{$target}: rating for {$beer->name}" . + " = {$score} ({$beer->url})"; + $this->doPrivmsg($source, $str); + } + break; + + case self::TYPE_SEARCH: + // only beer names, no scores + $str = ''; + $found = 0; + foreach ($result->beer as $beer) { + if (isset($beer->score)) { + ++$found; + if ($beer->score === -1) { + $score = '(not rated)'; + } else { + $score = $beer->score; + } + $str + = "{$target}: rating for {$beer->name}" . + " = {$score} ({$beer->url})"; + $this->doPrivmsg($source, $str); + } else { + $str .= "({$beer->name} -> {$beer->url}) "; + } + } + $foundnum = $result->num - $found; + $more = $found ? 'more ' : ''; + $str = "{$target}: {$foundnum} {$more}results... {$str}"; + $this->doPrivmsg($source, $str); + break; + + case self::TYPE_REFINE: + // Too many results; only output search URL + if ($result->num < 100) { + $num = $result->num; + } else { + $num = 'at least 100'; + } + $resultsword = (($result->num > 1) ? 'results' : 'result'); + $str = "{$target}: {$num} {$resultsword}; {$result->searchurl}"; + $this->doPrivmsg($source, $str); + break; + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php new file mode 100644 index 000000000..2b54fab68 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cache.php @@ -0,0 +1,106 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Cache + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cache + */ + +/** + * Implements a generic cache to be used by other plugins. + * + * @category Phergie + * @package Phergie_Plugin_Cache + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cache + */ +class Phergie_Plugin_Cache extends Phergie_Plugin_Abstract +{ + /** + * Key-value data storage for the cache + * + * @var array + */ + protected $cache = array(); + + /** + * Stores a value in the cache. + * + * @param string $key Key to associate with the value + * @param mixed $data Data to be stored + * @param int|null $ttl Time to live in seconds or NULL for forever + * @param bool $overwrite TRUE to overwrite any existing value + * associated with the specified key + * + * @return bool + */ + public function store($key, $data, $ttl = 3600, $overwrite = true) + { + if (!$overwrite && isset($this->cache[$key])) { + return false; + } + + if ($ttl) { + $expires = time()+$ttl; + } else { + $expires = null; + } + + $this->cache[$key] = array('data' => $data, 'expires' => $expires); + return true; + + } + + /** + * Fetches a previously stored value. + * + * @param string $key Key associated with the value + * + * @return mixed Stored value or FALSE if no value or an expired value + * is associated with the specified key + */ + public function fetch($key) + { + if (!isset($this->cache[$key])) { + return false; + } + + $item = $this->cache[$key]; + if (!is_null($item['expires']) && $item['expires'] < time()) { + $this->expire($key); + return false; + } + + return $item['data']; + } + + /** + * Expires a value that has exceeded its time to live. + * + * @param string $key Key associated with the value to expire + * + * @return bool + */ + protected function expire($key) + { + if (!isset($this->cache[$key])) { + return false; + } + unset($this->cache[$key]); + return true; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php new file mode 100644 index 000000000..2b76bd312 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine.php @@ -0,0 +1,69 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Caffeine + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Caffeine + */ + +/** + * Processes requests to serve users caffeinated beverages. + * + * @category Phergie + * @package Phergie_Plugin_Caffeine + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Caffeine + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Caffeine extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user a caffeinated beverage. + * + * @param string $request Request including the target and an optional + * suggestion of what caffeinated beverage to serve + * + * @return void + */ + public function onCommandCaffeine($request) + { + $format = $this->getConfig( + 'beer.format', + 'throws %target% %article% %item%.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Caffeine/caffeine.db', + 'caffeine', + $format, + $request + ); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php new file mode 100644 index 000000000..cdff52fd8 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Caffeine/db.php @@ -0,0 +1,51 @@ +<?php + +if (!defined('__DIR__')) { + define('__DIR__', dirname(__FILE__)); +} + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/caffeine.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE caffeine (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX caffeine_name ON caffeine (name)'); +$insert = $db->prepare('INSERT INTO caffeine (name, link) VALUES (:name, :link)'); + +// Get raw energyfiend.com data set +echo 'Downloading energyfiend.com data set', PHP_EOL; +$file = __DIR__ . '/the-caffeine-database.html'; +if (!file_exists($file)) { + copy('http://www.energyfiend.com/the-caffeine-database', $file); +} +$contents = file_get_contents($file); + +// Extract data from data set +echo 'Processing energyfiend.com data', PHP_EOL; +$contents = tidy_repair_string($contents); +libxml_use_internal_errors(true); +$doc = new DOMDocument; +$doc->loadHTML($contents); +libxml_clear_errors(); +$xpath = new DOMXPath($doc); +$caffeine = $xpath->query('//table[@id="caffeinedb"]//tr/td[1]'); +$db->beginTransaction(); +foreach ($caffeine as $drink) { + $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $drink->textContent); + $name = preg_replace('/\s*\v+\s*/', ' ', $name); + if ($drink->firstChild->nodeName == 'a') { + $link = 'http://energyfiend.com' + . $drink->firstChild->getAttribute('href'); + } else { + $link = null; + } + $insert->execute(array($name, $link)); +} +$db->commit(); + +// Clean up +echo 'Cleaning up', PHP_EOL; +unlink($file); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php new file mode 100755 index 000000000..99c69d873 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Censor.php @@ -0,0 +1,120 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Censor + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Censor + */ + +/** + * Facilitates censoring of event content or discardment of events + * containing potentially offensive phrases depending on the value of the + * configuration setting censor.mode ('off', 'censor', 'discard'). Also + * provides access to a web service for detecting censored words so that + * other plugins may optionally integrate and adjust behavior accordingly to + * prevent discardment of events. + * + * @category Phergie + * @package Phergie_Plugin_Censor + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Censor + * @uses extension soap + */ +class Phergie_Plugin_Censor extends Phergie_Plugin_Abstract +{ + /** + * SOAP client to interact with the CDYNE Profanity Filter API + * + * @var SoapClient + */ + protected $soap; + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('soap')) { + $this->fail('The PHP soap extension is required'); + } + + if (!in_array($this->config['censor.mode'], array('censor', 'discard'))) { + $this->plugins->removePlugin($this); + } + } + + /** + * Returns a "clean" version of a given string. + * + * @param string $string String to clean + * + * @return string Cleaned string + */ + public function cleanString($string) + { + if (empty($this->soap)) { + $this->soap = new SoapClient('http://ws.cdyne.com/ProfanityWS/Profanity.asmx?wsdl'); + } + $params = array('Text' => $string); + $attempts = 0; + while ($attempts < 3) { + try { + $response = $this->soap->SimpleProfanityFilter($params); + break; + } catch (SoapFault $e) { + $attempts++; + sleep(1); + } + } + if ($attempts == 3) { + return $string; + } + return $response->SimpleProfanityFilterResult->CleanText; + } + + /** + * Processes events before they are dispatched and either censors their + * content or discards them if they contain potentially offensive + * content. + * + * @return void + */ + public function preDispatch() + { + $events = $this->events->getEvents(); + + foreach ($events as $event) { + switch ($event->getType()) { + case Phergie_Event_Request::TYPE_PRIVMSG: + case Phergie_Event_Request::TYPE_ACTION: + case Phergie_Event_Request::TYPE_NOTICE: + $text = $event->getArgument(1); + $clean = $this->cleanString($text); + if ($text != $clean) { + if ($this->config['censor.mode'] == 'censor') { + $event->setArgument(1, $clean); + } else { + $this->events->removeEvent($event); + } + } + break; + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php new file mode 100644 index 000000000..eafeb657b --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail.php @@ -0,0 +1,71 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Cocktail + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cocktail + */ + +/** + * Processes requests to serve users cocktail. + * + * @category Phergie + * @package Phergie_Plugin_Cocktail + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cocktail + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Cocktail extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user a cocktail. + * + * @param string $request Request including the target and an optional + * suggestion of what cocktail to serve + * + * @return void + */ + public function onCommandCocktail($request) + { + $format = $this->getConfig( + 'cocktail.format', + 'throws %target% %article% %item%.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Cocktail/cocktail.db', + 'cocktail', + $format, + $request, + true + ); + } +} + diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php new file mode 100644 index 000000000..2e61dd0bd --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cocktail/db.php @@ -0,0 +1,74 @@ +<?php + +if (!defined('__DIR__')) { + define('__DIR__', dirname(__FILE__)); +} + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/cocktail.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE cocktail (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX cocktail_name ON cocktail (name)'); +$insert = $db->prepare('INSERT INTO cocktail (name, link) VALUES (:name, :link)'); + +// Get raw webtender.com data set +echo 'Downloading webtender.com data set', PHP_EOL; +$start = 1; +do { + $file = __DIR__ . '/' . $start . '.html'; + if (file_exists($file)) { + continue; + } + copy( + 'http://www.webtender.com/db/browse?level=2&dir=drinks&char=%2A&start=' . $start, + $file + ); + if (!isset($limit)) { + $contents = file_get_contents($file); + preg_match('/([0-9]+) found/', $contents, $match); + $limit = $match[1] + (150 - ($match[1] % 150)); + } + echo 'Got records ', $start, ' - ', min($start + 150, $limit), ' of ', $limit, PHP_EOL; + $start += 150; +} while ($start < $limit); + +// Extract data from data set +$start = 1; +while ($start < $limit) { + echo 'Processing ', $start, ' - ', min($start + 150, $limit), ' of ', $limit, PHP_EOL; + + $file = __DIR__ . '/' . $start . '.html'; + $contents = file_get_contents($file); + $contents = tidy_repair_string($contents); + libxml_use_internal_errors(true); + $doc = new DOMDocument; + $doc->loadHTML($contents); + libxml_clear_errors(); + $xpath = new DOMXPath($doc); + + $cocktails = $xpath->query('//li/a'); + $db->beginTransaction(); + foreach ($cocktails as $cocktail) { + $name = $cocktail->nodeValue; + $name = preg_replace('/ The$|^The |\s*\([^)]+\)\s*| #[0-9]+$/', '', $name); + $name = html_entity_decode($name); + $link = 'http://www.webtender.com' . $cocktail->getAttribute('href'); + $insert->execute(array($name, $link)); + } + $db->commit(); + + $start += 150; +} + +// Clean up +echo 'Cleaning up', PHP_EOL; +$start = 1; +while ($start < $limit) { + $file = __DIR__ . '/' . $start . '.html'; + unlink($file); + $start += 150; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php new file mode 100644 index 000000000..2058977ed --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Command.php @@ -0,0 +1,146 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Command + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Command + */ + +/** + * Handles parsing and execution of commands sent by users via messages sent + * to channels in which the bot is present or directly to the bot. + * + * @category Phergie + * @package Phergie_Plugin_Command + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Command + * @uses extension reflection + * @uses Phergie_Plugin_Message pear.phergie.org + */ +class Phergie_Plugin_Command extends Phergie_Plugin_Abstract +{ + /** + * Prefix for command method names + * + * @var string + */ + const METHOD_PREFIX = 'onCommand'; + + /** + * Cache for command lookups used to confirm that methods exist and + * parameter counts match + * + * @var array + */ + protected $methods = array(); + + /** + * Load the Message plugin + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Message'); + } + + /** + * Populates the methods cache. + * + * @return void + */ + public function populateMethodCache() + { + foreach ($this->getPluginHandler()->getPlugins() as $plugin) { + $reflector = new ReflectionClass($plugin); + foreach ($reflector->getMethods() as $method) { + $name = $method->getName(); + if (strpos($name, self::METHOD_PREFIX) === 0 + && !isset($this->methods[$name]) + ) { + $this->methods[$name] = array( + 'total' => $method->getNumberOfParameters(), + 'required' => $method->getNumberOfRequiredParameters() + ); + } + } + } + } + + /** + * Parses a given message and, if its format corresponds to that of a + * defined command, calls the handler method for that command with any + * provided parameters. + * + * @return void + */ + public function onPrivmsg() + { + // Populate the methods cache if needed + if (empty($this->methods)) { + $this->populateMethodCache(); + } + + // Check for a prefixed message + $msg = $this->plugins->message->getMessage(); + if ($msg === false) { + return; + } + + // Separate the command and arguments + $parsed = preg_split('/\s+/', $msg, 2); + $command = strtolower(array_shift($parsed)); + $args = count($parsed) ? array_shift($parsed) : ''; + + // Resolve aliases to their corresponding commands + $aliases = $this->getConfig('command.aliases', array()); + $result = preg_grep('/^' . preg_quote($command, '/') . '$/i', array_keys($aliases)); + if ($result) { + $command = $aliases[array_shift($result)]; + } + + // Check to ensure the command exists + $method = self::METHOD_PREFIX . ucfirst($command); + if (empty($this->methods[$method])) { + return; + } + + // If no arguments are passed... + if (empty($args)) { + + // If the method requires no arguments, call it + if (empty($this->methods[$method]['required'])) { + $this->getPluginHandler()->$method(); + } + + } else { + // If arguments are passed... + + // Parse the arguments + $args = preg_split('/\s+/', $args, $this->methods[$method]['total']); + + // If the minimum arguments are passed, call the method + if ($this->methods[$method]['required'] <= count($args)) { + call_user_func_array( + array($this->getPluginHandler(), $method), + $args + ); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php new file mode 100644 index 000000000..4bc2ceecb --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie.php @@ -0,0 +1,69 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Cookie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cookie + */ + +/** + * Processes requests to serve users cookies. + * + * @category Phergie + * @package Phergie_Plugin_Cookie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cookie + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Cookie extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user a cookie. + * + * @param string $request Request including the target and an optional + * suggestion of what cookie to serve + * + * @return void + */ + public function onCommandCookie($request) + { + $format = $this->getConfig( + 'cookie.format', + 'throws %target% %article% %item%.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Cookie/cookie.db', + 'cookies', + $format, + $request + ); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php new file mode 100644 index 000000000..27763159c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cookie/db.php @@ -0,0 +1,55 @@ +<?php + +if (!defined('__DIR__')) { + define('__DIR__', dirname(__FILE__)); +} + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/cookie.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE cookies (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX cookie_name ON cookies (name)'); +$insert = $db->prepare('INSERT INTO cookies (name, link) VALUES (:name, :link)'); + +// Get Cookies list from http://en.wikipedia.org/wiki/List_of_cookies +echo 'Downloading data from Wikipedia', PHP_EOL; +$file = __DIR__ . '/cookieslist.txt'; +if (!file_exists($file)) { + copy('http://en.wikipedia.org/wiki/List_of_cookies', $file); +} +$contents = file_get_contents($file); + +// Extract data from data set +echo 'Processing Wikipedia\'s cookies list', PHP_EOL; +$contents = tidy_repair_string($contents); +libxml_use_internal_errors(true); +$doc = new DOMDocument; +$doc->loadHTML($contents); +libxml_clear_errors(); +$xpath = new DOMXPath($doc); + +$cookies = $xpath->query('//table[@width="90%"]/tr/td[1]/a'); + +foreach ($cookies as $cookie) { + $name = $cookie->textContent; + $name = str_replace( + array('(',')',"\n", 'cookies'), + array('','', ' ', 'cookie'), + $name + ); + $name = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $name); + $name = trim($name); + $name = rtrim($name, 's'); + + $link = 'http://en.wikipedia.org' . $cookie->getAttribute('href'); + $insert->execute(array($name, $link)); + echo 'added [' . $name . '] -> '. $link . PHP_EOL; +} + +// Clean up +echo 'Cleaning up', PHP_EOL; +unlink($file); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php new file mode 100644 index 000000000..d24910fa3 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php @@ -0,0 +1,147 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Cron + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cron + */ + +/** + * Allows callbacks to be registered for asynchronous execution. + * + * @category Phergie + * @package Phergie_Plugin_Cron + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Cron + */ +class Phergie_Plugin_Cron extends Phergie_Plugin_Abstract +{ + /** + * Array of all registered callbacks with delays and arguments + * + * @var array + */ + protected $callbacks; + + /** + * Returns a human-readable representation of a callback for debugging + * purposes. + * + * @param callback $callback Callback to analyze + * + * @return string|boolean String representation of the callback or FALSE + * if the specified value is not a valid callback + */ + protected function getCallbackString($callback) + { + if (!is_callable($callback)) { + return false; + } + + if (is_array($callback)) { + $class = is_string($callback[0]) ? + $callback[0] : get_class($callback[0]); + $method = $class . '::' . $callback[1]; + return $method; + } + + return $callback; + } + + /** + * Registers a callback for execution sometime after a given delay + * relative to now. + * + * @param callback $callback Callback to be registered + * @param int $delay Delay in seconds from now when the callback + * will be executed + * @param array $arguments Arguments to pass to the callback when + * it's executed + * @param bool $repeat TRUE to automatically re-register the + * callback for the same delay after it's executed, FALSE + * otherwise + * + * @return void + */ + public function registerCallback($callback, $delay, + array $arguments = array(), $repeat = false) + { + $callbackString = $this->getCallbackString($callback); + if ($callbackString === false) { + echo 'DEBUG(Cron): Invalid callback specified - ', + var_export($callback, true), PHP_EOL; + return; + } + + $registered = time(); + $scheduled = $registered + $delay; + + $this->callbacks[] = array( + 'callback' => $callback, + 'delay' => $delay, + 'arguments' => $arguments, + 'registered' => $registered, + 'scheduled' => $scheduled, + 'repeat' => $repeat, + ); + + echo 'DEBUG(Cron): Callback ', $callbackString, + ' scheduled for ', date('H:i:s', $scheduled), PHP_EOL; + } + + /** + * Handles callback execution. + * + * @return void + */ + public function onTick() + { + $time = time(); + foreach ($this->callbacks as $key => &$callback) { + $callbackString = $this->getCallbackString($callback); + + $scheduled = $callback['scheduled']; + if ($time < $scheduled) { + continue; + } + + if (empty($callback['arguments'])) { + call_user_func($callback['callback']); + } else { + call_user_func_array( + $callback['callback'], + $callback['arguments'] + ); + } + + echo 'DEBUG(Cron): Callback ', $callbackString, + ' scheduled for ', date('H:i:s', $scheduled), ',', + ' executed at ', date('H:i:s', $now), PHP_EOL; + + if ($callback['repeat']) { + $callback['scheduled'] = $time + $callback['delay']; + echo 'DEBUG(Cron): Callback ', $callbackString, + ' scheduled for ', date('H:i:s', $callback['scheduled']), + PHP_EOL; + } else { + echo 'DEBUG(Cron): Callback ', $callbackString, + ' removed from callback list', PHP_EOL; + unset($this->callbacks[$key]); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php new file mode 100755 index 000000000..e79fcf13e --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ctcp.php @@ -0,0 +1,91 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Ctcp + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ctcp + */ + +/** + * Responds to various CTCP requests sent by the server and users. + * + * @category Phergie + * @package Phergie_Plugin_Ctcp + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ctcp + * @link http://www.irchelp.org/irchelp/rfc/ctcpspec.html + */ +class Phergie_Plugin_Ctcp extends Phergie_Plugin_Abstract +{ + /** + * Responds to a CTCP TIME request from a user with the current local + * time. + * + * @return void + */ + public function onTime() + { + $source = $this->getEvent()->getSource(); + $this->doTime($source, strftime('%c %z')); + } + + /** + * Responds to a CTCP VERSION request from a user with the codebase + * version. + * + * @return void + */ + public function onVersion() + { + $source = $this->getEvent()->getSource(); + $msg = 'Phergie ' . Phergie_Bot::VERSION . ' (http://phergie.org)'; + $this->doVersion($source, $msg); + } + + /** + * Responds to a CTCP PING request from a user. + * + * @return void + */ + public function onCtcpPing() + { + $event = $this->getEvent(); + $source = $event->getSource(); + $handshake = $event->getArgument(1); + $this->doPing($source, $handshake); + } + + /** + * Responds to a CTCP FINGER request from a user. + * + * @return void + */ + public function onFinger() + { + $connection = $this->getConnection(); + $name = $connection->getNick(); + $realname = $connection->getRealname(); + $username = $connection->getUsername(); + + $finger + = (empty($realname) ? $realname : $name) . + ' (' . (!empty($username) ? $username : $name) . ')'; + + $this->doFinger($source, $finger); + } +} +?> diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php new file mode 100644 index 000000000..ed258e10f --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Daddy.php @@ -0,0 +1,56 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Daddy + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Daddy + */ + +/** + * Simply responds to messages addressed to the bot that contain the phrase + * "Who's your daddy?" and related variations. + * + * @category Phergie + * @package Phergie_Plugin_Daddy + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Daddy + */ +class Phergie_Plugin_Daddy extends Phergie_Plugin_Abstract +{ + /** + * Checks messages for the question to which it should respond and sends a + * response when appropriate + * + * @return void + */ + public function onPrivmsg() + { + $config = $this->getConfig(); + $prefix = $config['command.prefix']; + $event = $this->getEvent(); + $text = $event->getArgument(1); + $target = $event->getNick(); + $source = $event->getSource(); + $pattern + = '/' . preg_quote($prefix) . + '\s*?who\'?s y(?:our|a) ([^?]+)\??/iAD'; + if (preg_match($pattern, $text, $m)) { + $msg = 'You\'re my ' . $m[1] . ', ' . $target . '!'; + $this->doPrivmsg($source, $msg); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php new file mode 100644 index 000000000..419322bd7 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Encoding.php @@ -0,0 +1,182 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Encoding + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Encoding + */ + +/** + * Handles decoding markup entities and converting text between character + * encodings. + * + * @category Phergie + * @package Phergie_Plugin_Encoding + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Encoding + */ +class Phergie_Plugin_Encoding extends Phergie_Plugin_Abstract +{ + /** + * Lookup table for entity conversions not supported by + * html_entity_decode() + * + * @var array + * @link http://us.php.net/manual/en/function.get-html-translation-table.php#73409 + * @link http://us.php.net/manual/en/function.get-html-translation-table.php#73410 + */ + protected static $entities = array( + 'α' => 913, + ''' => 39, + 'β' => 914, + '•' => 149, + 'χ' => 935, + 'ˆ' => 94, + 'δ' => 916, + 'ε' => 917, + 'η' => 919, + 'ƒ' => 402, + 'γ' => 915, + 'ι' => 921, + 'κ' => 922, + 'λ' => 923, + '“' => 147, + '‹' => 139, + '‘' => 145, + '—' => 151, + '−' => 45, + 'μ' => 924, + '–' => 150, + 'ν' => 925, + 'œ' => 140, + 'ω' => 937, + 'ο' => 927, + 'φ' => 934, + 'π' => 928, + 'ϖ' => 982, + 'ψ' => 936, + '”' => 148, + 'ρ' => 929, + '›' => 155, + '’' => 146, + 'š' => 138, + 'σ' => 931, + 'ς' => 962, + 'τ' => 932, + 'θ' => 920, + 'ϑ' => 977, + '˜' => 126, + '™' => 153, + 'ϒ' => 978, + 'υ' => 933, + 'ξ' => 926, + 'ÿ' => 159, + 'ζ' => 918, + ); + + /** + * Decodes markup entities in a given string. + * + * @param string $string String containing markup entities + * @param string $charset Optional character set name to use in decoding + * entities, defaults to UTF-8 + * + * @return string String with markup entities decoded + */ + public function decodeEntities($string, $charset = 'UTF-8') + { + $string = str_ireplace( + array_keys(self::$entities), + array_map('chr', self::$entities), + $string + ); + $string = html_entity_decode($string, ENT_QUOTES, $charset); + $string = preg_replace( + array('/�*([0-9]+);/me', '/�*([a-f0-9]+);/mei'), + array('$this->codeToUtf(\\1)', '$this->codeToUtf(hexdec(\\1))'), + $string + ); + return $string; + } + + /** + * Converts a given unicode to its UTF-8 equivalent. + * + * @param int $code Code to convert + * @return string Character corresponding to code + */ + public function codeToUtf8($code) + { + $code = (int) $code; + switch ($code) { + // 1 byte, 7 bits + case 0: + return chr(0); + case ($code & 0x7F): + return chr($code); + + // 2 bytes, 11 bits + case ($code & 0x7FF): + return chr(0xC0 | (($code >> 6) & 0x1F)) . + chr(0x80 | ($code & 0x3F)); + + // 3 bytes, 16 bits + case ($code & 0xFFFF): + return chr(0xE0 | (($code >> 12) & 0x0F)) . + chr(0x80 | (($code >> 6) & 0x3F)) . + chr(0x80 | ($code & 0x3F)); + + // 4 bytes, 21 bits + case ($code & 0x1FFFFF): + return chr(0xF0 | ($code >> 18)) . + chr(0x80 | (($code >> 12) & 0x3F)) . + chr(0x80 | (($code >> 6) & 0x3F)) . + chr(0x80 | ($code & 0x3F)); + } + } + + /** + * Transliterates characters in a given string where possible. + * + * @param string $string String containing characters to + * transliterate + * @param string $charsetFrom Optional character set of the string, + * defaults to UTF-8 + * @param string $charsetTo Optional character set to which the string + * should be converted, defaults to ISO-8859-1 + * + * @return string String with characters transliterated or the original + * string if transliteration was not possible + */ + public function transliterate($string, $charsetFrom = 'UTF-8', $charsetTo = 'ISO-8859-1') + { + // @link http://pecl.php.net/package/translit + if (function_exists('transliterate')) { + $string = transliterate($string, array('han_transliterate', 'diacritical_remove'), $charsetFrom, $charsetTo); + } elseif (function_exists('iconv')) { + $string = iconv($charsetFrom, $charsetTo . '//TRANSLIT', $string); + } else { + // @link http://stackoverflow.com/questions/1284535/php-transliteration/1285491#1285491 + $string = preg_replace( + '~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i', + '$1', + htmlentities($string, ENT_COMPAT, $charsetFrom) + ); + } + return $string; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php new file mode 100755 index 000000000..ca4d53fed --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Exception.php @@ -0,0 +1,120 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to plugin handling. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Plugin_Exception extends Phergie_Exception +{ + /** + * Error indicating that a path containing plugins was specified, but + * did not reference a readable directory + */ + const ERR_DIRECTORY_NOT_READABLE = 1; + + /** + * Error indicating that an attempt was made to locate the class for a + * specified plugin, but the class could not be found + */ + const ERR_CLASS_NOT_FOUND = 2; + + /** + * Error indicating that an attempt was made to locate the class for a + * specified plugin, but that the found class did not extend the base + * plugin class + */ + const ERR_INCORRECT_BASE_CLASS = 3; + + /** + * Error indicating that an attempt was made to locate the class for a + * specified plugin, but that the found class cannot be instantiated + */ + const ERR_CLASS_NOT_INSTANTIABLE = 4; + + /** + * Error indicating that an attempt was made to access a plugin that had + * not been loaded and autoloading was not enabled to load it + */ + const ERR_PLUGIN_NOT_LOADED = 5; + + /** + * Error indicating that an attempt was made to access the configuration + * handler before one had been set + */ + const ERR_NO_CONFIG_HANDLER = 6; + + /** + * Error indicating that an attempt was made to access the plugin + * handler before one had been set + */ + const ERR_NO_PLUGIN_HANDLER = 7; + + /** + * Error indicating that an attempt was made to access the event + * handler before one had been set + */ + const ERR_NO_EVENT_HANDLER = 8; + + /** + * Error indicating that an attempt was made to access the connection + * before one had been set + */ + const ERR_NO_CONNECTION = 9; + + /** + * Error indicating that an attempt was made to access the current + * incoming event before one had been set + */ + const ERR_NO_EVENT = 10; + + /** + * Error indicating that a dependency of the plugin was unavailable at + * the time that an attempt was made to load it + */ + const ERR_REQUIREMENT_UNSATISFIED = 11; + + /** + * Error indicating that a call was made to a nonexistent plugin method + * and that its __call() implementation did not process that call as an + * attempt to trigger an event - this is intended to aid in debugging of + * such situations + */ + const ERR_INVALID_CALL = 12; + + /** + * Error indicating that a fatal runtime issue was encountered within a + * plugin + */ + const ERR_FATAL_ERROR = 13; + + /** + * Error indicating that a class specified to be used for iterating + * plugins cannot be found by the autoloader or does not extend + * FilterIterator + */ + const ERR_INVALID_ITERATOR_CLASS = 14; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php new file mode 100644 index 000000000..d2a9d4dce --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Google.php @@ -0,0 +1,459 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Google + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Google + */ + +/** + * Provides commands used to access several services offered by Google + * including search, translation, weather, maps, and currency and general + * value unit conversion. + * + * @category Phergie + * @package Phergie_Plugin_Google + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Google + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + * @uses Phergie_Plugin_Temperature pear.phergie.org + */ +class Phergie_Plugin_Google extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $plugins->getPlugin('Http'); + $plugins->getPlugin('Weather'); + } + + /** + * Returns the first result of a Google search. + * + * @param string $query Search term + * + * @return void + * @todo Implement use of URL shortening here + */ + public function onCommandG($query) + { + $url = 'http://ajax.googleapis.com/ajax/services/search/web'; + $params = array( + 'v' => '1.0', + 'q' => $query + ); + $response = $this->plugins->http->get($url, $params); + $json = $response->getContent()->responseData; + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + if ($json->cursor->estimatedResultCount > 0) { + $msg + = $nick + . ': [ ' + . $json->results[0]->titleNoFormatting + . ' ] - ' + . $json->results[0]->url + . ' - More results: ' + . $json->cursor->moreResultsUrl; + $this->doPrivmsg($source, $msg); + } else { + $msg = $nick . ': No results for this query.'; + $this->doPrivmsg($source, $msg); + } + } + + /** + * Performs a Google Count search for the given term. + * + * @param string $query Search term + * + * @return void + */ + public function onCommandGc($query) + { + $url = 'http://ajax.googleapis.com/ajax/services/search/web'; + $params = array( + 'v' => '1.0', + 'q' => $query + ); + $response = $this->plugins->http->get($url, $params); + $json = $response->getContent()->responseData->cursor; + $count = $json->estimatedResultCount; + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + if ($count) { + $msg + = $nick . ': ' . + number_format($count, 0) . + ' estimated results for ' . $query; + $this->doPrivmsg($source, $msg); + } else { + $this->doPrivmsg($source, $nick . ': No results for this query.'); + } + } + + /** + * Performs a Google Translate search for the given term. + * + * @param string $from Language of the search term + * @param string $to Language to which the search term should be + * translated + * @param string $query Term to translate + * + * @return void + */ + public function onCommandGt($from, $to, $query) + { + $url = 'http://ajax.googleapis.com/ajax/services/language/translate'; + $params = array( + 'v' => '1.0', + 'q' => $query, + 'langpair' => $from . '|' . $to + ); + $response = $this->plugins->http->get($url, $params); + $json = $response->getContent(); + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + if (empty($json->responseData->translatedText)) { + $this->doPrivmsg($source, $nick . ': ' . $json->responseDetails); + } else { + $this->doPrivmsg( + $source, + $nick . ': ' . $json->responseData->translatedText + ); + } + } + + /** + * Performs a Google Weather search for the given term. + * + * @param string $location Location to search for + * @param int $offset Optional day offset from the current date + * between 0 and 3 to get the forecast + * + * @return void + */ + public function onCommandGw($location, $offset = null) + { + $url = 'http://www.google.com/ig/api'; + $params = array( + 'weather' => $location, + 'hl' => $this->getConfig('google.lang', 'en'), + 'oe' => 'UTF-8' + ); + $response = $this->plugins->http->get($url, $params); + $xml = $response->getContent()->weather; + + $event = $this->getEvent(); + $source = $event->getSource(); + $msg = ''; + if ($event->isInChannel()) { + $msg .= $event->getNick() . ': '; + } + + if (isset($xml->problem_cause)) { + $msg .= $xml->problem_cause->attributes()->data[0]; + $this->doPrivmsg($source, $msg); + return; + } + + $temperature = $this->plugins->getPlugin('Temperature'); + + $forecast = $xml->forecast_information; + $city = $forecast->city->attributes()->data[0]; + $zip = $forecast->postal_code->attributes()->data[0]; + + if ($offset !== null) { + $offset = (int) $offset; + if ($offset < 0) { + $this->doNotice($source, 'Past weather data is not available'); + return; + } elseif ($offset > 3) { + $this->doNotice($source, 'Future weather data is limited to 3 days from today'); + return; + } + + $linha = $xml->forecast_conditions[$offset]; + $low = $linha->low->attributes()->data[0]; + $high = $linha->high->attributes()->data[0]; + $units = $forecast->unit_system->attributes()->data[0]; + $condition = $linha->condition->attributes()->data[0]; + $day = $linha->day_of_week->attributes()->data[0]; + + $date = ($offset == 0) ? time() : strtotime('next ' . $day); + $day = ucfirst($day) . ' ' . date('n/j/y', $date); + + if ($units == 'US') { + $lowF = $low; + $lowC = $temperature->convertFahrenheitToCelsius($low); + $highF = $high; + $highC = $temperature->convertFahrenheitToCelsius($high); + } else { + $lowC = $low; + $lowF = $temperature->convertCelsiusToFahrenheit($lowC); + $highC = $high; + $highF = $temperature->convertCelsiusToFahrenheit($high); + } + + $msg .= 'Forecast for ' . $city . ' (' . $zip . ')' + . ' on ' . $day . ' ::' + . ' Low: ' . $lowF . 'F/' . $lowC . 'C,' + . ' High: ' . $highF . 'F/' . $highC . 'C,' + . ' Conditions: ' . $condition; + } else { + $conditions = $xml->current_conditions; + $condition = $conditions->condition->attributes()->data[0]; + $tempF = $conditions->temp_f->attributes()->data[0]; + $tempC = $conditions->temp_c->attributes()->data[0]; + $humidity = $conditions->humidity->attributes()->data[0]; + $wind = $conditions->wind_condition->attributes()->data[0]; + $time = $forecast->current_date_time->attributes()->data[0]; + $time = date('n/j/y g:i A', strtotime($time)) . ' +0000'; + + $hiF = $temperature->getHeatIndex($tempF, $humidity); + $hiC = $temperature->convertFahrenheitToCelsius($hiF); + + $msg .= 'Weather for ' . $city . ' (' . $zip . ') -' + . ' Temperature: ' . $tempF . 'F/' . $tempC . 'C,' + . ' ' . $humidity . ',' + . ' Heat Index: ' . $hiF . 'F/' . $hiC . 'C,' + . ' Conditions: ' . $condition . ',' + . ' Updated: ' . $time; + } + + $this->doPrivmsg($source, $msg); + } + + /** + * Performs a Google Maps search for the given term. + * + * @param string $location Location to search for + * + * @return void + */ + public function onCommandGmap($location) + { + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + + $location = utf8_encode($location); + $url = 'http://maps.google.com/maps/geo'; + $params = array( + 'q' => $location, + 'output' => 'json', + 'gl' => $this->getConfig('google.lang', 'en'), + 'sensor' => 'false', + 'oe' => 'utf8', + 'mrt' => 'all', + 'key' => $this->getConfig('google.key') + ); + $response = $this->plugins->http->get($url, $params); + $json = $response->getContent(); + if (!empty($json)) { + $qtd = count($json->Placemark); + if ($qtd > 1) { + if ($qtd <= 3) { + foreach ($json->Placemark as $places) { + $xy = $places->Point->coordinates; + $address = utf8_decode($places->address); + $url = 'http://maps.google.com/maps?sll=' . $xy[1] . ',' + . $xy[0] . '&z=15'; + $msg = $nick . ' -> ' . $address . ' - ' . $url; + $this->doPrivmsg($source, $msg); + } + } else { + $msg + = $nick . + ', there are a lot of places with that query.' . + ' Try to be more specific!'; + $this->doPrivmsg($source, $msg); + } + } elseif ($qtd == 1) { + $xy = $json->Placemark[0]->Point->coordinates; + $address = utf8_decode($json->Placemark[0]->address); + $url = 'http://maps.google.com/maps?sll=' . $xy[1] . ',' . $xy[0] + . '&z=15'; + $msg = $nick . ' -> ' . $address . ' - ' . $url; + $this->doPrivmsg($source, $msg); + } else { + $this->doPrivmsg($source, $nick . ', I found nothing.'); + } + } else { + $this->doPrivmsg($source, $nick . ', we have a problem.'); + } + } + + /** + * Perform a Google Convert query to convert a value from one metric to + * another. + * + * @param string $value Value to convert + * @param string $from Source metric + * @param string $to Destination metric + * + * @return void + */ + public function onCommandGconvert($value, $from, $to) + { + $url = 'http://www.google.com/finance/converter'; + $params = array( + 'a' => $value, + 'from' => $from, + 'to' => $to + ); + $response = $this->plugins->http->get($url, $params); + $contents = $response->getContent(); + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + if ($contents) { + libxml_use_internal_errors(true); + $doc = new DOMDocument; + $doc->loadHTML($contents); + libxml_clear_errors(); + $xpath = new DOMXPath($doc); + $result = $xpath->query('//div[@id="currency_converter_result"]'); + $div = $result->item(0); + $text = rtrim($div->textContent); + $this->doPrivmsg($source, $text); + } + } + + /** + * Performs a Google search to convert a value from one unit to another. + * + * @param string $query Query of the form "[quantity] [unit] to [unit2]" + * + * @return void + * + * @pluginCmd [quantity] [unit] to [unit2] Convert a value from one + * metric to another + */ + public function onCommandConvert($query) + { + $url = 'http://www.google.com/search?q=' . urlencode($query); + $response = $this->plugins->http->get($url); + $contents = $response->getContent(); + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + + if ($response->isError()) { + $code = $response->getCode(); + $message = $response->getMessage(); + $this->doNotice($nick, 'ERROR: ' . $code . ' ' . $message); + return; + } + + $start = strpos($contents, '<h3 class=r>'); + if ($start !== false) { + $end = strpos($contents, '</b>', $start); + $text = strip_tags(substr($contents, $start, $end - $start)); + $text = str_replace( + array(chr(195), chr(151), chr(160)), + array('x', '', ' '), + $text + ); + } + + if (isset($text)) { + $this->doPrivmsg($source, $nick . ': ' . $text); + } else { + $this->doNotice($nick, 'Sorry I couldn\'t find an answer.'); + } + } + + + /** + * Returns the first definition of a Google Dictionary search. + * + * @param string $query Word to get the definition + * + * @return void + * @todo Implement use of URL shortening here + */ + public function onCommandDefine($query) + { + $lang = $this->getConfig('google.lang', 'en'); + $url = 'http://www.google.com/dictionary/json'; + $params = array( + 'callback' => 'result', + 'q' => $query, + 'sl' => $lang, + 'tl' => $lang, + 'restrict' => 'pr,de' + ); + $response = $this->plugins->http->get($url, $params); + $json = $response->getContent(); + + // Remove some garbage from the JSON and decode it + $json = str_replace(array('result(', ',200,null)'), '', $json); + $json = str_replace('"', '¿?¿', $json); + $json = strip_tags(stripcslashes($json)); + $json = str_replace('"', "'", $json); + $json = str_replace('¿?¿', '"', $json); + $json = json_decode($json); + + $event = $this->getEvent(); + $source = $event->getSource(); + $nick = $event->getNick(); + if (!empty($json->webDefinitions)) { + $results = 0; + foreach ($json->primaries[0]->entries as $entry) { + if ($entry->type == 'meaning') { + $results++; + if (empty($text)) { + foreach ($entry->terms as $term) { + if ($term->type == 'text') { + $text = trim($term->text); + } + } + } + } + } + $more = $results > 1 ? ($results - 1) . ' ' : ''; + $lang_code = substr($lang, 0, 2); + $msg = $nick . ': ' . $text + . ' - You can find ' . $more . 'more results at ' + . 'http://www.google.com/dictionary' + . '?aq=f' + . '&langpair=' . $lang_code . '%7C' . $lang_code + . '&q=' . $query + . '&hl=' . $lang_code; + $this->doPrivmsg($source, $msg); + } else { + if ($lang != 'en'){ + $lang = 'en'; + $this->onCommandDefine($query); + } else { + $msg = $nick . ': No results for this query.'; + $this->doPrivmsg($source, $msg); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php new file mode 100755 index 000000000..c3086587b --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php @@ -0,0 +1,501 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Handles on-demand loading of, iteration over, and access to plugins. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Plugin_Handler implements IteratorAggregate, Countable +{ + /** + * Current list of plugin instances + * + * @var array + */ + protected $plugins; + + /** + * Paths in which to search for plugin class files + * + * @var array + */ + protected $paths; + + /** + * Flag indicating whether plugin classes should be instantiated on + * demand if they are requested but no instance currently exists + * + * @var bool + */ + protected $autoload; + + /** + * Phergie_Config instance that should be passed in to any plugin + * instantiated within the handler + * + * @var Phergie_Config + */ + protected $config; + + /** + * Phergie_Event_Handler instance that should be passed in to any plugin + * instantiated within the handler + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Name of the class to use for iterating over all currently loaded + * plugins + * + * @var string + */ + protected $iteratorClass = 'Phergie_Plugin_Iterator'; + + /** + * Constructor to initialize class properties and add the path for core + * plugins. + * + * @param Phergie_Config $config configuration to pass to any + * instantiated plugin + * @param Phergie_Event_Handler $events event handler to pass to any + * instantiated plugin + * + * @return void + */ + public function __construct( + Phergie_Config $config, + Phergie_Event_Handler $events + ) { + $this->config = $config; + $this->events = $events; + + $this->plugins = array(); + $this->paths = array(); + $this->autoload = false; + + if (!empty($config['plugins.paths'])) { + foreach ($config['plugins.paths'] as $dir => $prefix) { + $this->addPath($dir, $prefix); + } + } + + $this->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + } + + + /** + * Adds a path to search for plugin class files. Paths are searched in + * the reverse order in which they are added. + * + * @param string $path Filesystem directory path + * @param string $prefix Optional class name prefix corresponding to the + * path + * + * @return Phergie_Plugin_Handler Provides a fluent interface + * @throws Phergie_Plugin_Exception + */ + public function addPath($path, $prefix = '') + { + if (!is_readable($path)) { + throw new Phergie_Plugin_Exception( + 'Path "' . $path . '" does not reference a readable directory', + Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE + ); + } + + $this->paths[] = array( + 'path' => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, + 'prefix' => $prefix + ); + + return $this; + } + + /** + * Returns metadata corresponding to a specified plugin. + * + * @param string $plugin Short name of the plugin class + * + * @throws Phergie_Plugin_Exception Class file can't be found + * + * @return array|boolean Associative array containing the path to the + * class file and its containing directory as well as the full + * class name + */ + public function getPluginInfo($plugin) + { + foreach (array_reverse($this->paths) as $path) { + $file = $path['path'] . $plugin . '.php'; + if (file_exists($file)) { + $path = array( + 'dir' => $path['path'], + 'file' => $file, + 'class' => $path['prefix'] . $plugin, + ); + return $path; + } + } + + // If the class can't be found, display an error + throw new Phergie_Plugin_Exception( + 'Class file for plugin "' . $plugin . '" cannot be found', + Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND + ); + } + + /** + * Adds a plugin instance to the handler. + * + * @param string|Phergie_Plugin_Abstract $plugin Short name of the + * plugin class or a plugin object + * @param array $args Optional array of + * arguments to pass to the plugin constructor if a short name is + * passed for $plugin + * + * @return Phergie_Plugin_Abstract New plugin instance + */ + public function addPlugin($plugin, array $args = null) + { + // If a short plugin name is specified... + if (is_string($plugin)) { + $index = strtolower($plugin); + if (isset($this->plugins[$index])) { + return $this->plugins[$index]; + } + + // Attempt to locate and load the class + $info = $this->getPluginInfo($plugin); + $file = $info['file']; + $class = $info['class']; + include_once $file; + if (!class_exists($class, false)) { + throw new Phergie_Plugin_Exception( + 'File "' . $file . '" does not contain class "' . $class . '"', + Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND + ); + } + + // Check to ensure the class is a plugin class + if (!is_subclass_of($class, 'Phergie_Plugin_Abstract')) { + $msg + = 'Class for plugin "' . $plugin . + '" does not extend Phergie_Plugin_Abstract'; + throw new Phergie_Plugin_Exception( + $msg, + Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS + ); + } + + // Check to ensure the class can be instantiated + $reflection = new ReflectionClass($class); + if (!$reflection->isInstantiable()) { + throw new Phergie_Plugin_Exception( + 'Class for plugin "' . $plugin . '" cannot be instantiated', + Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE + ); + } + + // If the class is found, instantiate it + if (!empty($args)) { + $instance = $reflection->newInstanceArgs($args); + } else { + $instance = new $class; + } + + // Store the instance + $this->plugins[$index] = $instance; + $plugin = $instance; + + } elseif ($plugin instanceof Phergie_Plugin_Abstract) { + // If a plugin instance is specified... + + // Add the plugin instance to the list of plugins + $this->plugins[strtolower($plugin->getName())] = $plugin; + } + + // Configure and initialize the instance + $plugin->setPluginHandler($this); + $plugin->setConfig($this->config); + $plugin->setEventHandler($this->events); + $plugin->onLoad(); + + return $plugin; + } + + /** + * Adds multiple plugin instances to the handler. + * + * @param array $plugins List of elements where each is of the form + * 'ShortPluginName' or array('ShortPluginName', array($arg1, + * ..., $argN)) + * + * @return Phergie_Plugin_Handler Provides a fluent interface + */ + public function addPlugins(array $plugins) + { + foreach ($plugins as $plugin) { + if (is_array($plugin)) { + $this->addPlugin($plugin[0], $plugin[1]); + } else { + $this->addPlugin($plugin); + } + } + + return $this; + } + + /** + * Removes a plugin instance from the handler. + * + * @param string|Phergie_Plugin_Abstract $plugin Short name of the + * plugin class or a plugin object + * + * @return Phergie_Plugin_Handler Provides a fluent interface + */ + public function removePlugin($plugin) + { + if ($plugin instanceof Phergie_Plugin_Abstract) { + $plugin = $plugin->getName(); + } + $plugin = strtolower($plugin); + + unset($this->plugins[$plugin]); + + return $this; + } + + /** + * Returns the corresponding instance for a specified plugin, loading it + * if it is not already loaded and autoloading is enabled. + * + * @param string $name Short name of the plugin class + * + * @return Phergie_Plugin_Abstract Plugin instance + */ + public function getPlugin($name) + { + // If the plugin is loaded, return the instance + $lower = strtolower($name); + if (isset($this->plugins[$lower])) { + return $this->plugins[$lower]; + } + + // If autoloading is disabled, display an error + if (!$this->autoload) { + $msg + = 'Plugin "' . $name . '" has been requested, ' . + 'is not loaded, and autoload is disabled'; + throw new Phergie_Plugin_Exception( + $msg, + Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED + ); + } + + // If autoloading is enabled, attempt to load the plugin + $plugin = $this->addPlugin($name); + + // Return the added plugin + return $plugin; + } + + /** + * Returns the corresponding instances for multiple specified plugins, + * loading them if they are not already loaded and autoloading is + * enabled. + * + * @param array $names Optional list of short names of the plugin + * classes to which the returned plugin list will be limited, + * defaults to all presently loaded plugins + * + * @return array Associative array mapping lowercased plugin class short + * names to corresponding plugin instances + */ + public function getPlugins(array $names = array()) + { + if (empty($names)) { + return $this->plugins; + } + + $plugins = array(); + foreach ($names as $name) { + $plugins[strtolower($name)] = $this->getPlugin($name); + } + return $plugins; + } + + /** + * Returns whether or not at least one instance of a specified plugin + * class is loaded. + * + * @param string $name Short name of the plugin class + * + * @return bool TRUE if an instance exists, FALSE otherwise + */ + public function hasPlugin($name) + { + return isset($this->plugins[strtolower($name)]); + } + + /** + * Sets a flag used to determine whether plugins should be loaded + * automatically if they have not been explicitly loaded. + * + * @param bool $flag TRUE to have plugins autoload (default), FALSE + * otherwise + * + * @return Phergie_Plugin_Handler Provides a fluent interface. + */ + public function setAutoload($flag = true) + { + $this->autoload = $flag; + + return $this; + } + + /** + * Returns the value of a flag used to determine whether plugins should + * be loaded automatically if they have not been explicitly loaded. + * + * @return bool TRUE if autoloading is enabled, FALSE otherwise + */ + public function getAutoload() + { + return $this->autoload; + } + + /** + * Allows plugin instances to be accessed as properties of the handler. + * + * @param string $name Short name of the plugin + * + * @return Phergie_Plugin_Abstract Requested plugin instance + */ + public function __get($name) + { + return $this->getPlugin($name); + } + + /** + * Allows plugin instances to be detected as properties of the handler. + * + * @param string $name Short name of the plugin + * + * @return bool TRUE if the plugin is loaded, FALSE otherwise + */ + public function __isset($name) + { + return $this->hasPlugin($name); + } + + /** + * Allows plugin instances to be removed as properties of handler. + * + * @param string $name Short name of the plugin + * + * @return void + */ + public function __unset($name) + { + $this->removePlugin($name); + } + + /** + * Returns an iterator for all currently loaded plugin instances. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new $this->iteratorClass( + new ArrayIterator($this->plugins) + ); + } + + /** + * Sets the iterator class used for all currently loaded plugin + * instances. + * + * @param string $class Name of a class that extends FilterIterator + * + * @return Phergie_Plugin_Handler Provides a fluent API + * @throws Phergie_Plugin_Exception Class cannot be found or is not an + * FilterIterator-based class + */ + public function setIteratorClass($class) + { + $valid = true; + + try { + $error_reporting = error_reporting(0); // ignore autoloader errors + $r = new ReflectionClass($class); + error_reporting($error_reporting); + if (!$r->isSubclassOf('FilterIterator')) { + $message = 'Class ' . $class . ' is not a subclass of FilterIterator'; + $valid = false; + } + } catch (ReflectionException $e) { + $message = $e->getMessage(); + $valid = false; + } + + if (!$valid) { + throw new Phergie_Plugin_Exception( + $message, + Phergie_Plugin_Exception::ERR_INVALID_ITERATOR_CLASS + ); + } + + $this->iteratorClass = $class; + } + + /** + * Proxies method calls to all plugins containing the called method. + * + * @param string $name Name of the method called + * @param array $args Arguments passed in the method call + * + * @return void + */ + public function __call($name, array $args) + { + foreach ($this->getIterator() as $plugin) { + call_user_func_array(array($plugin, $name), $args); + } + return true; + } + + /** + * Returns the number of plugins contained within the handler. + * + * @return int Plugin count + */ + public function count() + { + return count($this->plugins); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php new file mode 100644 index 000000000..4c2c49b31 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Help.php @@ -0,0 +1,276 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Help + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Help + */ + +/** + * Provides access to descriptions of plugins and the commands they provide. + * + * @category Phergie + * @package Phergie_Plugin_Help + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Help + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Help extends Phergie_Plugin_Abstract +{ + /** + * Registry of help data indexed by plugin name + * + * @var array + */ + protected $registry; + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Creates a registry of plugin metadata on connect. + * + * @return void + */ + public function onConnect() + { + $this->populateRegistry(); + } + + /** + * Creates a registry of plugin metadata. + * + * @return void + */ + public function populateRegistry() + { + $this->registry = array(); + + foreach ($this->plugins as $plugin) { + $class = new ReflectionClass($plugin); + $pluginName = strtolower($plugin->getName()); + + // Parse the plugin description + $docblock = $class->getDocComment(); + $annotations = $this->getAnnotations($docblock); + if (isset($annotations['pluginDesc'])) { + $pluginDesc = implode(' ', $annotations['pluginDesc']); + } else { + $pluginDesc = $this->parseShortDescription($docblock); + } + $this->registry[$pluginName] = array( + 'desc' => $pluginDesc, + 'cmds' => array() + ); + + // Parse command method descriptions + $methodPrefix = Phergie_Plugin_Command::METHOD_PREFIX; + $methodPrefixLength = strlen($methodPrefix); + foreach ($class->getMethods() as $method) { + if (strpos($method->getName(), $methodPrefix) !== 0) { + continue; + } + + $cmd = strtolower(substr($method->getName(), $methodPrefixLength)); + $docblock = $method->getDocComment(); + $annotations = $this->getAnnotations($docblock); + + if (isset($annotations['pluginCmd'])) { + $cmdDesc = implode(' ', $annotations['pluginCmd']); + } else { + $cmdDesc = $this->parseShortDescription($docblock); + } + + $cmdParams = array(); + if (!empty($annotations['param'])) { + foreach ($annotations['param'] as $param) { + $match = null; + if (preg_match('/\h+\$([^\h]+)\h+/', $param, $match)) { + $cmdParams[] = $match[1]; + } + } + } + + $this->registry[$pluginName]['cmds'][$cmd] = array( + 'desc' => $cmdDesc, + 'params' => $cmdParams + ); + } + + if (empty($this->registry[$pluginName]['cmds'])) { + unset($this->registry[$pluginName]); + } + } + } + + /** + * Displays a list of plugins with help information available or + * commands available for a specific plugin. + * + * @param string $query Optional short name of a plugin for which commands + * should be returned or a command; if unspecified, a list of + * plugins with help information available is returned + * + * @return void + */ + public function onCommandHelp($query = null) + { + if ($query == 'refresh') { + $this->populateRegistry(); + } + + $nick = $this->getEvent()->getNick(); + $delay = $this->getConfig('help.delay', 2); + + // Handle requests for a plugin list + if (!$query) { + $msg = 'These plugins have help information available: ' + . implode(', ', array_keys($this->registry)); + $this->doPrivmsg($nick, $msg); + return; + } + + // Handle requests for plugin information + $query = strtolower($query); + if (isset($this->registry[$query]) + && empty($this->registry[$query]['cmds'][$query])) { + $msg = $query . ' - ' . $this->registry[$query]['desc']; + $this->doPrivmsg($nick, $msg); + + $msg = 'Available commands - ' + . implode(', ', array_keys($this->registry[$query]['cmds'])); + $this->doPrivmsg($nick, $msg); + + if ($this->getConfig('command.prefix')) { + $msg + = 'Note that these commands must be prefixed with "' + . $this->getConfig('command.prefix') + . '" (without quotes) when issued in a public channel.'; + $this->doPrivmsg($nick, $msg); + } + + return; + } + + // Handle requests for command information + foreach ($this->registry as $plugin => $data) { + if (empty($data['cmds'])) { + continue; + } + + $result = preg_grep('/^' . $query . '$/i', array_keys($data['cmds'])); + if (!$result) { + continue; + } + + $cmd = $data['cmds'][array_shift($result)]; + $msg = $query; + if (!empty($cmd['params'])) { + $msg .= ' [' . implode('] [', $cmd['params']) . ']'; + } + $msg .= ' - ' . $cmd['desc']; + $this->doPrivmsg($nick, $msg); + } + } + + /** + * Parses and returns the short description from a docblock. + * + * @param string $docblock Docblock comment code + * + * @return string Short description (i.e. content from the start of the + * docblock up to the first double-newline) + */ + protected function parseShortDescription($docblock) + { + $desc = preg_replace( + array('#^\h*\*\h*#m', '#^/\*\*\h*\v+\h*#', '#(?:\r?\n){2,}.*#s', '#\s*\v+\s*#'), + array('', '', '', ' '), + $docblock + ); + return $desc; + } + + /** + * Taken from PHPUnit/Util/Test.php and modified to fix an issue with + * tag content spanning multiple lines. + * + * PHPUnit + * + * Copyright (c) 2002-2010, Sebastian Bergmann <sb@sebastian-bergmann.de>. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @param string $docblock docblock to parse + * + * @return array + */ + protected function getAnnotations($docblock) + { + $annotations = array(); + + $regex = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?(?:\*\/|\* @)/ms'; + + if (preg_match_all($regex, $docblock, $matches)) { + $numMatches = count($matches[0]); + + for ($i = 0; $i < $numMatches; ++$i) { + $annotation = $matches['value'][$i]; + $annotation = preg_replace('/\s*\v+\s*\*\s*/', ' ', $annotation); + $annotation = rtrim($annotation); + $annotations[$matches['name'][$i]][] = $annotation; + } + } + + return $annotations; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php new file mode 100644 index 000000000..7c5c8704c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http.php @@ -0,0 +1,284 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Http + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Http + */ + +/** + * Provides an HTTP client for plugins to use in contacting web services or + * retrieving feeds or web pages. + * + * @category Phergie + * @package Phergie_Plugin_Http + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Http + * @uses extension simplexml optional + * @uses extension json optional + */ +class Phergie_Plugin_Http extends Phergie_Plugin_Abstract +{ + /** + * Response to the last executed HTTP request + * + * @var Phergie_Plugin_Http_Response + */ + protected $response; + + /** + * Mapping of content types to handlers for them + * + * @var array + */ + protected $handlers; + + /** + * Initializes the handler lookup table. + * + * @return void + */ + public function onLoad() + { + $this->handlers = array( + '(?:application|text)/xml(?:;.*)?' + => 'simplexml_load_string', + '(?:(?:application|text)/(?:x-)?json)|text/javascript.*' + => 'json_decode', + ); + + if (is_array($this->config['http.handlers'])) { + $this->handlers = array_merge( + $this->handlers, + $this->config['http.handlers'] + ); + } + } + + /** + * Sets a handler callback for a content type, which is called when a + * response of that content type is received to perform any needed + * transformations on the response body content before storing it in the + * response object. Note that the calling plugin is responsible for + * indicating any dependencies related to specified handler callbacks. + * + * @param string $type PCRE regular expression (without delimiters) that + * matches one or more MIME types + * @param callback $callback Callback to execute when a response of a content + * type matched by $type is encountered + * + * @return Phergie_Plugin_Http Provides a fluent interface + */ + public function setHandler($type, $callback) + { + if (!is_callable($callback)) { + throw new Phergie_Plugin_Exception( + 'Invalid callback specified', + Phergie_Plugin_Exception::ERR_FATAL_ERROR + ); + } + + $this->handlers[$type] = $callback; + + return $this; + } + + /** + * Supporting method that parses the status line of an HTTP response + * message. + * + * @param string $status Status line + * + * @return array Associative array containing the HTTP version, response + * code, and response description + */ + protected function parseStatusLine($status) + { + $parts = explode(' ', $status, 3); + $parsed = array( + 'version' => str_replace('HTTP/', '', $parts[0]), + 'code' => $parts[1], + 'message' => rtrim($parts[2]) + ); + return $parsed; + } + + /** + * Supporting method that acts as an error handler to intercept HTTP + * responses resulting in PHP-level errors. + * + * @param int $errno Level of the error raised + * @param string $errstr Error message + * @param string $errfile Name of the file in which the error was raised + * @param string $errline Line number on which the error was raised + * + * @return bool Always returns TRUE to allow normal execution to + * continue once this method terminates + */ + public function handleError($errno, $errstr, $errfile, $errline) + { + if ($httperr = strstr($errstr, 'HTTP/')) { + $parts = $this->parseStatusLine($httperr); + $this->response + ->setCode($parts['code']) + ->setMessage($parts['message']); + } + + return true; + } + + /** + * Supporting method that executes a request and handles the response. + * + * @param string $url URL to request + * @param array $context Associative array of stream context parameters + * + * @return Phergie_Plugin_Http_Response Object representing the response + * resulting from the request + */ + public function request($url, array $context) + { + $this->response = new Phergie_Plugin_Http_Response; + + $url = (string) $url; + $context = stream_context_create(array('http' => $context)); + + set_error_handler(array($this, 'handleError'), E_WARNING); + $stream = fopen($url, 'r', false, $context); + if ($stream) { + $meta = stream_get_meta_data($stream); + $status = $this->parseStatusLine($meta['wrapper_data'][0]); + $code = $status['code']; + $message = $status['message']; + $headers = array(); + foreach (array_slice($meta['wrapper_data'], 1) as $header) { + if (!strpos($header, ':')) { + continue; + } + list($name, $value) = explode(': ', $header, 2); + $headers[$name] = $value; + } + unset($meta['wrapper_data']); + + $this->response + ->setCode($code) + ->setMessage($message) + ->setHeaders($headers) + ->setMeta($meta); + + $body = stream_get_contents($stream); + $type = $this->response->getHeaders('content-type'); + foreach ($this->handlers as $expr => $handler) { + if (preg_match('#^' . $expr . '$#i', $type)) { + $handled = call_user_func($handler, $body); + if (!empty($handled)) { + $body = $handled; + } + } + } + + $this->response->setContent($body); + } + restore_error_handler(); + + return $this->response; + } + + /** + * Performs a GET request. + * + * @param string $url URL for the request + * @param array $query Optional associative array of parameters + * constituting the URL query string if $url has none + * @param array $context Optional associative array of additional stream + * context parameters + * + * @return Phergie_Plugin_Http_Response Received response data + */ + public function get($url, array $query = array(), array $context = array()) + { + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + + $context['method'] = 'GET'; + + return $this->request($url, $context); + } + + /** + * Performs a HEAD request. + * + * @param string $url URL for the request + * @param array $query Optional associative array of parameters + * constituting the URL query string if $url has none + * @param array $context Optional associative array of additional stream + * context parameters + * + * @return Phergie_Plugin_Http_Response Received response data + */ + public function head($url, array $query = array(), array $context = array()) + { + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + + $context['method'] = 'HEAD'; + + return $this->request($url, $context); + } + + /** + * Performs a POST request. + * + * @param string $url URL for the request + * @param array $query Optional associative array of parameters + * constituting the URL query string if $url has none + * @param array $post Optional associative array of parameters + * constituting the POST request body if it is using the + * traditional URL-encoded format + * @param array $context Optional associative array of additional stream + * context parameters + * + * @return Phergie_Plugin_Http_Response Received response data + */ + public function post($url, array $query = array(), + array $post = array(), array $context = array() + ) { + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + + $context['method'] = 'POST'; + + if (!empty($post) + && (!empty($context['header']) + xor stripos($context['header'], 'Content-Type')) + ) { + if (!empty($context['header'])) { + $context['header'] .= "\r\n"; + } else { + $context['header'] = ''; + } + $context['header'] .= + 'Content-Type: application/x-www-form-urlencoded'; + $context['content'] = http_build_query($post); + } + + return $this->request($url, $context); + } +}
\ No newline at end of file diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php new file mode 100644 index 000000000..b9e377cf6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Http/Response.php @@ -0,0 +1,281 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Http + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Http + */ + +/** + * Data structure for HTTP response information. + * + * @category Phergie + * @package Phergie_Plugin_Http + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Http + */ +class Phergie_Plugin_Http_Response +{ + /** + * HTTP response code or 0 if no HTTP response was received + * + * @var string + */ + protected $code; + + /** + * HTTP response strings + * + * @var array + */ + protected static $codeStrings = array( + 0 => 'No Response', + 100 => 'Continue', + 200 => 'OK', + 201 => 'Created', + 204 => 'No Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 408 => 'Request Timeout', + 410 => 'Gone', + 413 => 'Request Entity Too Large', + 414 => 'Request URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Method Not Implemented', + 503 => 'Service Unavailable', + 506 => 'Variant Also Negotiates' + ); + + /** + * Description of the HTTP response code or the error message if no HTTP + * response was received + * + * @var string + */ + protected $message; + + /** + * Content of the response body, decoded for supported content types + * + * @var mixed + */ + protected $content; + + /** + * Associative array mapping response header names to their values + * + * @var array + */ + protected $headers; + + /** + * Associative array containing other metadata about the response + * + * @var array + */ + protected $meta; + + /** + * Sets the HTTP response code. + * + * @param string $code Response code + * + * @return Phergie_Plugin_Http_Response Provides a fluent interface + */ + public function setCode($code) + { + $this->code = $code; + return $this; + } + + /** + * Returns the HTTP response code. + * + * @return string Response code + */ + public function getCode() + { + return $this->code; + } + + /** + * Returns the HTTP response code text. + * + * @return string Response code text + */ + public function getCodeAsString() + { + $code = $this->code; + + if (!isset(self::$codeStrings[$code])) { + return 'Unkown HTTP Status'; + } + + return self::$codeStrings[$code]; + } + + /** + * Returns whether the response indicates a client- or server-side error. + * + * @return bool TRUE if the response indicates an error, FALSE otherwise + */ + public function isError() + { + switch (substr($this->code, 0, 1)) { + case '0': + case '4': + case '5': + return true; + default: + return false; + } + } + + /** + * Sets the HTTP response description. + * + * @param string $message Response description + * + * @return Phergie_Plugin_Http_Response Provides a fluent interface + */ + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + /** + * Returns the HTTP response description. + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Sets the content of the response body. + * + * @param mixed $content Response body content + * + * @return Phergie_Plugin_Http_Response Provides a fluent interface + */ + public function setContent($content) + { + $this->content = $content; + return $this; + } + + /** + * Returns the content of the response body. + * + * @return mixed Response body content, decoded for supported content + * types + */ + public function getContent() + { + return $this->content; + } + + /** + * Sets the response headers. + * + * @param array $headers Associative array of response headers indexed + * by header name + * + * @return Phergie_Plugin_Http_Response Provides a fluent interface + */ + public function setHeaders(array $headers) + { + $names = array_map('strtolower', array_keys($headers)); + $values = array_values($headers); + $this->headers = array_combine($names, $values); + return $this; + } + + /** + * Returns all response headers or the value of a single specified + * response header. + * + * @param string $name Optional name of a single header for which the + * associated value should be returned + * + * @return array|string Associative array of all header values, a string + * containing the value of the header indicated by $name if one + * is set, or null if one is not + */ + public function getHeaders($name = null) + { + if ($name) { + $name = strtolower($name); + if (empty($this->headers[$name])) { + return null; + } + return $this->headers[$name]; + } + return $this->headers; + } + + /** + * Sets the response metadata. + * + * @param array $meta Associative array of response metadata + * + * @return Phergie_Plugin_Http_Response Provides a fluent interface + */ + public function setMeta(array $meta) + { + $this->meta = $meta; + return $this; + } + + /** + * Returns all metadata or the value of a single specified metadatum. + * + * @param string $name Optional name of a single metadatum for which the + * associated value should be returned + * + * @return array|string|null Associative array of all metadata values, a + * string containing the value of the metadatum indicated by + * $name if one is set, or null if one is not + */ + public function getMeta($name = null) + { + if ($name) { + if (empty($this->meta[$name])) { + return null; + } + return $this->meta[$name]; + } + return $this->meta; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php new file mode 100644 index 000000000..3af88dda9 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ideone.php @@ -0,0 +1,180 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Ideone + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ideone + */ + +/** + * Interfaces with ideone.com to execute code and return the result. + * + * @category Phergie + * @package Phergie_Plugin_Ideone + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ideone + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Ideone extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->plugins->getPlugin('Command'); + } + + /** + * Checks a service response for an error, sends a notice to the event + * source if an error has occurred, and returns whether an error was found. + * + * @param array $result Associative array representing the service response + * + * @return boolean TRUE if an error is found, FALSE otherwise + */ + protected function isError($result) + { + if ($result['error'] != 'OK') { + $this->doNotice($this->event->getNick(), 'ideone error: ' . $result['error']); + return true; + } + return false; + } + + /** + * Executes a source code sequence in a specified language and returns + * the result. + * + * @param string $language Programming language the source code is in + * @param string $code Source code to execute + * + * @return void + */ + public function onCommandIdeone($language, $code) + { + $source = $this->event->getSource(); + $nick = $this->event->getNick(); + + // Get authentication credentials + $user = $this->getConfig('ideone.user', 'test'); + $pass = $this->getConfig('ideone.pass', 'test'); + + // Normalize the command parameters + $language = strtolower($language); + + // Massage PHP code to allow for convenient shorthand + if ($language == 'php') { + if (!preg_match('/^<\?(?:php)?/', $code)) { + $code = '<?php ' . $code; + } + switch (substr($code, -1)) { + case '}': + case ';': + break; + default: + $code .= ';'; + break; + } + } + + // Identify the language to use + $client = new SoapClient('http://ideone.com/api/1/service.wsdl'); + $response = $client->getLanguages($user, $pass); + if ($this->isError($response)) { + return; + } + $languageLength = strlen($language); + foreach ($response['languages'] as $languageId => $languageName) { + if (strncasecmp($language, $languageName, $languageLength) == 0) { + break; + } + } + + // Send the paste data + $response = $client->createSubmission( + $user, + $pass, + $code, + $languageId, + null, // string input - data from stdin + true, // boolean run - TRUE to execute the code + false // boolean private - FALSE to make the paste public + ); + if ($this->isError($response)) { + return; + } + $link = $response['link']; + + // Wait until the paste data is processed or the service fails + $attempts = $this->getConfig('ideone.attempts', 10); + foreach (range(1, $attempts) as $attempt) { + $response = $client->getSubmissionStatus($user, $pass, $link); + if ($this->isError($response)) { + return; + } + if ($response['status'] == 0) { + $result = $response['result']; + break; + } else { + $result = null; + sleep(1); + } + } + if ($result == null) { + $this->doNotice($nick, 'ideone error: Timed out'); + return; + } + if ($result != 15) { + $this->doNotice($nick, 'ideone error: Status code ' . $result); + return; + } + + // Get details for the created paste + $response = $client->getSubmissionDetails( + $user, + $pass, + $link, + false, // boolean withSource - FALSE to not return the source code + false, // boolean withInput - FALSE to not return stdin data + true, // boolean withOutput - TRUE to include output + true, // boolean withStderr - TRUE to return stderr data + false // boolean withCmpinfo - TRUE to return compilation info + ); + if ($this->isError($response)) { + return; + } + + // Replace the output if it exceeds a specified maximum length + $outputLimit = $this->getConfig('ideone.output_limit', 100); + var_dump($response); + if ($outputLimit && strlen($response['output']) > $outputLimit) { + $response['output'] = 'Output is too long to post'; + } + + // Format the message + $msg = $this->getConfig('ideone.format', '%nick%: [ %link% ] %output%'); + $response['nick'] = $nick; + $response['link'] = 'http://ideone.com/' . $link; + foreach ($response as $key => $value) { + $msg = str_replace('%' . $key . '%', $value, $msg); + } + $this->doPrivmsg($source, $msg); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php new file mode 100755 index 000000000..622f7d3db --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Invisible.php @@ -0,0 +1,44 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Invisible + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Invisible + */ + +/** + * Marks the bot as invisible when it connects to the server. + * + * @category Phergie + * @package Phergie_Plugin_Invisible + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Invisible + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_2_3_2 + */ +class Phergie_Plugin_Invisible extends Phergie_Plugin_Abstract +{ + /** + * Marks the bot as invisible when it connects to the server. + * + * @return void + */ + public function onConnect() + { + $this->doMode($this->getConnection()->getNick(), '+i'); + $this->getPluginHandler()->removePlugin($this); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php new file mode 100644 index 000000000..d61096298 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Iterator.php @@ -0,0 +1,142 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Implements a filtering iterator for limiting executing of methods across + * a group of plugins. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Plugin_Iterator extends FilterIterator +{ + /** + * List of short names of plugins to exclude when iterating + * + * @var array + */ + protected $plugins = array(); + + /** + * List of method names where plugins with these methods will be + * excluded when iterating + * + * @var array + */ + protected $methods = array(); + + /** + * Overrides the parent constructor to reset the internal iterator's + * pointer to the current item, which the parent class errantly does not + * do. + * + * @param Iterator $iterator Iterator to filter + * + * @return void + * @link http://bugs.php.net/bug.php?id=52560 + */ + public function __construct(Iterator $iterator) + { + parent::__construct($iterator); + $this->rewind(); + } + + /** + * Adds to a list of plugins to exclude when iterating. + * + * @param mixed $plugins String containing the short name of a single + * plugin to exclude or an array of short names of multiple + * plugins to exclude + * + * @return Phergie_Plugin_Iterator Provides a fluent interface + */ + public function addPluginFilter($plugins) + { + if (is_array($plugins)) { + $this->plugins = array_unique( + array_merge($this->plugins, $plugins) + ); + } else { + $this->plugins[] = $plugins; + } + return $this; + } + + /** + * Adds to a list of method names where plugins defining these methods + * will be excluded when iterating. + * + * @param mixed $methods String containing the name of a single method + * or an array containing the name of multiple methods + * + * @return Phergie_Plugin_Iterator Provides a fluent interface + */ + public function addMethodFilter($methods) + { + if (is_array($methods)) { + $this->methods = array_merge($this->methods, $methods); + } else { + $this->methods[]= $methods; + } + return $this; + } + + /** + * Clears any existing plugin and methods filters. + * + * @return Phergie_Plugin_Iterator Provides a fluent interface + */ + public function clearFilters() + { + $this->plugins = array(); + $this->methods = array(); + } + + /** + * Implements FilterIterator::accept(). + * + * @return boolean TRUE to include the current item in those by returned + * during iteration, FALSE otherwise + */ + public function accept() + { + if (!$this->plugins && !$this->methods) { + return true; + } + + $current = $this->current(); + + if (in_array($current->getName(), $this->plugins)) { + return false; + } + + foreach ($this->methods as $method) { + if (method_exists($current, $method)) { + return false; + } + } + + return true; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php new file mode 100755 index 000000000..50c3ad8e6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Join.php @@ -0,0 +1,56 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Join + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Join + */ + +/** + * Joins a specified channel on command from a user. + * + * @category Phergie + * @package Phergie_Plugin_Join + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Join + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Join extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Joins a channel. + * + * @param string $channels Comma-delimited list of channels to join + * @param string $keys Optional comma-delimited list of channel keys + * + * @return void + */ + public function onCommandJoin($channels, $keys = null) + { + $this->doJoin($channels, $keys); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php new file mode 100644 index 000000000..27b4a087d --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php @@ -0,0 +1,451 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Karma + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Karma + */ + +/** + * Handles requests for incrementation or decrementation of a maintained list + * of counters for specified terms. + * + * @category Phergie + * @package Phergie_Plugin_Karma + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Karma + * @uses extension PDO + * @uses extension pdo_sqlite + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract +{ + /** + * SQLite object + * + * @var resource + */ + protected $db = null; + + /** + * Prepared statement to add a new karma record + * + * @var PDOStatement + */ + protected $insertKarma; + + /** + * Prepared statement to update an existing karma record + * + * @var PDOStatement + */ + protected $updateKarma; + + /** + * Retrieves an existing karma record + * + * @var PDOStatement + */ + protected $fetchKarma; + + /** + * Retrieves an existing fixed karma record + * + * @var PDOStatement + */ + protected $fetchFixedKarma; + + /** + * Retrieves a positive answer for a karma comparison + * + * @var PDOStatement + */ + protected $fetchPositiveAnswer; + + /** + * Retrieves a negative answer for a karma comparison + * + * @var PDOStatement + */ + protected $fetchNegativeAnswer; + + /** + * Check for dependencies and initializes a database connection and + * prepared statements. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $this->getDb(); + } + + /** + * Initializes prepared statements used by the plugin. + * + * @return void + */ + protected function initializePreparedStatements() + { + $this->fetchKarma = $this->db->prepare(' + SELECT karma + FROM karmas + WHERE term = :term + LIMIT 1 + '); + + $this->insertKarma = $this->db->prepare(' + INSERT INTO karmas (term, karma) + VALUES (:term, :karma) + '); + + $this->updateKarma = $this->db->prepare(' + UPDATE karmas + SET karma = :karma + WHERE term = :term + '); + + $this->fetchFixedKarma = $this->db->prepare(' + SELECT karma + FROM fixed_karmas + WHERE term = :term + LIMIT 1 + '); + + $this->fetchPositiveAnswer = $this->db->prepare(' + SELECT answer + FROM positive_answers + ORDER BY RANDOM() + LIMIT 1 + '); + + $this->fetchNegativeAnswer = $this->db->prepare(' + SELECT answer + FROM negative_answers + ORDER BY RANDOM() + LIMIT 1 + '); + } + + /** + * Returns a connection to the plugin database, initializing one if none + * is explicitly set. + * + * @return PDO Database connection + */ + public function getDb() + { + if (empty($this->db)) { + $this->db = new PDO('sqlite:' . dirname(__FILE__) . '/Karma/karma.db'); + $this->initializePreparedStatements(); + } + return $this->db; + } + + /** + * Sets the connection to the plugin database, mainly intended for unit + * testing. + * + * @param PDO $db Database connection + * + * @return Phergie_Plugin_Karma Provides a fluent interface + */ + public function setDb(PDO $db) + { + $this->db = $db; + $this->initializePreparedStatements(); + return $this; + } + + /** + * Get the canonical form of a given term. + * + * In the canonical form all sequences of whitespace + * are replaced by a single space and all characters + * are lowercased. + * + * @param string $term Term for which a canonical form is required + * + * @return string Canonical term + */ + protected function getCanonicalTerm($term) + { + $canonicalTerm = strtolower(preg_replace('|\s+|', ' ', trim($term, '()'))); + switch ($canonicalTerm) { + case 'me': + $canonicalTerm = strtolower($this->event->getNick()); + break; + case 'all': + case '*': + case 'everything': + $canonicalTerm = 'everything'; + break; + } + return $canonicalTerm; + } + + /** + * Intercepts a message and processes any contained recognized commands. + * + * @return void + */ + public function onPrivmsg() + { + $message = $this->getEvent()->getText(); + + $termPattern = '\S+?|\([^<>]+?\)+'; + $actionPattern = '(?P<action>\+\+|--)'; + + $modifyPattern = <<<REGEX + {^ + (?J) # allow overwriting capture names + \s* # ignore leading whitespace + + (?: # start with ++ or -- before the term + $actionPattern + (?P<term>$termPattern) + | # follow the term with ++ or -- + (?P<term>$termPattern) + $actionPattern # allow no whitespace between the term and the action + ) + $}ix +REGEX; + + $versusPattern = <<<REGEX + {^ + (?P<term0>$termPattern) + \s+(?P<method><|>)\s+ + (?P<term1>$termPattern)$# + $}ix +REGEX; + + $match = null; + + if (preg_match($modifyPattern, $message, $match)) { + $action = $match['action']; + $term = $this->getCanonicalTerm($match['term']); + $this->modifyKarma($term, $action); + } elseif (preg_match($versusPattern, $message, $match)) { + $term0 = trim($match['term0']); + $term1 = trim($match['term1']); + $method = $match['method']; + $this->compareKarma($term0, $term1, $method); + } + } + + /** + * Get the karma rating for a given term. + * + * @param string $term Term for which the karma rating needs to be + * retrieved + * + * @return void + */ + public function onCommandKarma($term) + { + $source = $this->getEvent()->getSource(); + $nick = $this->getEvent()->getNick(); + + $canonicalTerm = $this->getCanonicalTerm($term); + + $fixedKarma = $this->fetchFixedKarma($canonicalTerm); + if ($fixedKarma) { + $message = $nick . ': ' . $term . ' ' . $fixedKarma; + $this->doPrivmsg($source, $message); + return; + } + + $karma = $this->fetchKarma($canonicalTerm); + + $message = $nick . ': '; + + if ($term == 'me') { + $message .= 'You have'; + } else { + $message .= $term . ' has'; + } + + $message .= ' '; + + if ($karma) { + $message .= 'karma of ' . $karma; + } else { + $message .= 'neutral karma'; + } + + $message .= '.'; + + $this->doPrivmsg($source, $message); + } + + /** + * Resets the karma for a term to 0. + * + * @param string $term Term for which to reset the karma rating + * + * @return void + */ + public function onCommandReincarnate($term) + { + $data = array( + ':term' => $term, + ':karma' => 0 + ); + $this->updateKarma->execute($data); + } + + /** + * Compares the karma between two terms. Optionally increases/decreases + * the karma of either term. + * + * @param string $term0 First term + * @param string $term1 Second term + * @param string $method Comparison method (< or >) + * + * @return void + */ + protected function compareKarma($term0, $term1, $method) + { + $event = $this->getEvent(); + $nick = $event->getNick(); + $source = $event->getSource(); + + $canonicalTerm0 = $this->getCanonicalTerm($term0); + $canonicalTerm1 = $this->getCanonicalTerm($term1); + + $fixedKarma0 = $this->fetchFixedKarma($canonicalTerm0); + $fixedKarma1 = $this->fetchFixedKarma($canonicalTerm1); + + if ($fixedKarma0 || $fixedKarma1) { + return; + } + + if ($canonicalTerm0 == 'everything') { + $change = $method == '<' ? '++' : '--'; + $karma0 = 0; + $karma1 = $this->modifyKarma($canonicalTerm1, $change); + } elseif ($canonicalTerm1 == 'everything') { + $change = $method == '<' ? '--' : '++'; + $karma0 = $this->modifyKarma($canonicalTerm0, $change); + $karma1 = 0; + } else { + $karma0 = $this->fetchKarma($canonicalTerm0); + $karma1 = $this->fetchKarma($canonicalTerm1); + } + + // Combining the first and second branches here causes an odd + // single-line lapse in code coverage, but the lapse disappears if + // they're separated + if ($method == '<' && $karma0 < $karma1) { + $replies = $this->fetchPositiveAnswer; + } elseif ($method == '>' && $karma0 > $karma1) { + $replies = $this->fetchPositiveAnswer; + } else { + $replies = $this->fetchNegativeAnswer; + } + $replies->execute(); + $reply = $replies->fetchColumn(); + + if (max($karma0, $karma1) == $karma1) { + list($canonicalTerm0, $canonicalTerm1) = + array($canonicalTerm1, $canonicalTerm0); + } + + $message = str_replace( + array('%owner%','%owned%'), + array($canonicalTerm0, $canonicalTerm1), + $reply + ); + + $this->doPrivmsg($source, $message); + } + + /** + * Modifes a term's karma. + * + * @param string $term Term to modify + * @param string $action Karma action (either ++ or --) + * + * @return int Modified karma rating + */ + protected function modifyKarma($term, $action) + { + $karma = $this->fetchKarma($term); + if ($karma !== false) { + $statement = $this->updateKarma; + } else { + $statement = $this->insertKarma; + } + + $karma += ($action == '++') ? 1 : -1; + + $args = array( + ':term' => $term, + ':karma' => $karma + ); + $statement->execute($args); + + return $karma; + } + + /** + * Returns the karma rating for a specified term for which the karma + * rating can be modified. + * + * @param string $term Term for which to fetch the corresponding karma + * rating + * + * @return integer|boolean Integer value denoting the term's karma or + * FALSE if there is the specified term has no associated karma + * rating + */ + protected function fetchKarma($term) + { + $this->fetchKarma->execute(array(':term' => $term)); + $result = $this->fetchKarma->fetch(PDO::FETCH_ASSOC); + + if ($result === false) { + return false; + } + + return (int) $result['karma']; + } + + /** + * Returns a phrase describing the karma rating for a specified term for + * which the karma rating is fixed. + * + * @param string $term Term for which to fetch the corresponding karma + * rating + * + * @return string Phrase describing the karma rating, which may be append + * to the term to form a complete response + */ + protected function fetchFixedKarma($term) + { + $this->fetchFixedKarma->execute(array(':term' => $term)); + $result = $this->fetchFixedKarma->fetch(PDO::FETCH_ASSOC); + + if ($result === false) { + return false; + } + + return $result['karma']; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php new file mode 100644 index 000000000..d00cae0dd --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Lart.php @@ -0,0 +1,303 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Lart + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Lart + */ + +/** + * Accepts terms and corresponding definitions for storage to a local data + * source and performs and returns the result of lookups for term definitions + * as they are requested. + * + * @category Phergie + * @package Phergie_Plugin_Lart + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Lart + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses extension PDO + * @uses extension pdo_sqlite + */ +class Phergie_Plugin_Lart extends Phergie_Plugin_Abstract +{ + /** + * PDO instance for the database + * + * @var PDO + */ + protected $db; + + /** + * Prepared statement for inserting a new definition + * + * @var PDOStatement + */ + protected $save; + + /** + * Prepared statement for deleting the definition for a given term + * + * @var PDOStatement + */ + protected $delete; + + /** + * Prepared statement for searching for a definition for which the term + * matches as a regular expression against a given search string + * + * @var PDOStatement + */ + protected $process; + + /** + * Prepared statement for searching for a definition by its exact term + * + * @var PDOStatement + */ + protected $select; + + /** + * Checks for dependencies and initializes the database. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->fail('PDO and pdo_sqlite extensions must be installed'); + } + + $this->plugins->getPlugin('Command'); + + $dir = dirname(__FILE__) . '/' . $this->getName(); + $path = $dir . '/lart.db'; + $exists = file_exists($path); + if (!$exists) { + mkdir($dir); + } + + try { + $this->db = new PDO('sqlite:' . $path); + } catch (PDO_Exception $e) { + throw new Phergie_Plugin_Exception($e->getMessage()); + } + + $this->db->sqliteCreateFunction('preg_match', 'preg_match'); + + if (!$exists) { + $this->db->exec(' + CREATE TABLE lart ( + name VARCHAR(255), + definition TEXT, + hostmask VARCHAR(50), + tstamp VARCHAR(19) + ) + '); + $this->db->exec(' + CREATE UNIQUE INDEX lart_name ON lart (name) + '); + } + + $this->save = $this->db->prepare(' + REPLACE INTO lart (name, definition, hostmask, tstamp) + VALUES (:name, :definition, :hostmask, :tstamp) + '); + + $this->process = $this->db->prepare(' + SELECT * + FROM lart + WHERE preg_match(name, :name) + '); + + $this->select = $this->db->prepare(' + SELECT * + FROM lart + WHERE name = :name + '); + + $this->delete = $this->db->prepare(' + DELETE FROM lart + WHERE name = :name + '); + } + + /** + * Retrieves the definition for a given term if it exists. + * + * @param string $term Term to search for + * + * @return mixed String containing the definition or FALSE if no definition + * exists + */ + protected function getLart($term) + { + $this->process->execute(array(':name' => $term)); + $row = $this->process->fetchObject(); + if ($row === false) { + return false; + } + preg_match($row->name, $term, $match); + $definition = preg_replace( + "/(?:\\\\|\\$)([0-9]+)/e", + '$match[\1]', + $row->definition + ); + $event = $this->getEvent(); + $definition = str_replace( + array('$source', '$nick'), + array($event->getSource(), $event->getNick()), + $definition + ); + return $definition; + } + + /** + * Deletes a given definition. + * + * @param string $term Term for which the definition should be deleted + * + * @return boolean TRUE if the definition was found and deleted, FALSE + * otherwise + */ + protected function deleteLart($term) + { + $this->delete->execute(array(':name' => $term)); + return ($this->delete->rowCount() > 0); + } + + /** + * Saves a given definition. + * + * @param string $term Term to trigger a response containing the + * corresponding definition, may be a regular expression + * @param string $definition Definition corresponding to the term + * + * @return boolean TRUE if the definition was saved successfully, FALSE + * otherwise + */ + protected function saveLart($term, $definition) + { + $data = array( + ':name' => $term, + ':definition' => $definition, + ':hostmask' => (string) $this->getEvent()->getHostmask(), + ':tstamp' => time() + ); + $this->save->execute($data); + return ($this->save->rowCount() > 0); + } + + /** + * Returns information about a definition. + * + * @param string $term Term about which to return information + * + * @return void + */ + public function onCommandLartinfo($term) + { + $this->select->execute(array(':name' => $term)); + $row = $this->select->fetchObject(); + $msg = $this->getEvent()->getNick() . ': '; + if (!$row) { + $msg .= 'Lart not found'; + } else { + $msg .= 'Term: ' . $row->name + . ', Definition: ' . $row->definition + . ', User: ' . $row->hostmask + . ', Added: ' . date('n/j/y g:i A', $row->tstamp); + } + $this->doNotice($this->getEvent()->getSource(), $msg); + } + + /** + * Creates a new definition. + * + * @param string $term Term to add + * @param string $definition Definition to add + * + * @return void + */ + public function onCommandAddlart($term, $definition) + { + $result = $this->saveLart($term, $definition); + if ($result) { + $msg = 'Lart saved successfully'; + } else { + $msg = 'Lart could not be saved'; + } + $this->doNotice($this->getEvent()->getSource(), $msg); + } + + /** + * Removes an existing definition. + * + * @param string $term Term for which the definition should be removed + * + * @return void + */ + public function onCommandDeletelart($term) + { + $source = $this->getEvent()->getSource(); + if ($this->deleteLart($term)) { + $msg = 'Lart deleted successfully'; + } else { + $msg = 'Lart not found'; + } + $this->doNotice($source, $msg); + } + + /** + * Processes definition triggers in the text of the current event. + * + * @return void + */ + protected function processLart() + { + $lart = $this->getLart($this->getEvent()->getText()); + if ($lart) { + if (strpos($lart, '/me') === 0) { + $lart = substr($lart, 4); + $method = 'doAction'; + } else { + $method = 'doPrivmsg'; + } + $this->$method($this->getEvent()->getSource(), $lart); + } + } + + /** + * Processes definition triggers in messages. + * + * @return void + */ + public function onPrivmsg() + { + $this->processLart(); + } + + /** + * Processes definition triggers in CTCP actions. + * + * @return void + */ + public function onAction() + { + $this->processLart(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php new file mode 100644 index 000000000..af8fc7287 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php @@ -0,0 +1,112 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Message + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Message + */ + +/** + * Generalized plugin providing utility methods for + * prefix and bot named based message extraction. + * + * @category Phergie + * @package Phergie_Plugin_Message + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Message + */ +class Phergie_Plugin_Message extends Phergie_Plugin_Abstract +{ + + /** + * Check whether a message is specifically targeted at the bot. + * This is the case when the message starts with the bot's name + * followed by [,:>] or when it is a private message. + * + * @return boolean true when the message is specifically targeted at the bot, + * false otherwise. + */ + public function isTargetedMessage() + { + $event = $this->getEvent(); + + $self = preg_quote($this->connection->getNick()); + + $targetPattern = <<<REGEX + {^ + \s*{$self}\s*[:>,].* # expect the bots name, followed by a [:>,] + $}ix +REGEX; + + return !$event->isInChannel() + || preg_match($targetPattern, $event->getText()) > 0; + } + + /** + * Allow for prefix and bot name aware extraction of a message + * + * @return string|bool $message The message, which is possibly targeted at the + * bot or false if a prefix requirement failed + */ + public function getMessage() + { + $event = $this->getEvent(); + + $prefix = preg_quote($this->getConfig('command.prefix')); + $self = preg_quote($this->connection->getNick()); + $message = $event->getText(); + + // $prefixPattern matches : Phergie, do command <parameters> + // where $prefix = 'do' : do command <parameters> + // : Phergie, command <parameters> + $prefixPattern = <<<REGEX + {^ + (?: + \s*{$self}\s*[:>,]\s* # start with bot name + (?:{$prefix})? # which is optionally followed by the prefix + | + \s*{$prefix} # or start with the prefix + ) + \s*(.*) # always end with the message + $}ix +REGEX; + + // $noPrefixPattern matches : Phergie, command <parameters> + // : command <parameters> + $noPrefixPattern = <<<REGEX + {^ + \s*(?:{$self}\s*[:>,]\s*)? # optionally start with the bot name + (.*?) # always end with the message + $}ix +REGEX; + + $pattern = $noPrefixPattern; + + // If a prefix is set, force it as a requirement + if ($prefix && $event->isInChannel()) { + $pattern = $prefixPattern; + } + + $match = null; + + if (!preg_match($pattern, $message, $match)) { + return false; + } + + return $match[1]; + } +}
\ No newline at end of file diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php new file mode 100644 index 000000000..e97abecf7 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/NickServ.php @@ -0,0 +1,178 @@ +<?php
+/**
+ * Phergie
+ *
+ * PHP version 5
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.
+ * It is also available through the world-wide-web at this URL:
+ * http://phergie.org/license
+ *
+ * @category Phergie
+ * @package Phergie_Plugin_NickServ
+ * @author Phergie Development Team <team@phergie.org>
+ * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
+ * @license http://phergie.org/license New BSD License
+ * @link http://pear.phergie.org/package/Phergie_Plugin_NickServ
+ */
+
+/**
+ * Intercepts and responds to messages from the NickServ agent requesting that
+ * the bot authenticate its identify.
+ *
+ * The password configuration setting should contain the password registered
+ * with NickServ for the nick used by the bot.
+ *
+ * @category Phergie
+ * @package Phergie_Plugin_NickServ
+ * @author Phergie Development Team <team@phergie.org>
+ * @license http://phergie.org/license New BSD License
+ * @link http://pear.phergie.org/package/Phergie_Plugin_NickServ
+ * @uses Phergie_Plugin_Command pear.phergie.org
+ */
+class Phergie_Plugin_NickServ extends Phergie_Plugin_Abstract
+{
+ /**
+ * Nick of the NickServ bot
+ *
+ * @var string
+ */
+ protected $botNick;
+
+ /**
+ * Identify message
+ */
+ protected $identifyMessage;
+
+ /**
+ * Checks for dependencies and required configuration settings.
+ *
+ * @return void
+ */
+ public function onLoad()
+ {
+ $this->getPluginHandler()->getPlugin('Command');
+
+ // Get the name of the NickServ bot, defaults to NickServ
+ $this->botNick = $this->getConfig('nickserv.botnick', 'NickServ');
+
+ // Get the identify message
+ $this->identifyMessage = $this->getConfig(
+ 'nickserv.identify_message',
+ '/This nickname is registered./'
+ );
+ }
+
+ /**
+ * Checks for a notice from NickServ and responds accordingly if it is an
+ * authentication request or a notice that a ghost connection has been
+ * killed.
+ *
+ * @return void
+ */
+ public function onNotice()
+ {
+ $event = $this->event;
+ if (strtolower($event->getNick()) == strtolower($this->botNick)) {
+ $message = $event->getArgument(1);
+ $nick = $this->connection->getNick();
+ if (preg_match($this->identifyMessage, $message)) {
+ $password = $this->config['nickserv.password'];
+ if (!empty($password)) {
+ $this->doPrivmsg($this->botNick, 'IDENTIFY ' . $password);
+ }
+ unset($password);
+ } elseif (preg_match('/^.*' . $nick . '.* has been killed/', $message)) {
+ $this->doNick($nick);
+ }
+ }
+ }
+
+ /**
+ * Checks to see if the original nick has quit; if so, take the name back.
+ *
+ * @return void
+ */
+ public function onQuit()
+ {
+ $eventNick = $this->event->getNick();
+ $nick = $this->connection->getNick();
+ if ($eventNick == $nick) {
+ $this->doNick($nick);
+ }
+ }
+
+ /**
+ * Changes the in-memory configuration setting for the bot nick if it is
+ * successfully changed.
+ *
+ * @return void
+ */
+ public function onNick()
+ {
+ $event = $this->event;
+ $connection = $this->connection;
+ if ($event->getNick() == $connection->getNick()) {
+ $connection->setNick($event->getArgument(0));
+ }
+ }
+
+ /**
+ * Provides a command to terminate ghost connections.
+ *
+ * @return void
+ */
+ public function onCommandGhostbust()
+ {
+ $event = $this->event;
+ $user = $event->getNick();
+ $conn = $this->connection;
+ $nick = $conn->getNick();
+
+ if ($nick != $this->config['connections'][$conn->getHost()]['nick']) {
+ $password = $this->config['nickserv.password'];
+ if (!empty($password)) {
+ $this->doPrivmsg(
+ $this->event->getSource(),
+ $user . ': Attempting to ghost ' . $nick .'.'
+ );
+ $this->doPrivmsg(
+ $this->botNick,
+ 'GHOST ' . $nick . ' ' . $password,
+ true
+ );
+ }
+ }
+ }
+
+ /**
+ * Automatically send the GHOST command if the bot's nick is in use.
+ *
+ * @return void
+ */
+ public function onResponse()
+ {
+ if ($this->event->getCode() == Phergie_Event_Response::ERR_NICKNAMEINUSE) {
+ $password = $this->config['nickserv.password'];
+ if (!empty($password)) {
+ $this->doPrivmsg(
+ $this->botNick,
+ 'GHOST ' . $this->connection->getNick() . ' ' . $password
+ );
+ }
+ }
+ }
+
+ /**
+ * Handle the server sending a KILL request.
+ *
+ * @return void
+ */
+ public function onKill()
+ {
+ $this->doQuit($this->event->getArgument(1));
+ }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php new file mode 100755 index 000000000..c07cdd918 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Part.php @@ -0,0 +1,55 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Part + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Part + */ + +/** + * Parts a specified channel on command from a user. + * + * @category Phergie + * @package Phergie_Plugin_Part + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Part + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Part extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Parts a channel. + * + * @param string $channels Comma-delimited list of channels to leave + * + * @return void + */ + public function onCommandPart($channels) + { + $this->doPart($channels); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php new file mode 100644 index 000000000..e10d101e7 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php.php @@ -0,0 +1,78 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + */ + +/** + * Returns information on PHP functions as requested. + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + * @uses extension pdo + * @uses extension pdo_sqlite + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Php extends Phergie_Plugin_Abstract +{ + /** + * Data source to use + * + * @var Phergie_Plugin_Php_Source + */ + protected $source; + + /** + * Check for dependencies. + * + * @return void + */ + public function onLoad() + { + // @todo find a way to move this to Phergie_Plugin_Php_Source_Local + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->fail('PDO and pdo_sqlite extensions must be installed'); + } + + $this->getPluginHandler()->getPlugin('Command'); + + $this->source = new Phergie_Plugin_Php_Source_Local; + } + + /** + * Searches the data source for the requested function. + * + * @param string $functionName Name of the function to search for + * + * @return void + */ + public function onCommandPhp($functionName) + { + $nick = $this->event->getNick(); + if ($function = $this->source->findFunction($functionName)) { + $msg = $nick . ': ' . $function['description']; + $this->doPrivmsg($this->event->getSource(), $msg); + } else { + $msg = 'Search for function ' . $functionName . ' returned no results.'; + $this->doNotice($nick, $msg); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php new file mode 100644 index 000000000..c6cf3261b --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source.php @@ -0,0 +1,46 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + */ + +/** + * Data source interface for the Php plugin. + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + * @uses extension pdo + * @uses extension pdo_sqlite + * @uses Phergie_Plugin_Command pear.phergie.org + */ +interface Phergie_Plugin_Php_Source +{ + /** + * Searches for a description of the function. + * + * @param string $function Search pattern to match against the function + * name, wildcards supported using % + * + * @return array|null Associative array containing the function name and + * description or NULL if no results are found + */ + public function findFunction($function); +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php new file mode 100644 index 000000000..9292bea84 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Php/Source/Local.php @@ -0,0 +1,208 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + */ + +/** + * Data source for {@see Phergie_Plugin_Php}. This source reads function + * descriptions from a file and stores them in a SQLite database. When a + * function description is requested, the function is retrieved from the + * local database. + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + * @uses extension pdo + * @uses extension pdo_sqlite + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Php_Source_Local implements Phergie_Plugin_Php_Source +{ + /** + * Local database for storage + * + * @var PDO + */ + protected $database; + + /** + * Source of the PHP function summary + * + * @var string + */ + protected $url = 'http://cvs.php.net/viewvc.cgi/phpdoc/funcsummary.txt?revision=HEAD'; + + /** + * Constructor to initialize the data source. + * + * @return void + */ + public function __construct() + { + $path = dirname(__FILE__); + + try { + $this->database = new PDO('sqlite:' . $path . '/functions.db'); + $this->buildDatabase(); + // @todo Modify this to be rethrown as an appropriate + // Phergie_Plugin_Exception and handled in Phergie_Plugin_Php + } catch (PDOException $e) { + echo 'PDO failure: '.$e->getMessage(); + } + } + + /** + * Searches for a description of the function. + * + * @param string $function Search pattern to match against the function + * name, wildcards supported using % + * + * @return array|null Associative array containing the function name and + * description or NULL if no results are found + */ + public function findFunction($function) + { + // Remove possible parentheses + $split = preg_split('{\(|\)}', $function); + $function = (count($split)) ? array_shift($split) : $function; + + // Prepare the database statement + $stmt = $this->database->prepare('SELECT `name`, `description` FROM `functions` WHERE `name` LIKE :function'); + $stmt->execute(array(':function' => $function)); + + // Check the results + if (count($stmt) > 0) { + $result = $stmt->fetch(PDO::FETCH_ASSOC); + /** + * @todo add class and function URLS + * class methods: http://php.net/manual/en/classname.methodname.php + * functions: http://php.net/manual/en/function.functionname.php + * where '_' is replaced with '-' + */ + return $result; + } + + // No results found, return + return null; + } + + /** + * Build the database and parses the function summary file into it. + * + * @param bool $rebuild TRUE to force a rebuild of the table used to + * house function information, FALSE otherwise, defaults to FALSE + * + * @return void + */ + protected function buildDatabase($rebuild = false) + { + // Check to see if the functions table exists + $checkstmt = $this->database->query("SELECT COUNT(*) FROM `sqlite_master` WHERE `name` = 'functions'"); + $checkstmt->execute(); + $result = $checkstmt->fetch(PDO::FETCH_ASSOC); + unset( $checkstmt ); + $table = $result['COUNT(*)']; + unset( $result ); + // If the table doesn't exist, create it + if (!$table) { + $this->database->exec('CREATE TABLE `functions` (`name` VARCHAR(255), `description` TEXT)'); + $this->database->exec('CREATE UNIQUE INDEX `functions_name` ON `functions` (`name`)'); + } + + // If we created a new table, fill it with data + if (!$table || $rebuild) { + // Get the contents of the source file + // @todo Handle possible error cases better here; the @ operator + // shouldn't be needed + $contents = @file($this->url, FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES); + + if (!$contents) { + return; + } + + // Parse the contents + $valid = array(); + $firstPart = ''; + $lineNumber = 0; + foreach ($contents as $line) { + // Clean the current line + $line = trim($line); + + // Skip comment lines + if (0 === strpos($line, '#')) { + // reset the line if the current line is odd + if (($lineNumber % 2) !== 0) { + $lineNumber--; + } + continue; + } + + /* + * If the current line is even, it's the first part of the + * complete function description ... + */ + if (($lineNumber % 2) === 0) { + $firstPart = $line; + } else { + // ... it's the last part of the complete function description + $completeLine = $firstPart . ' ' . $line; + $firstPart = ''; + if (preg_match('{^([^\s]*)[\s]?([^)]*)\(([^\)]*)\)[\sU]+([\sa-zA-Z0-9\.,\-_()]*)$}', $completeLine, $matches)) { + $valid[] = $matches; + } + } + // Up the line number before going to the next line + $lineNumber++; + } + // free up some memory + unset($contents); + + // Process the valid matches + if (count($valid) > 0) { + // Clear the database + $this->database->exec('DELETE * FROM `functions`'); + + // Prepare the sql statement + $stmt = $this->database->prepare('INSERT INTO `functions` (`name`, `description`) VALUES (:name, :description)'); + $this->database->beginTransaction(); + + // Insert the data + foreach ($valid as $function) { + // Extract function values + list( , $retval, $name, $params, $desc) = $function; + if (empty($name)) { + $name = $retval; + $retval = ''; + } + // Reconstruct the complete function line + $line = trim($retval . ' ' . $name . '(' . $params . ') - ' . $desc); + // Execute the statement + $stmt->execute(array(':name' => $name, ':description' => $line)); + } + + // Commit the changes to the database + $this->database->commit(); + } + // free up some more memory + unset($valid); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php new file mode 100755 index 000000000..021670d97 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Ping.php @@ -0,0 +1,162 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Ping + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ping + */ + +/** + * Uses a self CTCP PING to ensure that the client connection has not been + * dropped. + * + * @category Phergie + * @package Phergie_Plugin_Ping + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Ping + */ +class Phergie_Plugin_Ping extends Phergie_Plugin_Abstract +{ + /** + * Timestamp for the last instance in which an event was received + * + * @var int + */ + protected $lastEvent; + + /** + * Timestamp for the last instance in which a PING was sent + * + * @var int + */ + protected $lastPing; + + /** + * Initialize event timestamps upon connecting to the server. + * + * @return void + */ + public function onConnect() + { + $this->lastEvent = time(); + $this->lastPing = null; + } + + /** + * Updates the timestamp since the last received event when a new event + * arrives. + * + * @return void + */ + public function preEvent() + { + $this->lastEvent = time(); + } + + /** + * Clears the ping time if a reply is received. + * + * @return void + */ + public function onPingResponse() + { + $this->lastPing = null; + } + + /** + * Performs a self ping if the event threshold has been exceeded or + * issues a termination command if the ping threshold has been exceeded. + * + * @return void + */ + public function onTick() + { + $time = time(); + if (!empty($this->lastPing)) { + if ($time - $this->lastPing > $this->getConfig('ping.ping', 20)) { + $this->doQuit(); + } + } elseif ( + $time - $this->lastEvent > $this->getConfig('ping.event', 300) + ) { + $this->lastPing = $time; + $this->doPing($this->getConnection()->getNick(), $this->lastPing); + } + } + + /** + * Gets the last ping time + * lastPing needs exposing for things such as unit testing + * + * @return int timestamp of last ping + */ + public function getLastPing() + { + return $this->lastPing; + } + + /** + * Set the last ping time + * lastPing needs to be exposed for unit testing + * + * @param int|null $ping timestamp of last ping + * + * @return self + */ + public function setLastPing($ping = null) + { + if (null === $ping) { + $ping = time(); + } + if (!is_int($ping)) { + throw new InvalidArgumentException('$ping must be an integer or null'); + } + $this->lastPing = $ping; + return $this; + } + + /** + * Gets the last event time + * lastEvent needs exposing for things such as unit testing + * + * @return int timestamp of last ping + */ + public function getLastEvent() + { + return $this->lastEvent; + } + + /** + * Set the last event time + * lastEvent needs to be exposed for unit testing + * + * @param int|null $event timestamp of last ping + * + * @return self + */ + public function setLastEvent($event = null) + { + if (null === $event) { + $event = time(); + } + if (!is_int($event)) { + throw new InvalidArgumentException('$ping must be an integer or null'); + } + $this->lastEvent = $event; + return $this; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php new file mode 100755 index 000000000..54e19fc06 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Pong.php @@ -0,0 +1,44 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Pong + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Pong + */ + +/** + * Responds to PING requests from the server. + * + * @category Phergie + * @package Phergie_Plugin_Pong + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Pong + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_2 + * @link http://irchelp.org/irchelp/rfc/chapter4.html#c4_6_3 + */ +class Phergie_Plugin_Pong extends Phergie_Plugin_Abstract +{ + /** + * Sends a PONG response for each PING request received by the server. + * + * @return void + */ + public function onPing() + { + $this->doPong($this->getEvent()->getArgument(0)); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php new file mode 100755 index 000000000..2312567fb --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Prioritize.php @@ -0,0 +1,99 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Prioritize + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Prioritize + */ + +/** + * Prioritizes events such that they are executed in order from least to most + * destructive. + * + * @category Phergie + * @package Phergie_Plugin_Prioritize + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Prioritize + */ +class Phergie_Plugin_Prioritize extends Phergie_Plugin_Abstract +{ + /** + * Event types ordered by priority of execution + * + * @var array + */ + protected $priority = array( + 'raw', + 'pass', + 'user', + 'ping', + 'pong', + 'notice', + 'join', + 'list', + 'names', + 'version', + 'stats', + 'links', + 'time', + 'trace', + 'admin', + 'info', + 'who', + 'whois', + 'whowas', + 'mode', + 'privmsg', + 'action', + 'nick', + 'topic', + 'invite', + 'kill', + 'part', + 'quit' + ); + + /** + * Prioritizes events from least to most destructive. + * + * @return void + */ + public function preDispatch() + { + $events = $this->getEventHandler(); + + // Categorize events by type + $categorized = array(); + foreach ($events as $event) { + $type = $event->getType(); + if (!isset($categorized[$type])) { + $categorized[$type] = array(); + } + $categorized[$type][] = $event; + } + + // Order events by type from least to most destructive + $types = array_intersect($this->priority, array_keys($categorized)); + $prioritized = array(); + foreach ($types as $type) { + $prioritized = array_merge($prioritized, $categorized[$type]); + } + + // Replace the original events array with the prioritized one + $events->replaceEvents($prioritized); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php new file mode 100644 index 000000000..bede0be2c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Puppet.php @@ -0,0 +1,89 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Puppet + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Puppet + */ + +/** + * Allows a user to effectively speak and act as the bot. + * + * @category Phergie + * @package Phergie_Plugin_Puppet + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Puppet + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Puppet extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Handles a request for the bot to repeat a given message in a specified + * channel. + * + * <code>say #chan message</code> + * + * @param string $channel Name of the channel + * @param string $message Message to repeat + * + * @return void + */ + public function onCommandSay($channel, $message) + { + $this->doPrivmsg($channel, $message); + } + + /** + * Handles a request for the bot to repeat a given action in a specified + * channel. + * + * <code>act #chan action</code> + * + * @param string $channel Name of the channel + * @param string $action Action to perform + * + * @return void + */ + public function onCommandAct($channel, $action) + { + $this->doAction($channel, $action); + } + + /** + * Handles a request for the bot to send the server a raw message + * + * <code>raw message</code> + * + * @param string $message Message to send + * + * @return void + */ + public function onCommandRaw($message) + { + $this->doRaw($message); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php new file mode 100755 index 000000000..eca22a98e --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Quit.php @@ -0,0 +1,54 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Quit + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Quit + */ + +/** + * Terminates the current connection upon command. + * + * @category Phergie + * @package Phergie_Plugin_Quit + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Quit + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Quit extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Issues a quit command when a message is received requesting that the + * bot terminate the current connection. + * + * @return void + */ + public function onCommandQuit() + { + $this->doQuit('Requested by ' . $this->getEvent()->getNick()); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php new file mode 100755 index 000000000..430577081 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php @@ -0,0 +1,122 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Reload + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Reload + */ + +/** + * Facilitates reloading of individual plugins for development purposes. + * Note that, because existing class definitions cannot be removed from + * memory, increased memory usage is an expected result of using this plugin. + * + * @category Phergie + * @package Phergie_Plugin_Reload + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Reload + * @uses Phergie_Plugin_Command pear.phergie.org + */ +class Phergie_Plugin_Reload extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Command'); + } + + /** + * Reloads a specified plugin. + * + * @param string $plugin Short name of the plugin to reload + * + * @return void + */ + public function onCommandReload($plugin) + { + $plugin = ucfirst($plugin); + + $evalClass = true; + if (strpos($plugin, ' ') !== false) { + $args = explode(' ', $plugin); + $plugin = $args[0]; + if (strtolower($args[1]) == 'force') { + $evalClass = false; + } + } + + if (!$this->plugins->hasPlugin($plugin)) { + echo 'DEBUG(Reload): ' . ucfirst($plugin) . ' is not loaded yet, loading', PHP_EOL; + try { + $this->plugins->getPlugin($plugin); + $this->plugins->command->populateMethodCache(); + } catch (Phergie_Plugin_Exception $e) { + if ($e->getCode() == Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND) { + echo 'DEBUG(Reload): ', $e->getMessage(), PHP_EOL; + } else { + throw $e; + } + } + return; + } + + try { + $info = $this->plugins->getPluginInfo($plugin); + } catch (Phergie_Plugin_Exception $e) { + $source = $this->event->getSource(); + $nick = $this->event->getNick(); + $this->doNotice($source, $nick . ': ' . $e->getMessage()); + return; + } + + $class = $info['class']; + $contents = file_get_contents($info['file']); + $newClass = $class . '_' . sha1($contents); + + if (class_exists($newClass, false)) { + if ($evalClass == true) { + echo 'DEBUG(Reload): Class ', $class, ' has not changed since last reload', PHP_EOL; + return; + } + } else { + $contents = preg_replace( + array('/^<\?(?:php)?/', '/class\s+' . $class . '/i'), + array('', 'class ' . $newClass), + $contents + ); + eval($contents); + } + + $instance = new $newClass; + $instance->setName($plugin); + $instance->setEvent($this->event); + $this->plugins + ->removePlugin($plugin) + ->addPlugin($instance); + + $this->plugins->command->populateMethodCache(); + if ($this->plugins->hasPlugin('Help')) { + $this->plugins->help->populateRegistry(); + } + + echo 'DEBUG(Reload): Reloaded ', $class, ' to ', $newClass, PHP_EOL; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php new file mode 100644 index 000000000..42d674c68 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Remind.php @@ -0,0 +1,378 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Remind + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Remind + */ + +/** + * Parses and logs messages that should be relayed to other users the next time + * the recipient is active on the same channel. + * + * @category Phergie + * @package Phergie_Plugin_Remind + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Remind + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Time pear.phergie.org + * @uses extension PDO + * @uses extension pdo_sqlite + */ +class Phergie_Plugin_Remind extends Phergie_Plugin_Abstract +{ + /** + * Number of reminders to show in public. + */ + protected $publicReminders = 3; + + /** + * PDO resource for a SQLite database containing the reminders. + * + * @var resource + */ + protected $db; + + /** + * Flag that indicates whether or not to use an in-memory reminder list. + * + * @var bool + */ + protected $keepListInMemory = true; + + /** + * In-memory store for pending reminders. + */ + protected $msgStorage = array(); + + /** + * Check for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $plugins->getPlugin('Time'); + + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->fail('PDO and pdo_sqlite extensions must be installed'); + } + + $dir = dirname(__FILE__) . '/' . $this->getName(); + $path = $dir . '/reminder.db'; + if (!file_exists($dir)) { + mkdir($dir); + } + + if (isset($this->config['remind.use_memory'])) { + $this->keepListInMemory = (bool) $this->config['remind.use_memory']; + } + + if (isset($this->config['remind.public_reminders'])) { + $this->publicReminders = (int) $this->config['remind.public_reminders']; + $this->publicReminders = max($this->publicReminders, 0); + } + + try { + $this->db = new PDO('sqlite:' . $path); + $this->createTables(); + } catch (PDO_Exception $e) { + throw new Phergie_Plugin_Exception($e->getMessage()); + } + } + + /** + * Intercepts a message and processes any contained recognized commands. + * + * @return void + */ + public function onPrivmsg() + { + $source = $this->getEvent()->getSource(); + $nick = $this->getEvent()->getNick(); + + $this->deliverReminders($source, $nick); + } + + /** + * Handle reminder requests + * + * @param string $recipient recipient of the message + * @param string $message message to tell the recipient + * + * @return void + * @see handleRemind() + */ + public function onCommandTell($recipient, $message) + { + $this->handleRemind($recipient, $message); + } + + /** + * Handle reminder requests + * + * @param string $recipient recipient of the message + * @param string $message message to tell the recipient + * + * @return void + * @see handleRemind() + */ + public function onCommandAsk($recipient, $message) + { + $this->handleRemind($recipient, $message); + } + + /** + * Handle reminder requests + * + * @param string $recipient recipient of the message + * @param string $message message to tell the recipient + * + * @return void + * @see handleRemind() + */ + public function onCommandRemind($recipient, $message) + { + $this->handleRemind($recipient, $message); + } + + /** + * Handles the tell/remind command (stores the message) + * + * @param string $recipient name of the recipient + * @param string $message message to store + * + * @return void + */ + protected function handleRemind($recipient, $message) + { + $source = $this->getEvent()->getSource(); + $nick = $this->getEvent()->getNick(); + + if (!$this->getEvent()->isInChannel()) { + $this->doPrivmsg($source, 'Reminders must be requested in-channel.'); + return; + } + + $q = $this->db->prepare( + 'INSERT INTO remind + ( + time, + channel, + recipient, + sender, + message + ) + VALUES + ( + :time, + :channel, + :recipient, + :sender, + :message + )' + ); + try { + $q->execute( + array( + 'time' => date(DATE_RFC822), + 'channel' => $source, + 'recipient' => strtolower($recipient), + 'sender' => strtolower($nick), + 'message' => $message + ) + ); + } catch (PDOException $e) { + } + + if ($rowid = $this->db->lastInsertId()) { + $this->doPrivmsg($source, 'ok, ' . $nick . ', message stored'); + } else { + $this->doPrivmsg( + $source, + $nick . ': bad things happened. Message not saved.' + ); + return; + } + + if ($this->keepListInMemory) { + $this->msgStorage[$source][strtolower($recipient)] = $rowid; + } + } + + /** + * Determines if the user has pending reminders, and if so, delivers them. + * + * @param string $channel channel to check + * @param string $nick nick to check + * + * @return void + */ + protected function deliverReminders($channel, $nick) + { + if ($channel[0] != '#') { + // private message, not a channel, so don't check + return; + } + + // short circuit if there's no message in memory (if allowed) + if ($this->keepListInMemory + && !isset($this->msgStorage[$channel][strtolower($nick)]) + ) { + return; + } + + // fetch and deliver messages + $reminders = $this->fetchMessages($channel, $nick); + if (count($reminders) > $this->publicReminders) { + $msgs = array_slice($reminders, 0, $this->publicReminders); + $privmsgs = array_slice($reminders, $this->publicReminders); + } else { + $msgs = $reminders; + $privmsgs = false; + } + + foreach ($msgs as $msg) { + $ts = $this->plugins->time->getCountdown($msg['time']); + $formatted = sprintf( + '%s: (from %s, %s ago) %s', + $nick, $msg['sender'], $ts, $msg['message'] + ); + $this->doPrivmsg($channel, $formatted); + $this->deleteMessage($msg['rowid'], $channel, $nick); + } + + if ($privmsgs) { + foreach ($privmsgs as $msg) { + $ts = $this->plugins->time->getCountdown($msg['time']); + $formatted = sprintf( + 'from %s, %s ago: %s', + $msg['sender'], $ts, $msg['message'] + ); + $this->doPrivmsg($nick, $formatted); + $this->deleteMessage($msg['rowid'], $channel, $nick); + } + $formatted = sprintf( + '%s: (%d more messages sent in private.)', + $nick, count($privmsgs) + ); + $this->doPrivmsg($channel, $formatted); + } + } + + /** + * Get pending messages (for a specific channel/recipient) + * + * @param string $channel channel on which to check for pending messages + * @param string $recipient user for which to check pending messages + * + * @return array of records + */ + protected function fetchMessages($channel = null, $recipient = null) + { + if ($channel) { + $qClause = 'WHERE channel = :channel AND recipient LIKE :recipient'; + $params = compact('channel', 'recipient'); + } else { + $qClause = ''; + $params = array(); + } + $q = $this->db->prepare( + 'SELECT rowid, channel, sender, recipient, time, message + FROM remind ' . $qClause + ); + $q->execute($params); + return $q->fetchAll(); + } + + /** + * Deletes a delivered message + * + * @param int $rowid ID of the message to delete + * @param string $channel message's channel + * @param string $nick message's recipient + * + * @return void + */ + protected function deleteMessage($rowid, $channel, $nick) + { + $nick = strtolower($nick); + $q = $this->db->prepare('DELETE FROM remind WHERE rowid = :rowid'); + $q->execute(array('rowid' => $rowid)); + + if ($this->keepListInMemory) { + if (isset($this->msgStorage[$channel][$nick]) + && $this->msgStorage[$channel][$nick] == $rowid + ) { + unset($this->msgStorage[$channel][$nick]); + } + } + } + + /** + * Determines if a table exists + * + * @param string $name Table name + * + * @return bool + */ + protected function haveTable($name) + { + $sql = 'SELECT COUNT(*) FROM sqlite_master WHERE name = ' + . $this->db->quote($name); + return (bool) $this->db->query($sql)->fetchColumn(); + } + + /** + * Creates the database table(s) (if they don't exist) + * + * @return void + */ + protected function createTables() + { + if (!$this->haveTable('remind')) { + $this->db->exec( + 'CREATE TABLE + remind + ( + time INTEGER, + channel TEXT, + recipient TEXT, + sender TEXT, + message TEXT + )' + ); + } + } + + /** + * Populates the in-memory cache of pending reminders + * + * @return void + */ + protected function populateMemory() + { + if (!$this->keepListInMemory) { + return; + } + foreach ($this->fetchMessages() as $msg) { + $this->msgStorage[$msg['channel']][$msg['recipient']] = $msg['rowid']; + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php new file mode 100755 index 000000000..cdb8f7f91 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Serve.php @@ -0,0 +1,160 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Serve + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Serve + */ + +/** + * Processes requests to serve a user something from a database. + * + * @category Phergie + * @package Phergie_Plugin_Serve + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Serve + * @uses extension pdo + * @uses extension pdo_sqlite + */ +class Phergie_Plugin_Serve extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->fail('PDO and pdo_sqlite extensions must be installed'); + } + } + + /** + * Retrieves a random item from the database table. + * + * @param string $database Path to the SQLite database file + * @param string $table Name of the database table + * @param array $request Parsed request + * + * @return object Retrieved item + */ + protected function getItem($database, $table, array $request) + { + $db = new PDO('sqlite:' . $database); + if (!empty($request['suggestion'])) { + $query = 'SELECT * FROM ' . $table . ' WHERE name LIKE ? ORDER BY RANDOM() LIMIT 1'; + $stmt = $db->prepare($query); + $stmt->execute(array('%' . $request['suggestion'] . '%')); + $item = $stmt->fetchObject(); + if (!$item) { + $item = new stdClass; + $item->name = $request['suggestion']; + $item->link = null; + } + } else { + $query = 'SELECT * FROM ' . $table . ' ORDER BY RANDOM() LIMIT 1'; + $stmt = $db->query($query); + $item = $stmt->fetchObject(); + } + return $item; + } + + /** + * Processes a request to serve a user something. + * + * @param string $database Path to the SQLite database file + * @param string $table Name of the database table + * @param string $format Format of the response where %target%, + * %item%, %article%', and %link will be replaced with their + * respective data + * @param string $request Request string including the target and an + * optional suggestion of the item to fetch + * @param boolean $censor TRUE to integrate with the Censor plugin, + * defaults to FALSE + * + * @return boolean TRUE if the request was processed successfully, FALSE + * otherwise + */ + public function serve($database, $table, $format, $request, $censor = false) + { + // Parse the request + $result = preg_match( + '/(?P<target>[^\s]+)(\s+an?\s+)?(?P<suggestion>.*)?/', + $request, + $match + ); + + if (!$result) { + return false; + } + + // Resolve the target + $target = $match['target']; + if ($target == 'me') { + $target = $this->event->getNick(); + } + + // Process the request + $item = $this->getItem($database, $table, $match); + + // Reprocess the request for censorship if required + if ($this->plugins->hasPlugin('Censor')) { + $plugin = $this->plugins->getPlugin('Censor'); + $attempts = 0; + while ($censor && $attempts < 3) { + $clean = $plugin->cleanString($item->name); + if ($item->name != $clean) { + $attempts++; + $item = $this->getItem($database, $table, $match); + } else { + $censor = false; + } + } + if ($censor && $attempts == 3) { + $this->doAction($this->event->getSource(), 'shrugs.'); + } + } + + // Derive the proper article for the item + if (preg_match('/^[aeiou]/i', $item->name)) { + $article = 'an'; + } else { + $article = 'a'; + } + + // Format the message + $replacements = array( + 'target' => $target, + 'item' => $item->name, + 'link' => $item->link, + 'article' => $article + ); + + $msg = $format; + foreach ($replacements as $placeholder => $value) { + $msg = str_replace( + '%' . $placeholder . '%', + $value, + $msg + ); + } + + // Send the message + $this->doAction($this->event->getSource(), $msg); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php new file mode 100644 index 000000000..b731cffc8 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php @@ -0,0 +1,118 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_TerryChay + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TerryChay + */ + +/** + * Handles requests for checking spelling of specified words and returning + * either confirmation of correctly spelled words or potential correct + * spellings for misspelled words. + * + * @category Phergie + * @package Phergie_Plugin_SpellCheck + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TerryChay + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses extension pspell + */ +class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract +{ + /** + * Spell check dictionary handler + * + * @var resource + */ + protected $pspell; + + /** + * Limit on the number of potential correct spellings returned + * + * @var int + */ + protected $limit; + + /** + * Check for dependencies. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('pspell')) { + $this->fail('pspell php extension is required'); + } + + if (!$this->getConfig('spellcheck.lang')) { + $this->fail('Setting spellcheck.lang must be filled-in'); + } + + $this->plugins->getPlugin('Command'); + + set_error_handler(array($this, 'loadDictionaryError')); + $this->pspell = pspell_new($this->getConfig('spellcheck.lang')); + restore_error_handler(); + + $this->limit = $this->getConfig('spellcheck.limit', 5); + } + + /** + * Intercepts and handles requests for spell checks. + * + * @param string $word the string to perform checks against + * + * @return void + */ + public function onCommandSpell($word) + { + $source = $this->event->getSource(); + $target = $this->event->getNick(); + + $message = $target . ': The word "' . $word; + $message .= '" seems to be spelled correctly.'; + if (!pspell_check($this->pspell, $word)) { + $suggestions = pspell_suggest($this->pspell, $word); + + $message = $target; + $message .= ': I could not find any suggestions for "' . $word . '".'; + if (!empty($suggestions)) { + $suggestions = array_splice($suggestions, 0, $this->limit); + $message = $target . ': Suggestions for "'; + $message .= $word . '": ' . implode(', ', $suggestions) . '.'; + } + } + + $this->doPrivmsg($source, $message); + } + + /** + * Handle any errors from loading dictionary + * + * @param integer $errno Error code + * @param string $errstr Error message + * @param string $errfile File that errored + * @param integer $errline Line where the error happened + * + * @return void + */ + protected function loadDictionaryError($errno, $errstr, $errfile, $errline) + { + $this->fail($errstr); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php new file mode 100644 index 000000000..dc2680a6d --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php @@ -0,0 +1,143 @@ +<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ *
+ * 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/>.
+ *
+ * Talks to the Statusnet IM architecture to enqueue incoming message messages
+ * and notify result of nickname registration checks
+ *
+ * @category Phergie
+ * @package Phergie_Plugin_Statusnet
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class Phergie_Plugin_Statusnet extends Phergie_Plugin_Abstract {
+ /**
+ * Message callback details
+ *
+ * @var array
+ */
+ protected $messageCallback;
+
+ /**
+ * Registration check callback details
+ *
+ * @var array
+ */
+ protected $regCallback;
+
+ /**
+ * Connection established callback details
+ *
+ * @var array
+ */
+ protected $connectedCallback;
+
+ /**
+ * Load callback from config
+ */
+ public function onLoad() {
+ $messageCallback = $this->config['statusnet.messagecallback'];
+ if (is_callable($messageCallback)) {
+ $this->messageCallback = $messageCallback;
+ } else {
+ $this->messageCallback = NULL;
+ }
+
+ $regCallback = $this->config['statusnet.regcallback'];
+ if (is_callable($regCallback)) {
+ $this->regCallback = $regCallback;
+ } else {
+ $this->regCallback = NULL;
+ }
+
+ $connectedCallback = $this->config['statusnet.connectedcallback'];
+ if (is_callable($connectedCallback)) {
+ $this->connectedCallback = $connectedCallback;
+ } else {
+ $this->connectedCallback = NULL;
+ }
+
+ $this->unregRegexp = $this->getConfig('statusnet.unregregexp', '/\x02(.*?)\x02 (?:isn\'t|is not) registered/i');
+ $this->regRegexp = $this->getConfig('statusnet.regregexp', '/(?:\A|\x02)(\w+?)\x02? (?:\(account|is \w+?\z)/i');
+ }
+
+ /**
+ * Passes incoming messages to StatusNet
+ *
+ * @return void
+ */
+ public function onPrivmsg() {
+ if ($this->messageCallback !== NULL) {
+ $event = $this->getEvent();
+ $source = $event->getSource();
+ $sender = $event->getNick();
+ $message = trim($event->getText());
+
+ if (strpos($source, '#') === 0) {
+ $botNick = $this->getConnection()->getNick();
+ $nickPos = strpos($message, $botNick);
+ $nickLen = strlen($botNick);
+ $colonPos = strpos($message, ':', $nickLen);
+ $commandStr = trim(substr($message, $colonPos+1));
+ if ($nickPos === 0 && $colonPos == $nickLen && !empty($commandStr)) {
+ call_user_func($this->messageCallback, array('source' => $source, 'sender' => $sender, 'message' => $commandStr));
+ }
+ } else {
+ call_user_func($this->messageCallback, array('source' => $source, 'sender' => $sender, 'message' => $message));
+ }
+ }
+ }
+
+ /**
+ * Catches the response from NickServ
+ *
+ * @return void
+ */
+ public function onNotice() {
+ if ($this->regCallback !== NULL) {
+ $event = $this->getEvent();
+ if ($event->getNick() == 'NickServ') {
+ $message = $event->getArgument(1);
+ if (preg_match($this->unregRegexp, $message, $groups)) {
+ $screenname = $groups[1];
+ call_user_func($this->regCallback, array('screenname' => $screenname, 'registered' => false));
+ } elseif (preg_match($this->regRegexp, $message, $groups)) {
+ $screenname = $groups[1];
+ call_user_func($this->regCallback, array('screenname' => $screenname, 'registered' => true));
+ }
+ }
+ }
+ }
+
+ /**
+ * Intercepts the end of the "message of the day" response and tells
+ * StatusNet we're connected
+ *
+ * @return void
+ */
+ public function onResponse() {
+ switch ($this->getEvent()->getCode()) {
+ case Phergie_Event_Response::RPL_ENDOFMOTD:
+ case Phergie_Event_Response::ERR_NOMOTD:
+ if ($this->connectedCallback !== NULL) {
+ call_user_func($this->connectedCallback);
+ }
+ }
+ }
+}
diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php new file mode 100644 index 000000000..c6453566c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea.php @@ -0,0 +1,69 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Tea + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Tea + */ + +/** + * Processes requests to serve users tea. + * + * @category Phergie + * @package Phergie_Plugin_Tea + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Tea + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Tea extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user tea. + * + * @param string $request Request including the target and an optional + * suggestion of what tea to serve + * + * @return void + */ + public function onCommandTea($request) + { + $format = $this->getConfig( + 'tea.format', + 'serves %target% a cup of %item% tea.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Tea/tea.db', + 'tea', + $format, + $request + ); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php new file mode 100644 index 000000000..21fe1950d --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tea/db.php @@ -0,0 +1,49 @@ +<?php + +if (!defined('__DIR__')) { + define('__DIR__', dirname(__FILE__)); +} + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/tea.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE tea (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX tea_name ON tea (name)'); +$insert = $db->prepare('INSERT INTO tea (name, link) VALUES (:name, :link)'); + +// Get raw teacuppa.com data set +echo 'Downloading teacuppa.com data set', PHP_EOL; +$file = __DIR__ . '/tea-list.html'; +if (!file_exists($file)) { + copy('http://www.teacuppa.com/tea-list.asp', $file); +} +$contents = file_get_contents($file); + +// Extract data from data set +echo 'Processing teacuppa.com data', PHP_EOL; +$contents = tidy_repair_string($contents); +libxml_use_internal_errors(true); +$doc = new DOMDocument; +$doc->loadHTML($contents); +libxml_clear_errors(); +$xpath = new DOMXPath($doc); +$teas = $xpath->query('//p[@class="page_title"]/following-sibling::table//a'); +$db->beginTransaction(); +foreach ($teas as $tea) { + $name = preg_replace( + array('/\s*\v+\s*/', '/\s+tea\s*$/i'), + array(' ', ''), + $tea->textContent + ); + $link = 'http://teacuppa.com/' . $tea->getAttribute('href'); + $insert->execute(array($name, $link)); +} +$db->commit(); + +// Clean up +echo 'Cleaning up', PHP_EOL; +unlink($file); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php new file mode 100644 index 000000000..541fd85cf --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Temperature.php @@ -0,0 +1,81 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Temperature + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Temperature + */ + +/** + * Performs temperature calculations for other plugins. + * + * @category Phergie + * @package Phergie_Plugin_Temperature + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Temperature + */ +class Phergie_Plugin_Temperature extends Phergie_Plugin_Abstract +{ + /** + * Converts a temperature in Celsius to Fahrenheit. + * + * @param int $temp Temperature in Celsius + * + * @return int Temperature converted to Fahrenheit + */ + public function convertCelsiusToFahrenheit($temp) + { + return round(((((int) $temp * 9) / 5) + 32)); + } + + /** + * Converts a temperature in Fahrenheit to Celsius. + * + * @param int $temp Temperature in Fahrenheit + * + * @return int Temperature converted to Celsius + */ + public function convertFahrenheitToCelsius($temp) + { + return round(((((int) $temp - 32) * 5) / 9)); + } + + /** + * Calculates the heat index (i.e. "feels like" temperature) based on + * temperature and relative humidity. + * + * @param int $temperature Temperature in degrees Fahrenheit + * @param int $humidity Relative humidity (ex: 68) + * @return int Heat index in degrees Fahrenheit + */ + public function getHeatIndex($temperature, $humidity) + { + $temperature2 = $temperature * $temperature; + $humidity2 = $humidity * $humidity; + return round( + -42.379 + + (2.04901523 * $temperature) + + (10.14333127 * $humidity) - + (0.22475541 * $temperature * $humidity) - + (0.00683783 * $temperature2) - + (0.05481717 * $humidity2) + + (0.00122874 * $temperature2 * $humidity) + + (0.00085282 * $temperature * $humidity2) - + (0.00000199 * $temperature2 * $humidity2) + ); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php new file mode 100644 index 000000000..246cfc398 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php @@ -0,0 +1,94 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_TerryChay + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TerryChay + */ + +/** + * Parses incoming messages for the words "Terry Chay" or tychay and responds + * with a random Terry fact retrieved from the Chayism web service. + * + * @category Phergie + * @package Phergie_Plugin_TerryChay + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TerryChay + * @uses Phergie_Plugin_Http pear.phergie.org + */ +class Phergie_Plugin_TerryChay extends Phergie_Plugin_Abstract +{ + /** + * URL to the web service + * + * @const string + */ + const URL = 'http://phpdoc.info/chayism/'; + + /** + * HTTP plugin + * + * @var Phergie_Plugin_Http + */ + protected $http; + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $this->getPluginHandler()->getPlugin('Http'); + } + + /** + * Fetches a chayism. + * + * @return string|bool Fetched chayism or FALSE if the operation failed + */ + public function getChayism() + { + return $this + ->getPluginHandler() + ->getPlugin('Http') + ->get(self::URL) + ->getContent(); + } + + /** + * Parses incoming messages for "Terry Chay" and related variations and + * responds with a chayism. + * + * @return void + */ + public function onPrivmsg() + { + $event = $this->getEvent(); + $source = $event->getSource(); + $message = $event->getText(); + $pattern + = '{^(' . preg_quote($this->getConfig('command.prefix')) . + '\s*)?.*(terry\s+chay|tychay)}ix'; + + if (preg_match($pattern, $message)) { + if($fact = $this->getChayism()) { + $this->doPrivmsg($source, 'Fact: ' . $fact); + } + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php new file mode 100644 index 000000000..8559426b6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/TheFuckingWeather.php @@ -0,0 +1,144 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_TheFuckingWeather + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TheFuckingWeather + */ + +/** + * Detects and responds to requests for current weather conditions in a + * particular location using data from a web service. + * + * @category Phergie + * @package Phergie_Plugin_TheFuckingWeather + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_TheFuckingWeather + * @link http://thefuckingweather.com + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + */ + +class Phergie_Plugin_TheFuckingWeather extends Phergie_Plugin_Abstract +{ + /** + * HTTP plugin + * + * @var Phergie_Plugin_Http + */ + protected $http = null; + + /** + * Base API URL + * + * @var string + */ + protected $url = 'http://www.thefuckingweather.com/?zipcode='; + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $pluginHandler = $this->getPluginHandler(); + $pluginHandler->getPlugin('Command'); + $this->http = $pluginHandler->getPlugin('Http'); + } + + /** + * Returns the weather from the specified location. + * + * @param string $location Location term + * + * @return void + * @todo Implement use of URL shortening here + */ + public function onCommandThefuckingweather($location) + { + $source = $this->getEvent()->getSource(); + $target = $this->getEvent()->getNick(); + $out = $this->getWeather($location); + if (!$out) { + $this->doNotice($source, $out); + } else { + $this->doPrivmsg($source, $target . ': ' . $out); + } + } + + /** + * Alias for TheFuckingWeather command. + * + * @param string $location Location term + * + * @return void + */ + public function onCommandTfw($location) + { + $this->onCommandThefuckingweather($location); + } + + /** + * Get the necessary content and returns the search result. + * + * @param string $location Location term + * + * @return string|bool Search result or FALSE if none is found + * @todo Try to optimize pregs + */ + protected function getWeather($location) + { + $url = $this->url . urlencode($location); + $response = $this->http->get($url); + $content = $response->getContent(); + + preg_match_all( + '#<div><span class="small">(.*?)<\/span><\/div>#im', + $content, $matches + ); + $location = $matches[1][0]; + + if (!empty($location)) { + preg_match_all( + '#<div class="large" >(.*?)<br \/>#im', + $content, $matches + ); + $temp_numb = (int) $matches[1][0]; + $temp_numb .= ' F / ' . round(($temp_numb - 32) / 1.8, 0) . ' C?!'; + + preg_match_all( + '#<br \/>(.*?)<\/div><div id="remark"><br \/>#im', + $content, $matches + ); + $temp_desc = $matches[1][0]; + + preg_match_all( + '#<div id="remark"><br \/>\n<span>(.*?)<\/span><\/div>#im', + $content, $matches + ); + $remark = $matches[1][0]; + + $result = "{$location}: {$temp_numb} {$temp_desc} ({$remark})"; + $result = preg_replace('/</', ' <', $result); + $result = strip_tags($result); + return html_entity_decode($result); + } else { + return 'No fucking clue where that is.'; + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php new file mode 100644 index 000000000..0d90bd8a6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Time.php @@ -0,0 +1,72 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Time + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Time + */ + +/** + * Helper plugin to assist other plugins with time manipulation, display. + * + * Any shared time-related code should go into this class. + * + * @category Phergie + * @package Phergie_Plugin_Time + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Time + */ +class Phergie_Plugin_Time extends Phergie_Plugin_Abstract +{ + /** + * Returns the time interval between the current time and a given + * timestamp. + * + * @param string $timestamp Timestamp compatible with strtotime() + * + * @return string + */ + public function getCountdown($timestamp) + { + $time = time() - strtotime($timestamp); + $return = array(); + + $days = floor($time / 86400); + if ($days > 0) { + $return[] = $days . 'd'; + $time %= 86400; + } + + $hours = floor($time / 3600); + if ($hours > 0) { + $return[] = $hours . 'h'; + $time %= 3600; + } + + $minutes = floor($time / 60); + if ($minutes > 0) { + $return[] = $minutes . 'm'; + $time %= 60; + } + + if ($time > 0 || count($return) <= 0) { + $return[] = ($time > 0 ? $time : '0') . 's'; + } + + return implode(' ', $return); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php new file mode 100644 index 000000000..d7d64a471 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld.php @@ -0,0 +1,148 @@ +<?php + +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Url + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Url + */ + +/** + * Responds to a request for a TLD (formatted as .tld where tld is the TLD to + * be looked up) with its corresponding description. + * + * @category Phergie + * @package Phergie_Plugin_Tld + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Tld + * @uses extension PDO + * @uses extension pdo_sqlite + */ +class Phergie_Plugin_Tld extends Phergie_Plugin_Abstract +{ + /** + * Connection to the database + * + * @var PDO + */ + protected $db; + + /** + * Prepared statement for selecting a single TLD + * + * @var PDOStatement + */ + protected $select; + + /** + * Prepared statement for selecting all TLDs + * + * @var PDOStatement + */ + protected $selectAll; + + /** + * Checks for dependencies and sets up the database and hard-coded values. + * + * @return void + */ + public function onLoad() + { + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->fail('PDO and pdo_sqlite extensions must be installed'); + } + + $dbFile = dirname(__FILE__) . '/Tld/tld.db'; + try { + $this->db = new PDO('sqlite:' . $dbFile); + + $this->select = $this->db->prepare(' + SELECT type, description + FROM tld + WHERE LOWER(tld) = LOWER(:tld) + '); + + $this->selectAll = $this->db->prepare(' + SELECT tld, type, description + FROM btld + '); + } catch (PDOException $e) { + $this->getPluginHandler()->removePlugin($this); + } + } + + /** + * takes a tld in the format '.tld' and returns its related data + * + * @param string $tld tld to process + * + * @return null + */ + public function onCommandTld($tld) + { + $tld = ltrim($tld, '.'); + $description = $this->getTld($tld); + $this->doPrivmsg( + $this->event->getSource(), + "{$this->getEvent()->getNick()}: .{$tld} -> " + . ($description ? $description : 'Unknown TLD') + ); + } + + /** + * Retrieves the definition for a given TLD if it exists + * + * @param string $tld TLD to search for + * + * @return mixed Definition of the given TLD as a string or false if unknown + */ + public function getTld($tld) + { + $tld = trim(strtolower($tld)); + if ($this->select->execute(array('tld' => $tld))) { + $tlds = $this->select->fetch(); + if (is_array($tlds)) { + return '(' . $tlds['type'] . ') ' . $tlds['description']; + } + } + return false; + } + + /** + * Retrieves a list of all the TLDs and their definitions + * + * @return mixed Array of all the TLDs and their definitions or FALSE on + * failure + */ + public function getTlds() + { + if ($this->selectAll->execute()) { + $tlds = $this->selectAll->fetchAll(); + if (is_array($tlds)) { + $tldinfo = array(); + foreach ($tlds as $key => $tld) { + if (!empty($tld['tld'])) { + $tldinfo[$tld['tld']] = "({$tld['type']}) " + . $tld['description']; + } + } + return $tldinfo; + } + } + return false; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php new file mode 100644 index 000000000..28f963ad1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Tld/db.php @@ -0,0 +1,68 @@ +<?php + +$dbFile = 'tld.db'; + +if (file_exists($dbFile)) { + exit; +} + +$db = new PDO('sqlite:' . dirname(__FILE__) . '/' . $dbFile); + +$query = ' + CREATE TABLE tld ( + tld VARCHAR(20), + type VARCHAR(20), + description VARCHAR(255) + ) +'; +$db->exec($query); + +$insert = $db->prepare(' + INSERT INTO tld (tld, type, description) + VALUES (:tld, :type, :description) +'); + +$contents = file_get_contents( + 'http://www.iana.org/domains/root/db/' +); + +libxml_use_internal_errors(true); +$doc = new DOMDocument; +$doc->loadHTML($contents); +libxml_clear_errors(); + +$descriptions = array( + 'com' => 'Commercial', + 'info' => 'Information', + 'net' => 'Network', + 'org' => 'Organization', + 'edu' => 'Educational', + 'name' => 'Individuals, by name' +); + +$xpath = new DOMXPath($doc); +$rows = $xpath->query('//tr[contains(@class, "iana-group")]'); +foreach (range(0, $rows->length - 1) as $index) { + $row = $rows->item($index); + $tld = strtolower(ltrim($row->childNodes->item(0)->textContent, '.')); + $type = $row->childNodes->item(1)->nodeValue; + if (isset($descriptions[$tld])) { + $description = $descriptions[$tld]; + } else { + $description = $row->childNodes->item(2)->textContent; + $regex = '{(^(?:Reserved|Restricted)\s*(?:exclusively\s*)?' + . '(?:for|to)\s*(?:members of\s*)?(?:the|support)?' + . '\s*|\s*as advised.*$)}i'; + $description = preg_replace($regex, '', $description); + $description = ucfirst(trim($description)); + } + $data = array_map( + 'html_entity_decode', + array( + 'tld' => $tld, + 'type' => $type, + 'description' => $description + ) + ); + $insert->execute($data); +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php new file mode 100644 index 000000000..4a77d1e41 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter.php @@ -0,0 +1,206 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Twitter + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Twitter + */ + +/** + * These requires are for library code, so they don't fit Autoload's normal + * conventions. + * + * @link http://github.com/scoates/simpletweet + */ +require dirname(__FILE__) . '/Twitter/twitter.class.php'; +require dirname(__FILE__) . '/Twitter/laconica.class.php'; + +/** + * Twitter plugin; Allows tweet (if configured) and twitter commands + * + * Usage: + * tweet text to tweet + * (sends a message to twitter and Phergie will give you the link) + * twitter username + * (fetches and displays the last tweet by @username) + * twitter username 3 + * (fetches and displays the third last tweet by @username) + * twitter 1234567 + * (fetches and displays tweet number 1234567) + * http://twitter.com/username/statuses/1234567 + * (same as `twitter 1234567`) + * + * @category Phergie + * @package Phergie_Plugin_Twitter + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Twitter + * @uses Phergie_Plugin_Time pear.phergie.org + */ +class Phergie_Plugin_Twitter extends Phergie_Plugin_Abstract +{ + /** + * Twitter object (from Simpletweet) + */ + protected $twitter; + + /** + * Twitter user + */ + protected $twitteruser = null; + + /** + * Password + */ + protected $twitterpassword = null; + + /** + * Register with the URL plugin, if possible + * + * @return void + */ + public function onConnect() + { + if ($url = $this->getPluginHandler()->getPlugin('Url')) { + $url->registerRenderer($this); + } + } + + /** + * Initialize (set up configuration vars) + * + * @return void + */ + public function onLoad() + { + if (!isset($this->config['twitter.class']) + || !$twitterClass = $this->config['twitter.class'] + ) { + $twitterClass = 'Twitter'; + } + + $this->twitteruser = $this->config['twitter.user']; + $this->twitterpassword = $this->config['twitter.password']; + $url = $this->config['twitter.url']; + + $this->twitter = new $twitterClass( + $this->twitteruser, + $this->twitterpassword, + $url + ); + + } + + /** + * Fetches the associated tweet and relays it to the channel + * + * @param string $tweeter if numeric the tweet number/id, otherwise the + * twitter user name (optionally prefixed with @) + * @param int $num optional tweet number for this user (number of + * tweets ago) + * + * @return void + */ + public function onCommandTwitter($tweeter = null, $num = 1) + { + $source = $this->getEvent()->getSource(); + if (is_numeric($tweeter)) { + $tweet = $this->twitter->getTweetByNum($tweeter); + } else if (is_null($tweeter) && $this->twitteruser) { + $tweet = $this->twitter->getLastTweet($this->twitteruser, 1); + } else { + $tweet = $this->twitter->getLastTweet(ltrim($tweeter, '@'), $num); + } + if ($tweet) { + $this->doPrivmsg($source, $this->formatTweet($tweet)); + } + } + + /** + * Sends a tweet to Twitter as the configured user + * + * @param string $txt the text to tweet + * + * @return void + */ + public function onCommandTweet($txt) + { + $nick = $this->getEvent()->getNick(); + if (!$this->twitteruser) { + return; + } + $source = $this->getEvent()->getSource(); + if ($tweet = $this->twitter->sendTweet($txt)) { + $this->doPrivmsg( + $source, 'Tweeted: ' + . $this->twitter->getUrlOutputStatus($tweet) + ); + } else { + $this->doNotice($nick, 'Tweet failed'); + } + } + + /** + * Formats a Tweet into a message suitable for output + * + * @param object $tweet JSON-decoded tweet object from Twitter + * @param bool $includeUrl whether or not to include the URL in the + * formatted output + * + * @return string + */ + protected function formatTweet(StdClass $tweet, $includeUrl = true) + { + $ts = $this->plugins->time->getCountDown($tweet->created_at); + $out = '<@' . $tweet->user->screen_name .'> '. $tweet->text + . ' - ' . $ts . ' ago'; + if ($includeUrl) { + $out .= ' (' . $this->twitter->getUrlOutputStatus($tweet) . ')'; + } + return $out; + } + + /** + * Renders a URL + * + * @param array $parsed parse_url() output for the URL to render + * + * @return bool + */ + public function renderUrl(array $parsed) + { + if ($parsed['host'] != 'twitter.com' + && $parsed['host'] != 'www.twitter.com' + ) { + // unable to render non-twitter URLs + return false; + } + + $source = $this->getEvent()->getSource(); + + if (preg_match('#^/(.*?)/status(es)?/([0-9]+)$#', $parsed['path'], $matches) + ) { + $tweet = $this->twitter->getTweetByNum($matches[3]); + if ($tweet) { + $this->doPrivmsg($source, $this->formatTweet($tweet, false)); + } + return true; + } + + // if we get this far, we haven't satisfied the URL, so bail: + return false; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php new file mode 100644 index 000000000..e411991db --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/laconica.class.php @@ -0,0 +1,41 @@ +<?php +/** + * Sean's Simple Twitter Library - Laconica extension + * + * Copyright 2008, Sean Coates + * Usage of the works is permitted provided that this instrument is retained + * with the works, so that any entity that uses the works is notified of this + * instrument. + * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. + * ( Fair License - http://www.opensource.org/licenses/fair.php ) + * Short license: do whatever you like with this. + * + */ +class Twitter_Laconica extends Twitter { + + /** + * Constructor; sets up configuration. + * + * @param string $user Laconica user name; null for limited read-only access + * @param string $pass Laconica password; null for limited read-only access + * @param string $baseUrl Base URL of Laconica install. Defaults to identi.ca + */ + public function __construct($user=null, $pass=null, $baseUrl = 'http://identi.ca/') { + $this->baseUrl = $baseUrl; + parent::__construct($user, $pass); + } + + /** + * Returns the base API URL + */ + protected function getUrlApi() { + return $this->baseUrlFull . 'api/'; + } + + /** + * Output URL: status + */ + public function getUrlOutputStatus(StdClass $tweet) { + return $this->baseUrl . 'notice/' . urlencode($tweet->id); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php new file mode 100644 index 000000000..31173a6d0 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Twitter/twitter.class.php @@ -0,0 +1,287 @@ +<?php +/** + * Sean's Simple Twitter Library + * + * Probably a little more or a little less than you need. + * + * Copyright 2008, Sean Coates + * Usage of the works is permitted provided that this instrument is retained + * with the works, so that any entity that uses the works is notified of this + * instrument. + * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. + * ( Fair License - http://www.opensource.org/licenses/fair.php ) + * Short license: do whatever you like with this. + * + * komode: le=unix language=php codepage=utf8 tab=4 notabs indent=4 + */ +class Twitter { + + /** + * Base URL for Twitter API + * + * Do not specify user/password in URL + */ + protected $baseUrl = 'http://twitter.com/'; + + /** + * Full base URL (includes user/pass) + * + * (created in Init) + */ + protected $baseUrlFull = null; + + /** + * Twitter API user + */ + protected $user; + + /** + * Twitter API password + */ + protected $pass; + + /** + * Constructor; sets up configuration. + * + * @param string $user Twitter user name; null for limited read-only access + * @param string $pass Twitter password; null for limited read-only access + */ + public function __construct($user=null, $pass=null) { + $this->baseUrlFull = $this->baseUrl; + if (null !== $user) { + // user is defined, so use it in the URL + $this->user = $user; + $this->pass = $pass; + $parsed = parse_url($this->baseUrl); + $this->baseUrlFull = $parsed['scheme'] . '://' . $this->user . ':' . + $this->pass . '@' . $parsed['host']; + // port (optional) + if (isset($parsed['port']) && is_numeric($parsed['port'])) { + $this->baseUrlFull .= ':' . $parsed['port']; + } + // append path (default: /) + if (isset($parsed['path'])) { + $this->baseUrlFull .= $parsed['path']; + } else { + $this->baseUrlFull .= '/'; + } + } + } + + /** + * Fetches a tweet by its number/id + * + * @param int $num the tweet id/number + * @return string (null on failure) + */ + public function getTweetByNum($num) { + if (!is_numeric($num)) { + return; + } + $tweet = json_decode(file_get_contents($this->getUrlStatus($num))); + return $tweet; + } + + /** + * Reads [last] tweet from user + * + * @param string $tweeter the tweeter username + * @param int $num this many tweets ago (1 = current tweet) + * @return string (false on failure) + */ + public function getLastTweet($tweeter, $num = 1) + { + $source = json_decode(file_get_contents($this->getUrlUserTimeline($tweeter))); + if ($num > count($source)) { + return false; + } + $tweet = $source[$num - 1]; + if (!isset($tweet->user->screen_name) || !$tweet->user->screen_name) { + return false; + } + return $tweet; + } + + /** + * fetches mentions for a user + */ + public function getMentions($sinceId=null, $count=20) { + return json_decode(file_get_contents($this->getUrlMentions($sinceId, $count))); + } + + /** + * Fetches followers for a user + */ + public function getFollowers($cursor=-1) { + return json_decode(file_get_contents($this->getUrlFollowers($cursor))); + } + + /** + * Follow a userid + */ + public function follow($userId) { + $params = array( + 'http' => array( + 'method' => 'POST', + 'content' => array(), + 'header' => 'Content-type: application/x-www-form-urlencoded', + ) + ); + $ctx = stream_context_create($params); + $fp = fopen($this->getUrlFollow($userId), 'rb', false, $ctx); + if (!$fp) { + return false; + } + $response = stream_get_contents($fp); + if ($response === false) { + return false; + } + $response = json_decode($response); + return $response; + } + + /** + * fetches DMs for a user + */ + public function getDMs($sinceId=null, $count=20, $page=1) { + return json_decode(file_get_contents($this->getUrlDMs($sinceId, $count, $page))); + } + + /** + * Send DM + */ + public function sendDM($screenName, $text) { + $data = http_build_query(array('screen_name'=>$screenName, 'text'=>$text)); + $params = array( + 'http' => array( + 'method' => 'POST', + 'content' => $data, + 'header' => 'Content-type: application/x-www-form-urlencoded', + ) + ); + $ctx = stream_context_create($params); + $fp = fopen($this->getUrlSendDM(), 'rb', false, $ctx); + if (!$fp) { + return false; + } + $response = stream_get_contents($fp); + if ($response === false) { + return false; + } + $response = json_decode($response); + return $response; + } + + /** + * Sends a tweet + * + * @param string $txt the tweet text to send + * @return string URL of tweet (or false on failure) + */ + public function sendTweet($txt, $limit=true) { + if ($limit) { + $txt = substr($txt, 0, 140); // twitter message size limit + } + $data = 'status=' . urlencode($txt); + $params = array( + 'http' => array( + 'method' => 'POST', + 'content' => $data, + 'header' => 'Content-type: application/x-www-form-urlencoded', + ) + ); + $ctx = stream_context_create($params); + $fp = fopen($this->getUrlTweetPost(), 'rb', false, $ctx); + if (!$fp) { + return false; + } + $response = stream_get_contents($fp); + if ($response === false) { + return false; + } + $response = json_decode($response); + return $response; + } + + /** + * Returns the base API URL + */ + protected function getUrlApi() { + return $this->baseUrlFull; + } + + /** + * Returns the status URL + * + * @param int $num the tweet number + */ + protected function getUrlStatus($num) { + return $this->getUrlApi() . 'statuses/show/'. urlencode($num) .'.json'; + } + + /** + * Returns the user timeline URL + */ + protected function getUrlUserTimeline($user) { + return $this->getUrlApi() . 'statuses/user_timeline/'. urlencode($user) .'.json'; + } + + /** + * Returns the tweet posting URL + */ + protected function getUrlTweetPost() { + return $this->getUrlApi() . 'statuses/update.json'; + } + + /** + * Output URL: status + */ + public function getUrlOutputStatus(StdClass $tweet) { + return $this->baseUrl . urlencode($tweet->user->screen_name) . '/statuses/' . urlencode($tweet->id); + } + + /** + * Return mentions URL + */ + public function getUrlMentions($sinceId=null, $count=20) { + $url = $this->baseUrlFull . 'statuses/mentions.json?count=' . urlencode($count); + if ($sinceId !== null) { + $url .= '&since_id=' . urlencode($sinceId); + } + return $url; + } + + /** + * Returns the followers URL + */ + public function getUrlFollowers($cursor=-1) { + return $this->baseUrlFull . 'statuses/followers.json?cursor=' . ((int)$cursor); + } + + /** + * Returns the follow-user URL + */ + public function getUrlFollow($userid) { + return $this->baseUrlFull . 'friendships/create/' . ((int) $userid) . '.json'; + } + + /** + * Returns the get DMs URL + */ + public function getUrlDMs($sinceId=null, $count=20, $page=1) { + $url = $this->baseUrlFull . 'direct_messages.json?'; + if ($sinceId !== null) { + $url .= 'since_id=' . urlencode($sinceId); + } + $url .= "&page={$page}"; + $url .= "&count={$count}"; + return $url; + } + + /** + * Returns the send DM URL + */ + public function getURLSendDM() { + return $this->baseUrlFull . 'direct_messages/new.json'; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php new file mode 100644 index 000000000..bac115b1b --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url.php @@ -0,0 +1,638 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Url + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Url + */ + +/** + * Monitors incoming messages for instances of URLs and responds with messages + * containing relevant information about detected URLs. + * + * Has an utility method accessible via + * $this->getPlugin('Url')->getTitle('http://foo..'). + * + * @category Phergie + * @package Phergie_Plugin_Url + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Url + * @uses Phergie_Plugin_Encoding pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + * @uses Phergie_Plugin_Tld pear.phergie.org + */ +class Phergie_Plugin_Url extends Phergie_Plugin_Abstract +{ + /** + * Links output format + * + * Can use the variables %nick%, %title% and %link% in it to display + * page titles and links + * + * @var string + */ + protected $baseFormat = '%message%'; + protected $messageFormat = '[ %link% ] %title%'; + + /** + * Flag indicating whether a single response should be sent for a single + * message containing multiple links + * + * @var bool + */ + protected $mergeLinks = true; + + /** + * Max length of the fetched URL title + * + * @var int + */ + protected $titleLength = 40; + + /** + * Url cache to prevent spamming, especially with multiple bots on the + * same channel + * + * @var array + */ + protected $urlCache = array(); + protected $shortCache = array(); + + /** + * Time in seconds to store the cached entries + * + * Setting it to 0 or below disables the cache expiration + * + * @var int + */ + protected $expire = 1800; + + /** + * Number of entries to keep in the cache at one time per channel + * + * Setting it to 0 or below disables the cache limit + * + * @var int + */ + protected $limit = 10; + + /** + * Flag that determines if the plugin will fall back to using an HTTP + * stream when a URL using SSL is detected and OpenSSL support isn't + * available in the PHP installation in use + * + * @var bool + */ + protected $sslFallback = true; + + /** + * Flag that is set to true by the custom error handler if an HTTP error + * code has been received + * + * @var boolean + */ + protected $errorStatus = false; + protected $errorMessage = null; + + /** + * Flag indicating whether or not to display error messages as the title + * if a link posted encounters an error + * + * @var boolean + */ + protected $showErrors = true; + + /** + * Flag indicating whether to detect schemeless URLS (i.e. "example.com") + * + * @var boolean + */ + protected $detectSchemeless = false; + + /** + * Shortener object + */ + protected $shortener; + + /** + * Array of renderers + */ + protected $renderers = array(); + + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Encoding'); + $plugins->getPlugin('Http'); + $plugins->getPlugin('Tld'); + + // make the shortener configurable + $shortener = $this->getConfig('url.shortener', 'Trim'); + $shortener = "Phergie_Plugin_Url_Shorten_{$shortener}"; + $this->shortener = new $shortener($this->plugins->getPlugin('Http')); + + if (!$this->shortener instanceof Phergie_Plugin_Url_Shorten_Abstract) { + $this->fail("Declared shortener class {$shortener} is not of proper ancestry"); + } + + // load config (a bit ugly, but focusing on porting): + foreach ( + array( + 'detect_schemeless' => 'detectSchemeless', + 'base_format' => 'baseFormat', + 'message_format' => 'messageFormat', + 'merge_links' => 'mergeLinks', + 'title_length' => 'titleLength', + 'show_errors' => 'showErrors', + 'expire' => 'expire', + ) as $config => $local) { + if (isset($this->config["url.{$config}"])) { + $this->$local = $this->config["uri.{$config}"]; + } + } + } + + /** + * Checks an incoming message for the presence of a URL and, if one is + * found, responds with its title if it is an HTML document and the + * shortened equivalent of its original URL if it meets length requirements. + * + * @todo Update this to pull configuration settings from $this->config + * rather than caching them as class properties + * @return void + */ + public function onPrivmsg() + { + $this->handleMsg(); + } + + /** + * Checks an incoming message for the presence of a URL and, if one is + * found, responds with its title if it is an HTML document and the + * shortened equivalent of its original URL if it meets length requirements. + * + * @todo Update this to pull configuration settings from $this->config + * rather than caching them as class properties + * @return void + */ + public function onAction() + { + $this->handleMsg(); + } + + /** + * Handles message events and responds with url titles. + * + * @return void + */ + protected function handleMsg() + { + $source = $this->getEvent()->getSource(); + $user = $this->getEvent()->getNick(); + + $responses = array(); + $urls = $this->findUrls($this->getEvent()->getArgument(1)); + + foreach ($urls as $parsed) { + $url = $parsed['glued']; + + // allow out-of-class renderers to handle this URL + foreach ($this->renderers as $renderer) { + if ($renderer->renderUrl($parsed) === true) { + // renderers should return true if they've fully + // rendered the passed URL (they're responsible + // for their own output) + $this->debug('Handled by renderer: ' . get_class($renderer)); + continue 2; + } + } + + // Convert url + $shortenedUrl = $this->shortener->shorten($url); + if (!$shortenedUrl) { + $this->debug('Invalid Url: Unable to shorten. (' . $url . ')'); + $shortenedUrl = $url; + } + + // Prevent spamfest + if ($this->checkUrlCache($url, $shortenedUrl)) { + $this->debug('Invalid Url: URL is in the cache. (' . $url . ')'); + continue; + } + + $title = $this->getTitle($url); + if (!empty($title)) { + $responses[] = str_replace( + array( + '%title%', + '%link%', + '%nick%' + ), array( + $title, + $shortenedUrl, + $user + ), $this->messageFormat + ); + } + + // Update cache + $this->updateUrlCache($url, $shortenedUrl); + unset($title, $shortenedUrl, $title); + } + + // Check to see if there were any URL responses, format them and handle if they + // get merged into one message or not + if (count($responses) > 0) { + if ($this->mergeLinks) { + $message = str_replace( + array( + '%message%', + '%nick%' + ), array( + implode('; ', $responses), + $user + ), $this->baseFormat + ); + $this->doPrivmsg($source, $message); + } else { + foreach ($responses as $response) { + $message = str_replace( + array( + '%message%', + '%nick%' + ), array( + implode('; ', $responses), + $user + ), $this->baseFormat + ); + $this->doPrivmsg($source, $message); + } + } + } + } + + /** + * Detect URLs in a given string. + * + * @param string $message the string to detect urls in + * + * @return array the array of urls found + */ + public function findUrls($message) + { + $pattern = '#'.($this->detectSchemeless ? '' : 'https?://').'(?:([0-9]{1,3}(?:\.[0-9]{1,3}){3})(?![^/]) | (' + .($this->detectSchemeless ? '(?<!http:/|https:/)[@/\\\]' : '').')?(?:(?:[a-z0-9_-]+\.?)+\.[a-z0-9]{1,6}))[^\s]*#xis'; + $urls = array(); + + // URL Match + if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $url = trim(rtrim($m[0], ', ].?!;')); + + // Check to see if the URL was from an email address, is a directory, etc + if (!empty($m[2])) { + $this->debug('Invalid Url: URL is either an email or a directory path. (' . $url . ')'); + continue; + } + + // Parse the given URL + if (!$parsed = $this->parseUrl($url)) { + $this->debug('Invalid Url: Could not parse the URL. (' . $url . ')'); + continue; + } + + // Check to see if the given IP/Host is valid + if (!empty($m[1]) and !$this->checkValidIP($m[1])) { + $this->debug('Invalid Url: ' . $m[1] . ' is not a valid IP address. (' . $url . ')'); + continue; + } + + // Process TLD if it's not an IP + if (empty($m[1])) { + // Get the TLD from the host + $pos = strrpos($parsed['host'], '.'); + $parsed['tld'] = ($pos !== false ? substr($parsed['host'], ($pos+1)) : ''); + + // Check to see if the URL has a valid TLD + if ($this->plugins->tld->getTld($parsed['tld']) === false) { + $this->debug('Invalid Url: ' . $parsed['tld'] . ' is not a supported TLD. (' . $url . ')'); + continue; + } + } + + // Check to see if the URL is to a secured site or not and handle it accordingly + if ($parsed['scheme'] == 'https' && !extension_loaded('openssl')) { + if (!$this->sslFallback) { + $this->debug('Invalid Url: HTTPS is an invalid scheme, OpenSSL isn\'t available. (' . $url . ')'); + continue; + } else { + $parsed['scheme'] = 'http'; + } + } + + if (!in_array($parsed['scheme'], array('http', 'https'))) { + $this->debug('Invalid Url: ' . $parsed['scheme'] . ' is not a supported scheme. (' . $url . ')'); + continue; + } + + $urls[] = $parsed + array('glued' => $this->glueURL($parsed)); + } + } + + return $urls; + } + + /** + * Checks a given URL (+shortened) against the cache to verify if they were + * previously posted on the channel. + * + * @param string $url The URL to check against + * @param string $shortenedUrl The shortened URL to check against + * + * @return bool + */ + protected function checkUrlCache($url, $shortenedUrl) + { + $source = $this->getEvent()->getSource(); + + /** + * Transform the URL (+shortened) into a HEX CRC32 checksum to prevent potential problems + * and minimize the size of the cache for less cache bloat. + */ + $url = $this->getUrlChecksum($url); + $shortenedUrl = $this->getUrlChecksum($shortenedUrl); + + $cache = array( + 'url' => isset($this->urlCache[$source][$url]) ? $this->urlCache[$source][$url] : null, + 'shortened' => isset($this->shortCache[$source][$shortenedUrl]) ? $this->shortCache[$source][$shortenedUrl] : null + ); + + $expire = $this->expire; + $this->debug("Cache expire: {$expire}"); + /** + * If cache expiration is enabled, check to see if the given url has expired in the cache + * If expire is disabled, simply check to see if the url is listed + */ + if (($expire > 0 && (($cache['url'] + $expire) > time() || ($cache['shortened'] + $expire) > time())) + || ($expire <= 0 && (isset($cache['url']) || isset($cache['shortened']))) + ) { + unset($cache, $url, $shortenedUrl, $expire); + return true; + } + unset($cache, $url, $shortenedUrl, $expire); + return false; + } + + /** + * Updates the cache and adds the given URL (+shortened) to the cache. It + * also handles cleaning the cache of old entries as well. + * + * @param string $url The URL to add to the cache + * @param string $shortenedUrl The shortened to add to the cache + * + * @return bool + */ + protected function updateUrlCache($url, $shortenedUrl) + { + $source = $this->getEvent()->getSource(); + + /** + * Transform the URL (+shortened) into a HEX CRC32 checksum to prevent potential problems + * and minimize the size of the cache for less cache bloat. + */ + $url = $this->getUrlChecksum($url); + $shortenedUrl = $this->getUrlChecksum($shortenedUrl); + $time = time(); + + // Handle the URL cache and remove old entries that surpass the limit if enabled + $this->urlCache[$source][$url] = $time; + if ($this->limit > 0 && count($this->urlCache[$source]) > $this->limit) { + asort($this->urlCache[$source], SORT_NUMERIC); + array_shift($this->urlCache[$source]); + } + + // Handle the shortened cache and remove old entries that surpass the limit if enabled + $this->shortCache[$source][$shortenedUrl] = $time; + if ($this->limit > 0 && count($this->shortCache[$source]) > $this->limit) { + asort($this->shortCache[$source], SORT_NUMERIC); + array_shift($this->shortCache[$source]); + } + unset($url, $shortenedUrl, $time); + } + + /** + * Transliterates a UTF-8 string into corresponding ASCII characters and + * truncates and appends an ellipsis to the string if it exceeds a given + * length. + * + * @param string $str String to decode + * @param int $trim Maximum string length, optional + * + * @return string + */ + protected function decode($str, $trim = null) + { + $out = $this->plugins->encoding->transliterate($str); + if ($trim > 0) { + $out = substr($out, 0, $trim) . (strlen($out) > $trim ? '...' : ''); + } + return $out; + } + + /** + * Takes a url, parses and cleans the URL without of all the junk + * and then return the hex checksum of the url. + * + * @param string $url url to checksum + * + * @return string the hex checksum of the cleaned url + */ + protected function getUrlChecksum($url) + { + $checksum = strtolower(urldecode($this->glueUrl($url, true))); + $checksum = preg_replace('#\s#', '', $this->plugins->encoding->transliterate($checksum)); + return dechex(crc32($checksum)); + } + + /** + * Parses a given URI and procceses the output to remove redundant + * or missing values. + * + * @param string $url the url to parse + * + * @return array the url components + */ + protected function parseUrl($url) + { + if (is_array($url)) return $url; + + $url = trim(ltrim($url, ' /@\\')); + if (!preg_match('&^(?:([a-z][-+.a-z0-9]*):)&xis', $url, $matches)) { + $url = 'http://' . $url; + } + $parsed = parse_url($url); + + if (!isset($parsed['scheme'])) { + $parsed['scheme'] = 'http'; + } + $parsed['scheme'] = strtolower($parsed['scheme']); + + if (isset($parsed['path']) && !isset($parsed['host'])) { + $host = $parsed['path']; + $path = ''; + if (strpos($parsed['path'], '/') !== false) { + list($host, $path) = array_pad(explode('/', $parsed['path'], 2), 2, null); + } + $parsed['host'] = $host; + $parsed['path'] = $path; + } + + return $parsed; + } + + /** + * Parses a given URI and then glues it back together in the proper format. + * If base is set, then it chops off the scheme, user and pass and fragment + * information to return a more unique base URI. + * + * @param string $uri uri to rebuild + * @param string $base set to true to only return the base components + * + * @return string the rebuilt uri + */ + protected function glueUrl($uri, $base = false) + { + $parsed = $uri; + if (!is_array($parsed)) { + $parsed = $this->parseUrl($parsed); + } + + if (is_array($parsed)) { + $uri = ''; + if (!$base) { + $uri .= (!empty($parsed['scheme']) ? $parsed['scheme'] . ':' . + ((strtolower($parsed['scheme']) == 'mailto') ? '' : '//') : ''); + $uri .= (!empty($parsed['user']) ? $parsed['user'] . + (!empty($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : ''); + } + if ($base && !empty($parsed['host'])) { + $parsed['host'] = trim($parsed['host']); + if (substr($parsed['host'], 0, 4) == 'www.') { + $parsed['host'] = substr($parsed['host'], 4); + } + } + $uri .= (!empty($parsed['host']) ? $parsed['host'] : ''); + if (!empty($parsed['port']) + && (($parsed['scheme'] == 'http' && $parsed['port'] == 80) + || ($parsed['scheme'] == 'https' && $parsed['port'] == 443)) + ) { + unset($parsed['port']); + } + $uri .= (!empty($parsed['port']) ? ':' . $parsed['port'] : ''); + if (!empty($parsed['path']) && (!$base || $base && $parsed['path'] != '/')) { + $uri .= (substr($parsed['path'], 0, 1) == '/') ? $parsed['path'] : ('/' . $parsed['path']); + } + $uri .= (!empty($parsed['query']) ? '?' . $parsed['query'] : ''); + if (!$base) { + $uri .= (!empty($parsed['fragment']) ? '#' . $parsed['fragment'] : ''); + } + } + return $uri; + } + + /** + * Checks the given string to see if its a valid IP4 address + * + * @param string $ip the ip to validate + * + * @return bool + */ + protected function checkValidIP($ip) + { + return long2ip(ip2long($ip)) === $ip; + } + + /** + * Returns the title of the given page + * + * @param string $url url to the page + * + * @return string title + */ + public function getTitle($url) + { + $http = $this->plugins->getPlugin('Http'); + $options = array( + 'timeout' => 3.5, + 'user_agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12' + ); + + $response = $http->head($url, array(), $options); + $header = $response->getHeaders('Content-Type'); + + if (!preg_match('#^(text/x?html|application/xhtml+xml)(?:;.*)?$#', $header)) { + $title = $header; + } else { + $response = $http->get($url, array(), $options); + $content = $response->getContent(); + if (preg_match('#<title[^>]*>(.*?)</title>#is', $content, $match)) { + $title = preg_replace('/[\s\v]+/', ' ', trim($match[1])); + } + } + $encoding = $this->plugins->getPlugin('Encoding'); + $title = $encoding->decodeEntities($title); + + if (empty($title)) { + if ($response->isError()) { + $title = $response->getCodeAsString(); + } else { + $title = 'No Title'; + } + } + + return $title; + } + + /** + * Output a debug message + * + * @param string $msg the message to output + * + * @return void + */ + protected function debug($msg) + { + echo "(DEBUG:Url) $msg\n"; + } + + /** + * Add a renderer to the stack + * + * @param object $obj the renderer to add + * + * @return void + */ + public function registerRenderer($obj) + { + $this->renderers[spl_object_hash($obj)] = $obj; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php new file mode 100644 index 000000000..607d1654c --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Abstract.php @@ -0,0 +1,105 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + */ + +/** + * URL shortener abstract class + * + * @category Phergie + * @package Phergie_Plugin_Url + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Url + * @uses Phergie_Plugin_Http pear.phergie.org + */ +abstract class Phergie_Plugin_Url_Shorten_Abstract +{ + protected $http; + + /** + * Constructor + * + * @param Phergie_Plugin_Http $http instance of the http plugin + */ + public function __construct(Phergie_Plugin_Http $http) + { + $this->http = $http; + } + + /** + * Returns an array of request parameters given a url to shorten. The + * following keys are valid request parameters: + * + * * 'uri': the URI for the request (required) + * * 'query': an array of key-value pairs sent in a GET request + * * 'post': an array of key-value pairs sent in a POST request + * * 'callback': to be called after the request is finished. Should accept + * a Phergie_Plugin_Http_Response object and return either the shortened + * url or false if an error has occured. + * + * If the 'post' key is present a POST request shall be made; otherwise + * a GET request will be made. The 'post' key can be an empty array and + * a post request will still be made. + * + * If no callback is provided the contents of the response will be returned. + * + * @param string $url the url to shorten + * + * @return array the request parameters + */ + protected abstract function getRequestParams($url); + + /** + * Shortens a given url. + * + * @param string $url the url to shorten + * + * @return string the shortened url or false on a failure + */ + public function shorten($url) + { + $defaults = array('get' => array(), 'post' => array(), 'callback' => null); + $options = array('timeout' => 2); + $params = $this->getRequestParams($url) + $defaults; + + // Should some kind of notice be thrown? Maybe just if getRequestParams does not return an array? + if (!is_array($params) || empty($params['uri'])) { + return $url; + } + + if (!empty($params['post'])) { + $response = $this->http->post($params['uri'], $params['get'], $params['post'], $options); + } else { + $response = $this->http->get($params['uri'], $params['get'], $options); + } + + if (is_callable($params['callback'])) { + return call_user_func($params['callback'], $response); + } + + $code = $response->getCode(); + $content = trim($response->getContent); + if ($code < 200 || $code >= 300 || empty($content)) { + return false; + } + + return $response->getContent(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php new file mode 100644 index 000000000..af7e8a514 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Url/Shorten/Trim.php @@ -0,0 +1,64 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Php + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Php + */ + +/** + * Shortens urls via the tr.im service + * + * @category Phergie + * @package Phergie_Plugin_Url + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Url + */ +class Phergie_Plugin_Url_Shorten_Trim extends Phergie_Plugin_Url_Shorten_Abstract +{ + /** + * Returns an array of request parameters given a url to shorten. The + * following keys are valid request parameters: + * + * @param string $url the url to shorten + * + * @return array the request parameters + */ + protected function getRequestParams($url) + { + return array( + 'uri' => 'http://api.tr.im/v1/trim_simple?url=' . rawurlencode($url), + 'callback' => array($this, 'onComplete') + ); + } + + /** + * Callback for when the URL has been shortened. Checks for error messages. + * + * @param Phergie_Plugin_Http_Response $response the response object + * + * @return string|bool the shortened url or false on failure + */ + protected function onComplete($response) + { + if (strpos($response->getContent(), 'Error: ') === 0) { + return false; + } + + return $response->getContent(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php new file mode 100644 index 000000000..9437073d8 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/UserInfo.php @@ -0,0 +1,413 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_UserInfo + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_UserInfo + */ + +/** + * Provides an API for querying information on users. + * + * @category Phergie + * @package Phergie_Plugin_UserInfo + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_UserInfo + */ +class Phergie_Plugin_UserInfo extends Phergie_Plugin_Abstract +{ + const REGULAR = 1; + const VOICE = 2; + const HALFOP = 4; + const OP = 8; + const ADMIN = 16; + const OWNER = 32; + + /** + * An array containing all the user information for a given channel + * + * @var array + */ + protected $store = array(); + + /** + * Tracks mode changes + * + * @return void + */ + public function onMode() + { + $args = $this->event->getArguments(); + + if (count($args) != 3) { + return; + } + + list($chan, $modes, $nicks) = $args; + + if (!preg_match('/(?:\+|-)[hovaq+-]+/i', $modes)) { + return; + } + + $chan = trim(strtolower($chan)); + $modes = str_split(trim(strtolower($modes)), 1); + $nicks = explode(' ', trim(strtolower($nicks))); + $operation = array_shift($modes); // + or - + + while ($char = array_shift($modes)) { + $nick = array_shift($nicks); + $mode = null; + + switch ($char) { + case 'q': + $mode = self::OWNER; + break; + case 'a': + $mode = self::ADMIN; + break; + case 'o': + $mode = self::OP; + break; + case 'h': + $mode = self::HALFOP; + break; + case 'v': + $mode = self::VOICE; + break; + } + + if (!empty($mode)) { + if ($operation == '+') { + $this->store[$chan][$nick] |= $mode; + } else if ($operation == '-') { + $this->store[$chan][$nick] ^= $mode; + } + } + } + } + + /** + * Tracks users joining a channel + * + * @return void + */ + public function onJoin() + { + $chan = trim(strtolower($this->event->getArgument(0))); + $nick = trim(strtolower($this->event->getNick())); + + $this->store[$chan][$nick] = self::REGULAR; + } + + /** + * Tracks users leaving a channel + * + * @return void + */ + public function onPart() + { + $chan = trim(strtolower($this->event->getArgument(0))); + $nick = trim(strtolower($this->event->getNick())); + + if (isset($this->store[$chan][$nick])) { + unset($this->store[$chan][$nick]); + } + } + + /** + * Tracks users quitting a server + * + * @return void + */ + public function onQuit() + { + $nick = trim(strtolower($this->event->getNick())); + + foreach ($this->store as $chan => $store) { + if (isset($store[$nick])) { + unset($this->store[$chan][$nick]); + } + } + } + + /** + * Tracks users changing nicks + * + * @return void + */ + public function onNick() + { + $nick = trim(strtolower($this->event->getNick())); + $newNick = trim(strtolower($this->event->getArgument(0))); + + foreach ($this->store as $chan => $store) { + if (isset($store[$nick])) { + $this->store[$chan][$newNick] = $store[$nick]; + unset($this->store[$chan][$nick]); + } + } + } + + /** + * Populates the internal user listing for a channel when the bot joins it. + * + * @return void + */ + public function onResponse() + { + if ($this->event->getCode() != Phergie_Event_Response::RPL_NAMREPLY) { + return; + } + + $desc = preg_split('/[@*=]\s*/', $this->event->getDescription(), 2); + list($chan, $users) = array_pad(explode(' :', trim($desc[1])), 2, null); + $users = explode(' ', trim($users)); + + $chan = trim(strtolower($chan)); + + foreach ($users as $user) { + if (empty($user)) { + continue; + } + + $user = trim(strtolower($user)); + $flag = self::REGULAR; + + if ($user[0] == '~') { + $flag |= self::OWNER; + } else if ($user[0] == '&') { + $flag |= self::ADMIN; + } else if ($user[0] == '@') { + $flag |= self::OP; + } else if ($user[0] == '%') { + $flag |= self::HALFOP; + } else if ($user[0] == '+') { + $flag |= self::VOICE; + } + + if ($flag != self::REGULAR) { + $user = substr($user, 1); + } + + $this->store[$chan][$user] = $flag; + } + } + + /** + * Debugging function + * + * @return void + */ + public function onPrivmsg() + { + if ($this->getConfig('debug', false) == false) { + return; + } + + list($target, $msg) = array_pad($this->event->getArguments(), 2, null); + + if (preg_match('#^ishere (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isIn($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^isowner (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isOwner($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^isadmin (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isAdmin($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^isop (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isOp($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^ishop (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isHalfop($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^isvoice (\S+)$#', $msg, $m)) { + $this->doPrivmsg($target, $this->isVoice($m[1], $target) ? 'true' : 'false'); + } elseif (preg_match('#^channels (\S+)$#', $msg, $m)) { + $channels = $this->getChannels($m[1]); + $this->doPrivmsg($target, $channels ? join(', ', $channels) : 'unable to find nick'); + } elseif (preg_match('#^users (\S+)$#', $msg, $m)) { + $nicks = $this->getUsers($m[1]); + $this->doPrivmsg($target, $nicks ? join(', ', $nicks) : 'unable to find channel'); + } elseif (preg_match('#^random (\S+)$#', $msg, $m)) { + $nick = $this->getrandomuser($m[1]); + $this->doPrivmsg($target, $nick ? $nick : 'unable to find channel'); + } + } + + /** + * Checks whether or not a given user has a mode + * + * @param int $mode A numeric mode (identified by the class constants) + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function is($mode, $nick, $chan) + { + $chan = trim(strtolower($chan)); + $nick = trim(strtolower($nick)); + + if (!isset($this->store[$chan][$nick])) { + return false; + } + + return ($this->store[$chan][$nick] & $mode) != 0; + } + + /** + * Checks whether or not a given user has owner (~) status + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isOwner($nick, $chan) + { + return $this->is(self::OWNER, $nick, $chan); + } + + /** + * Checks whether or not a given user has admin (&) status + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isAdmin($nick, $chan) + { + return $this->is(self::ADMIN, $nick, $chan); + } + + /** + * Checks whether or not a given user has operator (@) status + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isOp($nick, $chan) + { + return $this->is(self::OP, $nick, $chan); + } + + /** + * Checks whether or not a given user has halfop (%) status + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isHalfop($nick, $chan) + { + return $this->is(self::HALFOP, $nick, $chan); + } + + /** + * Checks whether or not a given user has voice (+) status + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isVoice($nick, $chan) + { + return $this->is(self::VOICE, $nick, $chan); + } + + /** + * Checks whether or not a given user is in a channel + * + * @param string $nick The nick to check + * @param string $chan The channel to check in + * + * @return bool + */ + public function isIn($nick, $chan) + { + return $this->is(self::REGULAR, $nick, $chan); + } + + /** + * Returns the entire user list for a channel or false if the bot is not + * in the channel. + * + * @param string $chan The channel name + * + * @return array|bool + */ + public function getUsers($chan) + { + $chan = trim(strtolower($chan)); + if (isset($this->store[$chan])) { + return array_keys($this->store[$chan]); + } + return false; + } + + /** + * Returns the nick of a random user present in a given channel or false + * if the bot is not present in the channel. + * + * @param string $chan The channel name + * + * @return array|bool + */ + public function getRandomUser($chan) + { + $chan = trim(strtolower($chan)); + + if (isset($this->store[$chan])) { + $ignore = array('chanserv', 'q', 'l', 's'); + + do { + $nick = array_rand($this->store[$chan], 1); + } while (in_array($nick, $ignore)); + + return $nick; + } + + return false; + } + + /** + * Returns a list of channels in which a given user is present. + * + * @param string $nick Nick of the user (optional, defaults to the bot's + * nick) + * + * @return array|bool + */ + public function getChannels($nick = null) + { + if (empty($nick)) { + $nick = $this->connection->getNick(); + } + + $nick = trim(strtolower($nick)); + $channels = array(); + + foreach ($this->store as $chan => $store) { + if (isset($store[$nick])) { + $channels[] = $chan; + } + } + + return $channels; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php new file mode 100644 index 000000000..7d4f85a19 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Weather.php @@ -0,0 +1,149 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Weather + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Weather + */ + +/** + * Detects and responds to requests for current weather conditions in a + * particular location using data from a web service. Requires registering + * with weather.com to obtain authentication credentials, which must be + * stored in the configuration settings weather.partner_id and + * weather.license_key for the plugin to function. + * + * @category Phergie + * @package Phergie_Plugin_Weather + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Weather + * @link http://www.weather.com/services/xmloap.html + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + * @uses Phergie_Plugin_Temperature pear.phergie.org + * @uses extension SimpleXML + */ +class Phergie_Plugin_Weather extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $plugins->getPlugin('Http'); + $plugins->getPlugin('Temperature'); + + if (empty($this->config['weather.partner_id']) + || empty($this->config['weather.license_key'])) { + $this->fail('weather.partner_id and weather.license_key must be specified'); + } + } + + /** + * Returns a weather report for a specified location. + * + * @param string $location Zip code or city/state/country specification + * + * @return void + */ + public function onCommandWeather($location) + { + $response = $this->plugins->http->get( + 'http://xoap.weather.com/search/search', + array('where' => $location) + ); + + if ($response->isError()) { + $this->doNotice( + $this->event->getNick(), + 'ERROR: ' . $response->getCode() . ' ' . $response->getMessage() + ); + return; + } + + $nick = $this->event->getNick(); + + $xml = $response->getContent(); + if (count($xml->loc) == 0) { + $this->doNotice($nick, 'No results for that location.'); + return; + } + + $where = (string) $xml->loc[0]['id']; + $response = $this->plugins->http->get( + 'http://xoap.weather.com/weather/local/' . $where, + array( + 'cc' => '*', + 'link' => 'xoap', + 'prod' => 'xoap', + 'par' => $this->config['weather.partner_id'], + 'key' => $this->config['weather.license_key'], + ) + ); + + if ($response->isError()) { + $this->doNotice( + $this->event->getNick(), + 'ERROR: ' . $response->getCode() . ' ' . $response->getMessage() + ); + return; + } + + $temperature = $this->plugins->getPlugin('Temperature'); + $xml = $response->getContent(); + $weather = 'Weather for ' . (string) $xml->loc->dnam . ' - '; + switch ($xml->head->ut) { + case 'F': + $tempF = $xml->cc->tmp; + $tempC = $temperature->convertFahrenheitToCelsius($tempF); + break; + case 'C': + $tempC = $xml->cc->tmp; + $tempF = $temperature->convertCelsiusToFahrenheit($tempC); + break; + default: + $this->doNotice( + $this->event->getNick(), + 'ERROR: No scale information given.'); + break; + } + $r = $xml->cc->hmid; + $hiF = $temperature->getHeatIndex($tempF, $r); + $hiC = $temperature->convertFahrenheitToCelsius($hiF); + $weather .= 'Temperature: ' . $tempF . 'F/' . $tempC . 'C'; + $weather .= ', Humidity: ' . (string) $xml->cc->hmid . '%'; + if ($hiF > $tempF || $hiC > $tempC) { + $weather .= ', Heat Index: ' . $hiF . 'F/' . $hiC . 'C'; + } + $weather .= + ', Conditions: ' . (string) $xml->cc->t . + ', Updated: ' . (string) $xml->cc->lsup . + ' [ http://weather.com/weather/today/' . + str_replace( + array('(', ')', ',', ' '), + array('', '', '', '+'), + (string) $xml->loc->dnam + ) . + ' ]'; + + $this->doPrivmsg($this->event->getSource(), $nick . ': ' . $weather); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php new file mode 100644 index 000000000..9aa0845a6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine.php @@ -0,0 +1,69 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Wine + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Wine + */ + +/** + * Processes requests to serve users wine. + * + * @category Phergie + * @package Phergie_Plugin_Wine + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Wine + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Serve pear.phergie.org + */ +class Phergie_Plugin_Wine extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->plugins; + $plugins->getPlugin('Command'); + $plugins->getPlugin('Serve'); + } + + /** + * Processes requests to serve a user a wine. + * + * @param string $request Request including the target and an optional + * suggestion of what wine to serve + * + * @return void + */ + public function onCommandWine($request) + { + $format = $this->getConfig( + 'wine.format', + 'serves %target% a glass of %item%.' + ); + + $this->plugins->getPlugin('Serve')->serve( + dirname(__FILE__) . '/Wine/wine.db', + 'wine', + $format, + $request + ); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php new file mode 100644 index 000000000..ce01c2d98 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Wine/db.php @@ -0,0 +1,53 @@ +<?php + +// Create database schema +echo 'Creating database', PHP_EOL; +$file = __DIR__ . '/wine.db'; +if (file_exists($file)) { + unlink($file); +} +$db = new PDO('sqlite:' . $file); +$db->exec('CREATE TABLE wine (name VARCHAR(255), link VARCHAR(255))'); +$db->exec('CREATE UNIQUE INDEX wine_name ON wine (name)'); +$insert = $db->prepare('INSERT INTO wine (name, link) VALUES (:name, :link)'); + +// Get and decompress lcboapi.com data set +$outer = __DIR__ . '/current.zip'; +if (!file_exists($outer)) { + echo 'Downloading lcboapi.com data set', PHP_EOL; + copy('http://lcboapi.com/download/current.zip', $outer); +} + +echo 'Decompressing lcboapi.com data set', PHP_EOL; +$zip = new ZipArchive; +$zip->open($outer); +$stat = $zip->statIndex(0); +$inner = __DIR__ . '/' . $stat['name']; +$zip->extractTo(__DIR__); +$zip->close(); +$zip = new ZipArchive; +$zip->open($inner); +$stat = $zip->statIndex(0); +$file = __DIR__ . '/' . $stat['name']; +$zip->extractTo(__DIR__); +$zip->close(); + +// Aggregate data set into the database +$lcbo = new PDO('sqlite:' . $file); +$result = $lcbo->query('SELECT product_no, name FROM products WHERE primary_category = "Wine"'); +$wines = $result->fetchAll(); +echo 'Processing lcboapi.com data - ', number_format(count($wines), 0), ' records', PHP_EOL; +$db->beginTransaction(); +foreach ($wines as $wine) { + $name = $wine['name']; + $link = 'http://lcboapi.com/products/' . $wine['product_no']; + $insert->execute(array($name, $link)); +} +$db->commit(); + +// Clean up +echo 'Cleaning up', PHP_EOL; +unset($lcbo); +unlink($outer); +unlink($inner); +unlink($file); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php new file mode 100644 index 000000000..e06ea5640 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Youtube.php @@ -0,0 +1,168 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Plugin_Youtube + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Youtube + */ + +/** + * Provides commands used to access several services offered by Google + * including search, translation, weather, maps, and currency and general + * value unit conversion. + * + * @category Phergie + * @package Phergie_Plugin_Youtube + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Plugin_Youtube + * @uses Phergie_Plugin_Command pear.phergie.org + * @uses Phergie_Plugin_Http pear.phergie.org + */ +class Phergie_Plugin_Youtube extends Phergie_Plugin_Abstract +{ + /** + * Checks for dependencies. + * + * @return void + */ + public function onLoad() + { + $plugins = $this->getPluginHandler(); + $plugins->getPlugin('Command'); + $plugins->getPlugin('Http'); + if ($url = $plugins->getPlugin('Url')) { + $url->registerRenderer($this); + } + } + + /** + * Queries the YouTube video search web service, processes the first + * result, and sends a message back to the current event source. + * + * @param string $query Search term + * + * @return object YouTube result object + */ + protected function queryYoutube($query) + { + $url = 'http://gdata.youtube.com/feeds/api/videos'; + $params = array( + 'max-results' => '1', + 'alt' => 'json', + 'q' => $query + ); + $http = $this->plugins->getPlugin('Http'); + $response = $http->get($url, $params); + $json = $response->getContent(); + + $entries = $json->feed->entry; + if (!$entries) { + $this->doNotice($this->event->getNick(), 'Query returned no results'); + return; + } + $entry = reset($entries); + + $nick = $this->event->getNick(); + $link = $entry->link[0]->href; + $title = $entry->title->{'$t'}; + $author = $entry->author[0]->name->{'$t'}; + $seconds = $entry->{'media$group'}->{'yt$duration'}->seconds; + $published = $entry->published->{'$t'}; + $views = $entry->{'yt$statistics'}->viewCount; + $rating = $entry->{'gd$rating'}->average; + + $minutes = floor($seconds / 60); + $seconds = str_pad($seconds % 60, 2, '0', STR_PAD_LEFT); + $parsed_link = parse_url($link); + parse_str($parsed_link['query'], $parsed_query); + $link = 'http://youtu.be/' . $parsed_query['v']; + $published = date('n/j/y g:i A', strtotime($published)); + $views = number_format($views, 0); + $rating = round($rating, 2); + + $format = $this->getConfig('youtube.format'); + if (!$format) { + $format = '%nick%:' + . ' [ %link% ]' + . ' "%title%" by %author%,' + . ' Length %minutes%:%seconds%,' + . ' Published %published%,' + . ' Views %views%,' + . ' Rating %rating%'; + } + + $replacements = array( + 'nick' => $nick, + 'link' => $link, + 'title' => $title, + 'author' => $author, + 'minutes' => $minutes, + 'seconds' => $seconds, + 'published' => $published, + 'views' => $views, + 'rating' => $rating + ); + + $msg = $format; + foreach ($replacements as $from => $to) { + $msg = str_replace('%' . $from . '%', $to, $msg); + } + $this->doPrivmsg($this->event->getSource(), $msg); + } + + /** + * Returns the first result of a YouTube search. + * + * @param string $query Search query + * + * @return void + */ + public function onCommandYoutube($query) + { + $this->queryYoutube($query); + } + + /** + * Renders YouTube URLs. + * + * @param array $parsed parse_url() output for the URL to render + * + * @return boolean TRUE if the URL was rendered successfully, FALSE + * otherwise + */ + public function renderUrl(array $parsed) + { + switch ($parsed['host']) { + case 'youtu.be': + $v = ltrim($parsed['path'], '/'); + break; + case 'youtube.com': + case 'www.youtube.com': + parse_str($parsed['query'], $parsed_query); + if (!empty($parsed_query['v'])) { + $v = $parsed_query['v']; + break; + } + default: + return false; + } + + $this->queryYoutube($v); + + return true; + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php new file mode 100755 index 000000000..0ec3569fe --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Process/Abstract.php @@ -0,0 +1,130 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for obtaining and processing incoming events. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +abstract class Phergie_Process_Abstract +{ + /** + * Current driver instance + * + * @var Phergie_Driver_Abstract + */ + protected $driver; + + /** + * Current connection handler instance + * + * @var Phergie_Connection_Handler + */ + protected $connections; + + /** + * Current plugin handler instance + * + * @var Phergie_Plugin_Handler + */ + protected $plugins; + + /** + * Current event handler instance + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Current end-user interface instance + * + * @var Phergie_Ui_Abstract + */ + protected $ui; + + /** + * List of arguments for use within the instance + * + * @var array + */ + protected $options = array(); + + /** + * Gets the required class refences from Phergie_Bot. + * + * @param Phergie_Bot $bot Current bot instance in use + * @param array $options Optional processor arguments + * + * @return void + */ + public function __construct(Phergie_Bot $bot, array $options = array()) + { + $this->driver = $bot->getDriver(); + $this->plugins = $bot->getPluginHandler(); + $this->connections = $bot->getConnectionHandler(); + $this->events = $bot->getEventHandler(); + $this->ui = $bot->getUi(); + $this->options = $options; + } + + /** + * Sends resulting outgoing events from ealier processing in handleEvents. + * + * @param Phergie_Connection $connection Active connection + * + * @return void + */ + protected function processEvents(Phergie_Connection $connection) + { + $this->plugins->preDispatch(); + if (count($this->events)) { + foreach ($this->events as $event) { + $this->ui->onCommand($event, $connection); + + $method = 'do' . ucfirst(strtolower($event->getType())); + call_user_func_array( + array($this->driver, $method), + $event->getArguments() + ); + } + } + $this->plugins->postDispatch(); + + if ($this->events->hasEventOfType(Phergie_Event_Request::TYPE_QUIT)) { + $this->ui->onQuit($connection); + $this->connections->removeConnection($connection); + } + + $this->events->clearEvents(); + } + + /** + * Obtains and processes incoming events. + * + * @return void + */ + public abstract function handleEvents(); +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Async.php b/plugins/Irc/extlib/phergie/Phergie/Process/Async.php new file mode 100644 index 000000000..78d595914 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Process/Async.php @@ -0,0 +1,157 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Connection data processor which polls to handle input in an + * asynchronous manner. Will also cause the application tick at + * the user-defined wait time. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Process_Async extends Phergie_Process_Abstract +{ + /** + * Length of time to poll for stream activity (seconds) + * + * @var int + */ + protected $sec = 0; + + /** + * Length of time to poll for stream activity (microseconds) + * + * @var int + */ + protected $usec = 200000; + + /** + * Length of time to wait between ticks. + * + * @var int + */ + protected $wait = 0; + + /** + * Records when the application last performed a tick + * + * @var int + */ + protected $lastTick = 0; + + /** + * Overrides the parent class to set the poll time. + * + * @param Phergie_Bot $bot Main bot class + * @param array $options Processor arguments + * + * @return void + */ + public function __construct(Phergie_Bot $bot, array $options) + { + if (!$bot->getDriver() instanceof Phergie_Driver_Streams) { + throw new Phergie_Process_Exception( + 'The Async event processor requires the Streams driver' + ); + } + + foreach (array('sec', 'usec') as $var) { + if (isset($options[$var])) { + if (!is_int($options[$var])) { + throw new Phergie_Process_Exception( + 'Processor option "' . $var . '" must be an integer' + ); + } + $this->$var = $options[$var]; + } + } + + if (!isset($this->sec) && !isset($this->usec)) { + throw new Phergie_Process_Exception( + 'One of the processor options "sec" or "usec" must be specified' + ); + } + + parent::__construct($bot, $options); + } + + /** + * Waits for stream activity and performs event processing on + * connections with data to read. + * + * @return void + */ + protected function handleEventsAsync() + { + $hostmasks = $this->driver->getActiveReadSockets($this->sec, $this->usec); + if (!$hostmasks) { + return; + } + $connections = $this->connections->getConnections($hostmasks); + foreach ($connections as $connection) { + $this->driver->setConnection($connection); + $this->plugins->setConnection($connection); + $this->plugins->onTick(); + + if ($event = $this->driver->getEvent()) { + $this->ui->onEvent($event, $connection); + $this->plugins->setEvent($event); + $this->plugins->preEvent(); + $this->plugins->{'on' . ucfirst($event->getType())}(); + } + + $this->processEvents($connection); + } + } + + /** + * Perform application tick event on all plugins and connections. + * + * @return void + */ + protected function doTick() + { + foreach ($this->connections as $connection) { + $this->plugins->setConnection($connection); + $this->plugins->onTick(); + $this->processEvents($connection); + } + } + + /** + * Obtains and processes incoming events, then sends resulting outgoing + * events. + * + * @return void + */ + public function handleEvents() + { + $time = time(); + if ($this->lastTick == 0 || ($this->lastTick + $this->wait <= $time)) { + $this->doTick(); + $this->lastTick = $time; + } + $this->handleEventsAsync(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Exception.php b/plugins/Irc/extlib/phergie/Phergie/Process/Exception.php new file mode 100755 index 000000000..f964443c6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Process/Exception.php @@ -0,0 +1,33 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Exception related to event processor operations. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Process_Exception extends Phergie_Exception +{ +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Process/Standard.php b/plugins/Irc/extlib/phergie/Phergie/Process/Standard.php new file mode 100644 index 000000000..24adbaf99 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Process/Standard.php @@ -0,0 +1,57 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Connection data processor which reads all connections looking + * for a response. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Process_Standard extends Phergie_Process_Abstract +{ + /** + * Obtains and processes incoming events, then sends resulting outgoing + * events. + * + * @return void + */ + public function handleEvents() + { + foreach ($this->connections as $connection) { + $this->driver->setConnection($connection); + $this->plugins->setConnection($connection); + $this->plugins->onTick(); + + if ($event = $this->driver->getEvent()) { + $this->ui->onEvent($event, $connection); + $this->plugins->setEvent($event); + $this->plugins->preEvent(); + $this->plugins->{'on' . ucfirst($event->getType())}(); + } + + $this->processEvents($connection); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php b/plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php new file mode 100644 index 000000000..9c1b3a6f1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/StatusnetBot.php @@ -0,0 +1,98 @@ +<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ *
+ * 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/>.
+ *
+ * Extends the bot class (Phergie_Bot) to allow connection and access to
+ * sockets and to allow StatusNet to 'drive' the bot
+ *
+ * @category Phergie
+ * @package Phergie_StatusnetBot
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class Phergie_StatusnetBot extends Phergie_Bot {
+ /**
+ * Set up bot and connect to servers
+ *
+ * @return void
+ */
+ public function connect() {
+ $ui = $this->getUi();
+ $ui->setEnabled($this->getConfig('ui.enabled'));
+
+ $this->loadPlugins();
+ $this->loadConnections();
+ }
+
+ /**
+ * Transmit raw command to server using driver
+ *
+ * Handles construction of command strings and their transmission to the
+ * server.
+ *
+ * @param string $command Command to send
+ * @param string|array $args Optional string or array of sequential
+ * arguments
+ *
+ * @return string Command string that was sent
+ * @throws Phergie_Driver_Exception
+ */
+ public function send($command, $args = '') {
+ return $this->getDriver()->send($command, $args);
+ }
+
+ /**
+ * Handle incoming data on the socket using the handleEvents
+ * method of the Processor
+ *
+ * @return void
+ */
+ public function handleEvents() {
+ $this->getProcessor()->handleEvents();
+ }
+
+ /**
+ * Close the current connection and reconnect to the server
+ *
+ * @return void
+ */
+ public function reconnect() {
+ $driver = $this->getDriver();
+ $sockets = $driver->getSockets();
+
+ // Close any existing connections
+ try {
+ $driver->forceQuit();
+ } catch (Phergie_Driver_Exception $e){}
+ try {
+ $driver->doConnect();
+ } catch (Phergie_Driver_Exception $e){
+ $driver->forceQuit();
+ throw $e;
+ }
+ }
+
+ /**
+ * Get the sockets used by the bot
+ *
+ * @return array Array of socket resources
+ */
+ public function getSockets() {
+ return $this->getDriver()->getSockets();
+ }
+}
\ No newline at end of file diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL new file mode 100755 index 000000000..dfc985756 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/INSTALL @@ -0,0 +1,24 @@ +----------------------------------------------------------------------------- +-- About LogViewer +----------------------------------------------------------------------------- + +The purpose of this tool is to make an HTML display of the Logs made by the +Logging plugin for Phergie stored in SQLite database. + +----------------------------------------------------------------------------- +-- Installation +----------------------------------------------------------------------------- + +To install this, simply copy the contents of this directory into whatever +directory you want this to run under. + +----------------------------------------------------------------------------- +-- Configuration +----------------------------------------------------------------------------- + +The only configuration needed at this point in time is to edit the $database +variable in the top of the index.php to point to the appropriate Phergie +log file. + +Stuff may get more complicated in the future ;) But this is meant to be +simple for now.
\ No newline at end of file diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php new file mode 100755 index 000000000..fc27fa294 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Tools/LogViewer/index.php @@ -0,0 +1,368 @@ +<?php +// Phergie Log Viewer ... Currently designed as a single PHP file in order to make it easy to +// 'install' this. Just drop the index.php (or whatever name you wish to rename it to) wherever +// you wish, and it will simply work. Sure, it would be nice to structure some of this stuff into +// various include files/etc. But right now this is simple enough of a quick log viewer, that it's +// just one file. + + +/********** SETUP **********/ + +// (Change any of these if/as needed for your setup) +ini_set('default_charset', 'UTF-8'); +date_default_timezone_set('UTC'); +$log = "/PATH/AND/FILENAME/TO/YOUR/LOGFILE/PLEASE.db"; + + +/********** PREPARATION **********/ + +$db = new PDO('sqlite:' . $log); +if (!is_object($db)) { + // Failure, can't access Phergie Log. Bail with an error message, not pretty, but works: + echo "ERROR: Cannot access Phergie Log File, please check the configuration & access privileges"; + exit(); +} + + +/********** DETECTION **********/ + +// Determine the mode of the application and call the appropriate handler function +$mode = empty($_GET['m']) ? '' : $_GET['m']; +switch ($mode) { + case 'channel': + show_days($db); + break; + case 'day': + show_log($db); + break; + default: + show_channels($db); +} + +// Exit not really needed here, but reminds us that everything below is support functions: +exit(); + + +/********** MODES **********/ + +/** + * show_channels + * + * Provide a list of all channel's that we are logging information for: + * + * @param PDO A PDO object referring to the database + * @return void + * @author Eli White <eli@eliw.com> + **/ +function show_channels(PDO $db) { + // Begin the HTML page: + template_header('Channels'); + echo "\nChannels:\n<ul>\n"; + + // Loop through the database reading in each channel, and echoing out a <li> for it. + // only grab actual channels that start with # ... also pre-lowercase everything. + // this allows us to 'deal' with variable caps in how the channels were logged. + $channels = $db->query(" + select distinct lower(chan) as c + from logs + where chan like '#%' + "); + foreach ($channels as $row) { + $html = utf8specialchars($row['c']); + $url = urlencode($row['c']); + echo "<li><a href=\"?m=channel&w={$url}\">{$html}</a></li>\n"; + } + + // Finish off the page: + echo "\n</ul>\n"; + template_footer(); +} + +/** + * show_days + * + * Create a calendar view of all days available for this particular channel + * + * NOTE: May get unwieldy if large log files. Perhaps consider in the future + * making a paginated version of this? by year? Or a separate 'which year' page + * before this? Not to worry about now. + * + * @param PDO A PDO object referring to the database + * @return void + * @author Eli White <eli@eliw.com> + **/ +function show_days(PDO $db) { + $channel = $_GET['w']; + $url = urlencode($channel); + + // Begin the HTML page: + template_header('Daily Logs for Channel: ' . utf8specialchars($channel)); + echo "\n<ul>\n"; + + // Query the database to discover all days that are available for this channel: + $data = array(); + $prepared = $db->prepare(" + select distinct date(tstamp) as day + from logs + where lower(chan) = :chan + "); + $prepared->execute(array(':chan' => $channel)); + foreach ($prepared as $row) { + list($y, $m, $d) = explode('-', $row['day']); + $data[$y][$m][$d] = "{$y}-{$m}-{$d}"; + } + + // For now, just loop over them all and provide a list: + ksort($data); + foreach ($data as $year => $months) { + ksort($months); + foreach ($months as $month => $days) { + // Figure out a few facts about this month: + $stamp = mktime(0, 0, 0, $month, 1, $year); + $first_weekday = idate('w', $stamp); + $days_in_month = idate('t', $stamp); + $name = date('F', $stamp); + + // We have a month ... start a new table: + echo <<<EOTABLE +<div class="month"> + <table> + <caption>{$name} {$year}</caption> + <tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th></tr> +EOTABLE; + // Now we need to start looping through the days in this month: + echo '<tr>'; + $rowmod = 0; + // Loop through all day entries, no matter how many blanks we need: + for ($d = (-$first_weekday + 1); $d < $days_in_month + 1; $d++) { + if (!($rowmod++ % 7)) { + // Stop/start a new row: + echo '</tr><tr>'; + } + echo '<td>'; + // If this day is pre or post actual month days, make it blank: + if (($d < 1) || ($d > $days_in_month)) { + echo ' '; + } elseif (isset($days[$d])) { + // Make a link to the day's log: + echo "<a href=\"?m=day&w={$url}&d={$days[$d]}\">{$d}</a>"; + } else { + // Just a dead number: + echo $d; + } + echo '</td>'; + } + // Finish off any blanks needed for a complete table row: + while ($rowmod++ % 7) { + echo '<td> </td>'; + } + echo "</tr></table></div>\n"; + } + } + + // Finish off the page: + echo "\n</ul>\n"; + template_footer(); +} + +/** + * show_log + * + * Actually show the log for this specific day + * + * @param PDO A PDO object referring to the database + * @return void + * @author Eli White <eli@eliw.com> + **/ +function show_log(PDO $db) { + $channel = $_GET['w']; + $day = $_GET['d']; + $parts = explode('-', $day); + $formatted_date = "{$parts[0]}-{$parts[1]}-{$parts[2]}"; + + // Begin the HTML page: + template_header('Date: ' . utf8specialchars($formatted_date) . + ' - Channel: ' . utf8specialchars($channel)); + + // Query the database to get all log lines for this date: + $prepared = $db->prepare(" + select time(tstamp) as t, type, nick, message + from logs + where lower(chan) = :chan and date(tstamp) = :day + order by tstamp asc + "); + $prepared->execute(array( + ':chan' => $channel, + ':day' => $day, + )); + + // Loop through each line, + foreach ($prepared as $row) { + // Prepare some basic details for output: + $color = nick_color($row['nick']); + $time = utf8specialchars($row['t']); + $msg = utf8specialchars($row['message']); + $nick = utf8specialchars($row['nick']); + $type = false; + + // Now change the format of the line based upon the type: + switch ($row['type']) { + case 4: // PRIVMSG (A Regular Message) + echo "[$time] <span style=\"color:#{$color};\"><{$nick}></span> {$msg}<br />\n"; + break; + case 5: // ACTION (emote) + echo "[$time] <span style=\"color:#{$color};\">*{$nick} {$msg}</span><br />\n"; + break; + case 1: // JOIN + echo "[$time] -> {$nick} joined the room.<br />\n"; + break; + case 2: // PART (leaves channel) + echo "[$time] -> {$nick} left the room: {$msg}<br />\n"; + break; + case 3: // QUIT (quits the server) + echo "[$time] -> {$nick} left the server: {$msg}<br />\n"; + break; + case 6: // NICK (changes their nickname) + echo "[$time] -> {$nick} is now known as: {$msg}<br />\n"; + break; + case 7: // KICK (booted) + echo "[$time] -> {$nick} boots {$msg} from the room.<br />\n"; + break; + case 8: // MODE (changed their mode) + $type = 'MODE'; + case 9: // TOPIC (changed the topic) + $type = $type ? $type : 'TOPIC'; + echo "[$time] -> {$nick}: :{$type}: {$msg}<br />\n"; + } + } + + // Finish up the page: + template_footer(); +} + +/** + * nick_color + * + * Uses a silly little algorithm to pick a consistent but unique(ish) color for + * any given username. NOTE: Augment this in the future to make it not generate + * 'close to white' ones, also maybe to ensure uniqueness? (Not allow two to have + * colors that are close to each other?) + * + * @return string A CSS valid hex color string + * @author Eli White <eli@eliw.com> + **/ +function nick_color($user) { + static $colors = array(); + + if (!isset($colors[$user])) { + $colors[$user] = substr(md5($user), 0, 6); + } + + return $colors[$user]; +} + +/** + * utf8specialchars + * + * Just a quick wrapper around htmlspecialchars + * + * @param string The UTF8 string to escape + * @return string An escaped and ready for HTML use string + * @author Eli White <eli@eliw.com> + **/ +function utf8specialchars($string) { + return htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); +} + + +/********** TEMPLATES **********/ + +/** + * template_header + * + * Echo out the header for each HTML page + * + * @param $title string The title to be used for this page. + * @return void + * @author Eli White <eli@eliw.com> + **/ +function template_header($title) { + $css = template_css(); + echo <<<EOHTML +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <title>Phergie LogViewer - {$title}</title> + <style type="text/css" media="all">{$css}</style> + </head> + <body> + <h2>Phergie LogViewer - {$title}</h2> +EOHTML; +} + +/** + * template_footer + * + * Echo out the bottom of each HTML page + * + * @return void + * @author Eli White <eli@eliw.com> + **/ +function template_footer() { + echo <<<EOHTML + </body> +</html> +EOHTML; +} + +/** + * template_css + * + * Generate the CSS used by these HTML pages & return it. + * + * @return string The CSS in question: + * @author Eli White <eli@eliw.com> + **/ +function template_css() { + return <<<EOCSS + div.month { + float: left; + height: 15em; + } + + div.month table { + border-collapse: collapse; + border: 2px solid black; + margin-right: 2em; + } + + div.month td, div.month th { + text-align: center; + vertical-align: bottom; + border: 1px solid gray; + width: 2em; + height: 1.7em; + padding: 1px; + margin: 0px; + } + + div.month th { + text-decoration: bold; + border: 2px solid black; + } + + div.month a { + text-decoration: none; + } + + a:visited, a:link { + color: blue; + } + + a:active, a:hover { + color: red; + } +EOCSS; +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Tools/README b/plugins/Irc/extlib/phergie/Phergie/Tools/README new file mode 100755 index 000000000..ed8fe2946 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Tools/README @@ -0,0 +1,6 @@ +This directory holds any assorted tools that work with Phergie but that are +not part of Phergie itself. Such as tools to examine data that Phergie is +capturing/logging. + +Each should live inside of it's own directory, and be completely self-contained +with instructions inside on how to use it. diff --git a/plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php b/plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php new file mode 100644 index 000000000..e2e2ce2f1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Ui/Abstract.php @@ -0,0 +1,116 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * Base class for end-user interfaces. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +abstract class Phergie_Ui_Abstract +{ + /** + * Handler for when a server connection is attempted. + * + * @param string $host Server hostname + * + * @return void + */ + public function onConnect($host) + { + } + + /** + * Handler for when an attempt is made to load a plugin. + * + * @param string $plugin Short name of the plugin + * + * @return void + */ + public function onPluginLoad($plugin) + { + } + + /** + * Handler for when a plugin fails to load. + * + * @param string $plugin Short name of the plugin + * @param string $message Message describing the reason for the failure + * + * @return void + */ + public function onPluginFailure($plugin, $message) + { + } + + /** + * Handler for when the bot receives an IRC event. + * + * @param Phergie_Event_Abstract $event Received event + * @param Phergie_Connection $connection Connection on which the event + * was received + * + * @return void + */ + public function onEvent(Phergie_Event_Abstract $event, + Phergie_Connection $connection + ) { + } + + /** + * Handler for when the bot sends a command to a server. + * + * @param Phergie_Event_Command $event Event representing the command + * being sent + * @param Phergie_Connection $connection Connection on which the command + * is being sent + * + * @return void + */ + public function onCommand(Phergie_Event_Command $event, + Phergie_Connection $connection + ) { + } + + /** + * Handler for when the bot terminates a connection to a server. + * + * @param Phergie_Connection $connection Terminated connection + * + * @return void + */ + public function onQuit(Phergie_Connection $connection) + { + } + + /** + * Handler for when the bot shuts down after terminating all server + * connections. + * + * @return void + */ + public function onShutdown() + { + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Ui/Console.php b/plugins/Irc/extlib/phergie/Phergie/Ui/Console.php new file mode 100644 index 000000000..a0a528b3f --- /dev/null +++ b/plugins/Irc/extlib/phergie/Phergie/Ui/Console.php @@ -0,0 +1,223 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * End-user interface that produces console output when running the bot from + * a shell. + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ +class Phergie_Ui_Console extends Phergie_Ui_Abstract +{ + /** + * Flag that toggles all console output + * + * @var bool + */ + protected $enabled; + + /** + * Format for timestamps included in console output + * + * @var string + * @link http://php.net/date + */ + protected $format; + + /** + * Constructor to initialize object properties. + * + * @return void + */ + public function __construct() + { + $this->enabled = true; + $this->format = 'H:i:s'; + } + + /** + * Outputs a timestamped line to the console if console output is enabled. + * + * @param string $line Line to output + * + * @return void + */ + protected function console($line) + { + if ($this->enabled) { + echo date($this->format), ' ', $line, PHP_EOL; + } + } + + /** + * Returns whether console output is enabled. + * + * @return bool TRUE if console output is enabled, FALSE otherwise + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * Sets whether console output is enabled. + * + * @param bool $enabled TRUE to enable console output, FALSE otherwise, + * defaults to TRUE + * + * @return Phergie_Ui_Console Provides a fluent interface + */ + public function setEnabled($enabled = true) + { + $this->enabled = (bool) $enabled; + return $this; + } + + /** + * Returns the format used for timestamps in console output. + * + * @return string + * @link http://php.net/date + */ + public function getFormat() + { + return $this->format; + } + + /** + * Sets the format used for timestamps in console output, overwriting + * any previous format used. + * + * @param string $format Timestamp format + * + * @return Phergie_Ui_Console Provides a fluent interface + * @link http://php.net/date + */ + public function setFormat($format) + { + $this->format = (string) $format; + return $this; + } + + /** + * Outputs a prompt when a server connection is attempted. + * + * @param string $host Server hostname + * + * @return void + */ + public function onConnect($host) + { + $this->console('Connecting to ' . $host); + } + + /** + * Outputs a prompt when a plugin is loaded successfully. + * + * @param string $plugin Short name of the plugin + * + * @return void + */ + public function onPluginLoad($plugin) + { + $this->console('Loaded plugin ' . $plugin); + } + + /** + * Outputs a prompt when a plugin fails to load. + * + * @param string $plugin Short name of the plugin + * @param string $message Message describing the reason for the failure + * + * @return void + */ + public function onPluginFailure($plugin, $message) + { + $this->console('Unable to load plugin ' . $plugin . ' - ' . $message); + } + + /** + * Outputs a prompt when the bot receives an IRC event. + * + * @param Phergie_Event_Abstract $event Received event + * @param Phergie_Connection $connection Connection on which the + * event was received + * + * @return void + */ + public function onEvent(Phergie_Event_Abstract $event, + Phergie_Connection $connection + ) { + $host = $connection->getHostmask()->getHost(); + $this->console($host . ' <- ' . $event->getRawData()); + } + + /** + * Outputs a prompt when the bot sends a command to a server. + * + * @param Phergie_Event_Command $event Event representing the + * command being sent + * @param Phergie_Connection $connection Connection on which the + * command is being sent + * + * @return void + */ + public function onCommand(Phergie_Event_Command $event, + Phergie_Connection $connection + ) { + $plugin = $event->getPlugin()->getName(); + $host = $connection->getHostmask()->getHost(); + $type = strtoupper($event->getType()); + $args = implode(' ', $event->getArguments()); + $this->console( + $plugin . ' plugin: ' . + $host . ' -> ' . $type . ' ' . $args + ); + } + + /** + * Outputs a prompt when the bot terminates a connection to a server. + * + * @param Phergie_Connection $connection Terminated connection + * + * @return void + */ + public function onQuit(Phergie_Connection $connection) + { + $host = $connection->getHostmask()->getHost(); + $this->console('Disconnecting from ' . $host); + } + + /** + * Outputs a prompt when the bot shuts down after terminating all server + * connections. + * + * @return void + */ + public function onShutdown() + { + $this->console('Shutting down'); + } +} diff --git a/plugins/Irc/extlib/phergie/PhergiePackageTask.php b/plugins/Irc/extlib/phergie/PhergiePackageTask.php new file mode 100644 index 000000000..dfc760a16 --- /dev/null +++ b/plugins/Irc/extlib/phergie/PhergiePackageTask.php @@ -0,0 +1,110 @@ +<?php + +require_once 'phing/tasks/ext/PearPackage2Task.php'; + +class PhergiePackageTask extends PearPackage2Task +{ + protected function setOptions() + { + $this->pkg->addMaintainer('lead', 'team', 'Phergie Development Team', 'team@phergie.org'); + + $path = str_replace('_', '/', $this->package) . '.php'; + if (file_exists($path)) { + $contents = file_get_contents($path); + preg_match_all('#/\*\*(.*)\*/#Ums', $contents, $matches, PREG_SET_ORDER); + $doc = $matches[1][1]; + + $have_summary = false; + $have_description = false; + foreach ($this->options as $option) { + switch ($option->getName()) { + case 'summary': + $have_summary = true; + break; + case 'description': + $have_descripion = true; + break; + } + } + + if (!$have_summary || !$have_description) { + $description = substr($doc, 0, strpos($doc, '@')); + $description = trim(preg_replace(array('#^[\h*]*|[\h*]*$#m', '#[\h]+#m'), array('', ' '), $description)); + $split = preg_split('/\v\v+/', $description); + $summary = trim(array_shift($split)); + if (!$have_summary) { + $this->pkg->setSummary(htmlentities($summary, ENT_QUOTES)); + } + if (!$have_description) { + $this->pkg->setDescription(htmlentities($description, ENT_QUOTES)); + } + } + + $doc = preg_split('/\v+/', $doc); + $doc = preg_grep('/@uses/', $doc); + $doc = preg_replace('/\s*\* @uses\s+|\s+$/', '', $doc); + foreach ($doc as $line) { + if (strpos($line, 'extension') === 0) { + $line = explode(' ', $line); + $name = $line[1]; + $optional = 'required'; + if (isset($line[2])) { + $optional = $line[2]; + } + $this->pkg->addExtensionDep( + $optional, + $name + ); + } else { + $line = explode(' ', $line); + $name = $line[0]; + $channel = $line[1]; + $optional = 'required'; + if (isset($line[2])) { + $optional = $line[2]; + } + $this->pkg->addPackageDepWithChannel( + $optional, + $name, + $channel + ); + } + } + } + + $newmap = array(); + foreach ($this->mappings as $key => $map) { + switch ($map->getName()) { + case 'releases': + $releases = $map->getValue(); + foreach ($releases as $release) { + $this->pkg->addRelease(); + if (isset($release['installconditions'])) { + if (isset($release['installconditions']['os'])) { + $this->pkg->setOsInstallCondition($release['installconditions']['os']); + } + } + if (isset($release['filelist'])) { + if (isset($release['filelist']['install'])) { + foreach ($release['filelist']['install'] as $file => $as) { + $this->pkg->addInstallAs($file, $as); + } + } + if (isset($release['filelist']['ignore'])) { + foreach ($release['filelist']['ignore'] as $file) { + $this->pkg->addIgnoreToRelease($file); + } + } + } + } + break; + + default: + $newmap[] = $map; + } + } + $this->mappings = $newmap; + + parent::setOptions(); + } +} diff --git a/plugins/Irc/extlib/phergie/README b/plugins/Irc/extlib/phergie/README new file mode 100644 index 000000000..d97ce0524 --- /dev/null +++ b/plugins/Irc/extlib/phergie/README @@ -0,0 +1,11 @@ +Phergie is an IRC bot written for PHP 5.2. + +Main project web site: http://phergie.org + +Instructions for running your own instance of Phergie: http://phergie.org/users/ + +Architectural overview for plugin developers: http://phergie.org/developers/ + +Support: http://phergie.org/support/ + +Bug reports/feature requests: http://github.com/elazar/phergie/issues diff --git a/plugins/Irc/extlib/phergie/Settings.php.dist b/plugins/Irc/extlib/phergie/Settings.php.dist new file mode 100755 index 000000000..87b4a95c6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Settings.php.dist @@ -0,0 +1,98 @@ +<?php + +return array( + + // One array per connection, pretty self-explanatory + 'connections' => array( + // Ex: All connection info for the Freenode network + array( + 'host' => 'irc.freenode.net', + 'port' => 6667, + 'username' => 'Elazar', + 'realname' => 'Matthew Turland', + 'nick' => 'Phergie2', + // 'password' => 'password goes here if needed', + // 'transport' => 'ssl', // uncomment to connect using SSL + // 'encoding' => 'UTF8', // uncomment if using UTF8 + ) + ), + + 'processor' => 'async', + 'processor.options' => array('usec' => 200000), + // Time zone. See: http://www.php.net/manual/en/timezones.php + 'timezone' => 'UTC', + + // Whitelist of plugins to load + 'plugins' => array( + // To enable a plugin, simply add a string to this array containing + // the short name of the plugin as shown below. + + // 'ShortPluginName', + + // Below is an example of enabling the AutoJoin plugin, for which + // the corresponding PEAR package is Phergie_Plugin_AutoJoin. This + // plugin allows you to set a list of channels in this configuration + // file that the bot will automatically join when it connects to a + // server. If you'd like to enable this plugin, simply install it, + // uncomment the line below, and set a value for the setting + // autojoin.channels (examples for which are located further down in + // this file). + + // 'AutoJoin', + + // A few other recommended plugins: + + // Servers randomly send PING events to clients to ensure that + // they're still connected and will eventually terminate the + + // connection if a PONG response is not received. The Pong plugin + // handles sending these responses. + + // 'Pong', + + // It's sometimes difficult to distinguish between a lack of + // activity on a server and the client not receiving data even + // though a connection remains open. The Ping plugin performs a self + // CTCP PING sporadically to ensure that its connection is still + // functioning and, if not, terminates the bot. + + // 'Ping', + + // Sometimes it's desirable to have the bot disconnect gracefully + // when issued a command to do so via a PRIVMSG event. The Quit + // plugin implements this using the Command plugin to intercept the + // command. + + // 'Quit', + ), + + // If set to true, this allows any plugin dependencies for plugins + // listed in the 'plugins' option to be loaded even if they are not + // explicitly included in that list + 'plugins.autoload' => true, + + // Enables shell output describing bot events via Phergie_Ui_Console + 'ui.enabled' => true, + + // Examples of supported values for autojoins.channel: + // 'autojoin.channels' => '#channel1,#channel2', + // 'autojoin.channels' => array('#channel1', '#channel2'), + // 'autojoin.channels' => array( + // 'host1' => '#channel1,#channel2', + // 'host2' => array('#channel3', '#channel4') + // ), + + // Examples of setting values for Ping plugin settings + + // This is the amount of time in seconds that the Ping plugin will wait + // to receive an event from the server before it initiates a self-ping + + // 'ping.event' => 300, // 5 minutes + + // This is the amount of time in seconds that the Ping plugin will wait + // following a self-ping attempt before it assumes that a response will + // never be received and terminates the connection + + // 'ping.ping' => 10, // 10 seconds + +); diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php new file mode 100644 index 000000000..ba94cd0ea --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php @@ -0,0 +1,262 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Connection. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_ConnectionTest extends PHPUnit_Framework_TestCase +{ + /** + * Associative array containing an option-to-value mapping + * + * @var array + */ + private $options = array( + 'host' => 'example.com', + 'port' => 4080, + 'transport' => 'udp', + 'encoding' => 'ASCII', + 'nick' => 'MyNick', + 'username' => 'MyUsername', + 'realname' => 'MyRealName', + 'password' => 'MyPassword', + ); + + /** + * Data provider for testGetOptionReturnsDefault(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetOptionReturnsDefault() + */ + public function dataProviderTestGetOptionReturnsDefault() + { + return array( + array('transport', 'tcp'), + array('encoding', 'ISO-8859-1'), + array('port', 6667), + array('password', null), + ); + } + + /** + * Tests that a default values are used for some options. + * + * @param string $option Name of the option with a default value + * @param mixed $value Default value of the option + * + * @return void + * @dataProvider dataProviderTestGetOptionReturnsDefault + */ + public function testGetOptionReturnsDefault($option, $value) + { + $connection = new Phergie_Connection; + $this->assertEquals($value, $connection->{'get' . ucfirst($option)}()); + } + + /** + * Tests that a default encoding is used if one isn't specified. + * + * @return void + */ + public function testGetEncodingReturnsDefault() + { + $connection = new Phergie_Connection; + $this->assertEquals('ISO-8859-1', $connection->getEncoding()); + } + + /** + * Tests that options can be set via the constructor. + * + * @return void + */ + public function testSetOptionsViaConstructor() + { + $connection = new Phergie_Connection($this->options); + foreach ($this->options as $key => $value) { + $this->assertEquals($value, $connection->{'get' . ucfirst($key)}()); + } + } + + /** + * Data provider for testGetHostmaskMissingDataGeneratesException(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetHostmaskMissingDataGeneratesException() + */ + public function dataProviderTestGetHostmaskMissingDataGeneratesException() + { + return array( + array(null, $this->options['username'], $this->options['host']), + array($this->options['nick'], null, $this->options['host']), + array($this->options['nick'], $this->options['username'], null), + ); + } + + /** + * Tests that attempting to retrieve a hostmask without option values + * for all of its constituents generates an exception. + * + * @param string $nick Bot nick + * @param string $username Bot username + * @param string $host Server hostname + * + * @return void + * @dataProvider dataProviderTestGetHostmaskMissingDataGeneratesException + */ + public function testGetHostmaskMissingDataGeneratesException($nick, $username, $host) + { + $options = array( + 'nick' => $nick, + 'username' => $username, + 'host' => $host, + ); + + $connection = new Phergie_Connection($options); + + try { + $hostmask = $connection->getHostmask(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to retrieve a hostmask with all required + * options is successful. + * + * @return void + */ + public function testGetHostmaskWithValidData() + { + $options = array( + 'nick' => 'MyNick', + 'username' => 'MyUsername', + 'host' => 'example.com' + ); + + $connection = new Phergie_Connection($options); + $hostmask = $connection->getHostmask(); + $this->assertType('Phergie_Hostmask', $hostmask); + } + + /** + * Data provider for testGetRequiredOptionsWithoutValuesSet(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetRequiredOptionsWithoutValuesSet() + */ + public function dataProviderTestGetRequiredOptionsWithoutValuesSet() + { + return array( + array('host'), + array('nick'), + array('username'), + array('realname'), + ); + } + + /** + * Tests that attempting to retrieve values of required options when no + * values are set results in an exception. + * + * @param string $option Option name + * + * @return void + * @dataProvider dataProviderTestGetRequiredOptionsWithoutValuesSet + */ + public function testGetRequiredOptionsWithoutValuesSet($option) + { + try { + $connection = new Phergie_Connection; + $value = $connection->{'get' . ucfirst($option)}(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to set an invalid value for the transport + * results in an exception. + * + * @return void + */ + public function testSetTransportWithInvalidValue() + { + $connection = new Phergie_Connection; + try { + $connection->setTransport('blah'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to set an invalid value for the encoding + * results in an exception. + * + * @return void + */ + public function testSetEncodingWithInvalidValue() + { + $connection = new Phergie_Connection; + try { + $connection->setEncoding('blah'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that options can be set collectively after the connection is + * instantiated. + * + * @return void + */ + public function testSetOptions() + { + $connection = new Phergie_Connection; + $connection->setOptions($this->options); + foreach ($this->options as $key => $value) { + $this->assertEquals($value, $connection->{'get' . ucfirst($key)}()); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php new file mode 100644 index 000000000..9ecdd327a --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php @@ -0,0 +1,837 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Handler. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_HandlerTest extends PHPUnit_Framework_TestCase +{ + /** + * Plugin handler instance being tested + * + * @var Phergie_Plugin_Handler + */ + protected $handler; + + /** + * Mock Phergie_Config instance passed to the plugin handler constructor + * + * @var Phergie_Config + */ + protected $config; + + /** + * Mock Phergie_Event_Handler instance passed to the plugin handler + * constructor + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Returns a mock plugin instance. + * + * @param string $name Optional short name for the mock plugin, defaults + * to 'TestPlugin' + * @param array $methods Optional list of methods to override + * + * @return Phergie_Plugin_Abstract + */ + protected function getMockPlugin($name = 'TestPlugin', array $methods = array()) + { + $methods[] = 'getName'; + $plugin = $this->getMock('Phergie_Plugin_Abstract', $methods); + $plugin + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); + return $plugin; + } + + /** + * Sets up a new handler instance before each test. + * + * @return void + */ + public function setUp() + { + $this->config = $this->getMock('Phergie_Config'); + $this->events = $this->getMock('Phergie_Event_Handler'); + $this->handler = new Phergie_Plugin_Handler( + $this->config, + $this->events + ); + } + + /** + * Tests iterability of the plugin handler. + * + * @return void + */ + public function testImplementsIteratorAggregate() + { + $reflection = new ReflectionObject($this->handler); + + $this->assertTrue( + $reflection->implementsInterface('IteratorAggregate'), + 'Handler does not implement IteratorAggregate' + ); + + $this->assertType( + 'Iterator', + $this->handler->getIterator(), + 'getIterator() must return an iterator' + ); + } + + /** + * Tests that a default iterator is returned if none is explicitly set. + * + * @return void + */ + public function testGetIteratorReturnsDefault() + { + $this->assertType( + 'Phergie_Plugin_Iterator', + $this->handler->getIterator() + ); + } + + /** + * Tests the ability to change the handler's iterator class when a valid + * class is specified. + * + * @return void + */ + public function testSetIteratorClassWithValidClass() + { + eval(' + class DummyIterator extends FilterIterator { + public function accept() { + return true; + } + } + '); + + $this->handler->setIteratorClass('DummyIterator'); + + $this->assertType( + 'DummyIterator', + $this->handler->getIterator() + ); + } + + /** + * Tests that a failure occurs when a nonexistent iterator class is + * specified. + * + * @return void + */ + public function testSetIteratorClassWithNonexistentClass() + { + try { + $this->handler->setIteratorClass('FooIterator'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Tests that a failure occurs when a class that is not a subclass of + * FilterIterator is specified. + * + * @return void + */ + public function testSetIteratorClassWithNonFilterIteratorClass() + { + try { + $this->handler->setIteratorClass('ArrayIterator'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Tests countability of the plugin handler. + * + * @return void + */ + public function testImplementsCountable() + { + $reflection = new ReflectionObject($this->handler); + + $this->assertTrue( + $reflection->implementsInterface('Countable'), + 'Handler does not implement Countable' + ); + + $this->assertType( + 'int', + count($this->handler), + 'count() must return an integer' + ); + } + + /** + * Tests the plugin handler exposing added plugins as instance + * properties of the handler via isset(). + * + * @return void + */ + public function testImplementsIsset() + { + $pluginName = 'TestPlugin'; + $this->assertFalse(isset($this->handler->{$pluginName})); + $plugin = $this->getMockPlugin($pluginName); + $this->handler->addPlugin($plugin); + $this->assertTrue(isset($this->handler->{$pluginName})); + } + + /** + * Tests the plugin handler exposing added plugins as instance + * properties of the handler. + * + * @depends testImplementsIsset + * @return void + */ + public function testImplementsGet() + { + $plugin = $this->getMockPlugin(); + $this->handler->addPlugin($plugin); + $name = $plugin->getName(); + $getPlugin = $this->handler->getPlugin($name); + $this->assertTrue(isset($this->handler->$name)); + $get = $this->handler->$name; + $this->assertSame($getPlugin, $get); + } + + /** + * Tests the plugin handler allowing for plugin removal via unset(). + * + * @depends testImplementsGet + * @return void + */ + public function testImplementsUnset() + { + $plugin = $this->getMockPlugin(); + $this->handler->addPlugin($plugin); + unset($this->handler->{$plugin->getName()}); + $this->assertFalse($this->handler->hasPlugin($plugin->getName())); + } + + /** + * Tests the plugin handler executing a callback on all contained + * plugins. + * + * @return void + */ + public function testImplementsCall() + { + foreach (range(1, 2) as $index) { + $plugin = $this->getMockPlugin('TestPlugin' . $index, array('callback')); + $plugin + ->expects($this->once()) + ->method('callback'); + $this->handler->addPlugin($plugin); + } + + $this->assertTrue($this->handler->callback()); + } + + /** + * Tests a newly instantiated handler not having plugins associated with + * it. + * + * @depends testImplementsCountable + * @return void + */ + public function testEmptyHandlerHasNoPlugins() + { + $this->assertEquals(0, count($this->handler)); + } + + /** + * Tests a newly instantiated handler not having autoloading enabled by + * default. + * + * @return void + */ + public function testGetAutoloadDefaultsToNotAutoload() + { + $this->assertFalse($this->handler->getAutoload()); + } + + /** + * Tests setAutoload(). + * + * @depends testGetAutoloadDefaultsToNotAutoload + * @return void + */ + public function testSetAutoload() + { + $this->assertSame( + $this->handler->setAutoload(true), + $this->handler, + 'setAutoload() does not provide a fluent interface' + ); + + $this->assertTrue( + $this->handler->getAutoload(), + 'setAutoload() had no effect on getAutoload()' + ); + } + + /** + * Tests addPath() providing a fluent interface. + * + * @return void + */ + public function testAddPathProvidesFluentInterface() + { + $handler = $this->handler->addPath(dirname(__FILE__)); + $this->assertSame($this->handler, $handler); + } + + /** + * Tests addPath() throwing an exception when it cannot read the + * directory. + * + * @return void + */ + public function testAddPathThrowsExceptionOnUnreadableDirectory() + { + try { + $this->handler->addPath('/an/unreadable/directory/path'); + } catch(Phergie_Plugin_Exception $e) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_DIRECTORY_NOT_READABLE, + $e->getCode() + ); + return; + } + + $this->fail('An expected exception has not been raised'); + } + + /** + * Tests adding a path to the plugin handler. + * + * @return void + */ + public function testAddPath() + { + $pluginName = 'Mock'; + + try { + $this->handler->addPlugin($pluginName); + } catch(Phergie_Plugin_Exception $e) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND, + $e->getCode() + ); + } + + if (!isset($e)) { + $this->fail('Plugin loaded, path was already present'); + } + + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + + try { + $this->handler->addPlugin($pluginName); + } catch(Phergie_Plugin_Exception $e) { + $this->fail('Added path, plugin still not found'); + } + } + + /** + * Tests addPlugin() returning an added plugin instance. + * + * @return void + */ + public function testAddPluginByInstanceReturnsPluginInstance() + { + $plugin = $this->getMockPlugin(); + $returnedPlugin = $this->handler->addPlugin($plugin); + $this->assertSame( + $returnedPlugin, + $plugin, + 'addPlugin() does not return the instance passed to it' + ); + } + + /** + * Tests adding a plugin to the handler using the plugin's short name. + * + * @return void + */ + public function testAddPluginByShortName() + { + $pluginName = 'Mock'; + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + + $returnedPlugin = $this->handler->addPlugin($pluginName); + $this->assertTrue($this->handler->hasPlugin($pluginName)); + + $this->assertType( + 'Phergie_Plugin_Mock', + $this->handler->getPlugin($pluginName) + ); + + $this->assertSame( + $this->handler->getPlugin($pluginName), + $returnedPlugin, + 'Handler does not contain added plugin' + ); + } + + + /** + * Tests adding a plugin instance to the handler. + * + * @return void + */ + public function testAddPluginByInstance() + { + $plugin = $this->getMockPlugin(); + $returnedPlugin = $this->handler->addPlugin($plugin); + $this->assertTrue($this->handler->hasPlugin('TestPlugin')); + + $this->assertSame( + $plugin, + $returnedPlugin, + 'addPlugin() does not return added plugin instance' + ); + + $this->assertSame( + $plugin, + $this->handler->getPlugin('TestPlugin'), + 'getPlugin() does not return added plugin instance' + ); + } + + /** + * Tests addPlugin() throwing an exception when the plugin class file + * can't be found. + * + * @return void + */ + public function testAddPluginThrowsExceptionWhenPluginFileNotFound() + { + try { + $this->handler->addPlugin('TestPlugin'); + } catch(Phergie_Plugin_Exception $e) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND, + $e->getCode() + ); + return; + } + + $this->fail('An expected exception has not been raised'); + } + + /** + * Recursively removes all files and subdirectories in a directory. + * + * @param string $path Directory path + * @return void + */ + private function removeDirectory($path) + { + if (file_exists($path)) { + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($it as $entry) { + if ($it->isDot()) { + continue; + } + if ($entry->isDir()) { + rmdir($entry->getPathname()); + } else { + unlink($entry->getPathname()); + } + } + } + } + + /** + * Tests addPlugin() throwing an exception when the plugin class file is + * found, but does not contain the plugin class as expected. + * + * @return void + */ + public function testAddPluginThrowsExceptionWhenPluginClassNotFound() + { + $path = sys_get_temp_dir() . '/Phergie/Plugin'; + $this->removeDirectory(dirname($path)); + mkdir($path, 0777, true); + touch($path . '/TestPlugin.php'); + $this->handler->addPath($path, 'Phergie_Plugin_'); + + try { + $this->handler->addPlugin('TestPlugin'); + } catch(Phergie_Plugin_Exception $e) { } + + if (isset($e)) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND, + $e->getCode() + ); + } else { + $this->fail('An expected exception has not been raised'); + } + + $this->removeDirectory(dirname($path)); + } + + /** + * Tests addPlugin() throwing an exception when trying to instantiate a + * class that doesn't extend Phergie_Plugin_Abstract. + * + * @return void + */ + public function testAddPluginThrowsExceptionIfRequestingNonPlugin() + { + try { + $this->handler->addPlugin('Handler'); + } catch(Phergie_Plugin_Exception $e) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_INCORRECT_BASE_CLASS, + $e->getCode() + ); + return; + } + + $this->fail('An expected exception has not been raised'); + } + + /** + * Tests addPlugin() throwing an exception when trying to instantiate a + * class that can't be instantiated. + * + * @return void + */ + public function testAddPluginThrowsExceptionIfPluginNotInstantiable() + { + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + try { + $this->handler->addPlugin('TestNonInstantiablePluginFromFile'); + } catch(Phergie_Plugin_Exception $e) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_CLASS_NOT_INSTANTIABLE, + $e->getCode() + ); + return; + } + + $this->fail('An expected exception has not been raised'); + } + + /** + * Tests adding a plugin by its short name with arguments passed to the + * plugin constructor. + * + * @return void + */ + public function testAddPluginShortNamePassesArgsToConstructor() + { + $pluginName = 'Mock'; + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + + $arguments = array('a', 'b', 'c'); + $plugin = $this->handler->addPlugin($pluginName, $arguments); + + $this->assertAttributeSame( + $arguments, + 'arguments', + $plugin, + 'Arguments do not match' + ); + } + + /** + * Tests addPlugin() passing Phergie_Config to an instantiated plugin. + * + * @return void + */ + public function testAddPluginPassesConstructorArguments() + { + $pluginName = 'Mock'; + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + $plugin = $this->handler->addPlugin($pluginName); + + $this->assertSame( + $this->config, + $plugin->getConfig(), + 'Phergie_Config instances do not match' + ); + + $this->assertSame( + $this->events, + $plugin->getEventHandler(), + 'Phergie_Event_Handler instances do not match' + ); + } + + /** + * Tests addPlugin() calling onLoad() on an instantiated plugin. + * + * @return void + */ + public function testAddPluginCallsOnLoadOnInstantiatedPlugin() + { + $plugin = $this->getMockPlugin(null, array('onLoad')); + $plugin + ->expects($this->once()) + ->method('onLoad'); + $this->handler->addPlugin($plugin); + } + + /** + * Tests addPlugin() returning the same plugin when called twice. + * + * @return void + */ + public function testAddPluginReturnsSamePluginWhenAskedTwice() + { + $pluginName = 'Mock'; + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + $plugin1 = $this->handler->addPlugin($pluginName); + $plugin2 = $this->handler->addPlugin($pluginName); + $this->assertSame($plugin1, $plugin2); + } + + /** + * Tests getPlugin() throwing an exception when trying to get an + * unloaded plugin with autoload disabled. + * + * @depends testGetAutoloadDefaultsToNotAutoload + * @return void + */ + public function testExceptionThrownWhenLoadingPluginWithoutAutoload() + { + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + + try { + $this->handler->getPlugin('Mock'); + } catch (Phergie_Plugin_Exception $expected) { + $this->assertEquals( + Phergie_Plugin_Exception::ERR_PLUGIN_NOT_LOADED, + $expected->getCode() + ); + return; + } + + $this->fail('An expected exception has not been raised'); + } + + /** + * Tests addPlugins() with a plugin short name and no plugin constructor + * arguments. + * + * @depends testAddPluginByShortName + * @depends testAddPluginByInstance + * @return void + */ + public function testAddPluginsWithoutArguments() + { + $prefix = 'Phergie_Plugin_'; + $this->handler->addPath(dirname(__FILE__), $prefix); + + $plugin = 'Mock'; + $this->handler->addPlugins(array($plugin)); + $returnedPlugin = $this->handler->getPlugin($plugin); + $this->assertContains( + get_class($returnedPlugin), + $prefix . $plugin, + 'Short name plugin not of expected class' + ); + } + + /** + * Tests addPlugins() with a plugin short name and plugin constructor + * arguments. + * + * @depends testAddPluginByShortName + * @depends testAddPluginByInstance + * @return void + */ + public function testAddPluginsWithArguments() + { + $prefix = 'Phergie_Plugin_'; + $this->handler->addPath(dirname(__FILE__), $prefix); + + $arguments = array(1, 2, 3); + $plugin = array('Mock', $arguments); + $this->handler->addPlugins(array($plugin)); + $returnedPlugin = $this->handler->getPlugin('Mock'); + $this->assertEquals( + $arguments, + $returnedPlugin->getArguments(), + 'Constructor arguments for instance plugin do not match' + ); + } + + /** + * Tests removePlugin() with a plugin instance. + * + * @depends testAddPluginByInstance + * @return void + */ + public function testRemovePluginByInstance() + { + $plugin = $this->getMockPlugin(); + $this->handler->addPlugin($plugin); + $this->handler->removePlugin($plugin); + $this->assertFalse( + $this->handler->hasPlugin($plugin->getName()), + 'Plugin was not removed' + ); + } + + /** + * Tests removePlugin() with a plugin short name. + * + * @depends testAddPluginByShortName + * @return void + */ + public function testRemovePluginByShortName() + { + $plugin = 'Mock'; + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + + $this->handler->addPlugin($plugin); + $this->handler->removePlugin($plugin); + $this->assertFalse( + $this->handler->hasPlugin($plugin), + 'Plugin was not removed' + ); + } + + /** + * Tests getPlugin() when the plugin is not already loaded and + * autoloading is disabled. + * + * @depends testSetAutoload + * @return void + */ + public function testGetPluginWithAutoloadEnabled() + { + $this->handler->setAutoload(true); + $this->handler->addPath(dirname(__FILE__), 'Phergie_Plugin_'); + $plugin = $this->handler->getPlugin('Mock'); + $this->assertType( + 'Phergie_Plugin_Mock', + $plugin, + 'Retrieved plugin not of expected class' + ); + } + + /** + * Tests getPlugins(). + * + * @depends testGetPluginWithAutoloadEnabled + * @return void + */ + public function testGetPlugins() + { + $plugin1 = $this->getMockPlugin('TestPlugin1'); + $this->handler->addPlugin($plugin1); + + $plugin2 = $this->getMockPlugin('TestPlugin2'); + $this->handler->addPlugin($plugin2); + + $expected = array( + 'testplugin1' => $plugin1, + 'testplugin2' => $plugin2, + ); + + $actual = $this->handler->getPlugins(); + $this->assertEquals($expected, $actual); + + $actual = $this->handler->getPlugins(array('testplugin1', 'testplugin2')); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that multiple plugin iterators can be used concurrently. + * + * @return void + */ + public function testUseMultiplePluginIteratorsConcurrently() + { + $plugin1 = $this->getMockPlugin('TestPlugin1'); + $this->handler->addPlugin($plugin1); + + $plugin2 = $this->getMockPlugin('TestPlugin2'); + $this->handler->addPlugin($plugin2); + + $iterator1 = $this->handler->getIterator(); + $iterator1->next(); + $this->assertSame($plugin2, $iterator1->current()); + + $iterator2 = $this->handler->getIterator(); + $this->assertSame($plugin1, $iterator2->current()); + } + + /** + * Tests adding plugin paths via configuration. + * + * @return void + */ + public function testAddPluginPathsViaConfiguration() + { + $dir = dirname(__FILE__); + $prefix = 'Phergie_Plugin_'; + $paths = array($dir => $prefix); + $this->config + ->expects($this->any()) + ->method('offsetExists') + ->will($this->returnValue(true)); + $this->config + ->expects($this->any()) + ->method('offsetGet') + ->will($this->returnValue($paths)); + + // Reinitialize the handler so the configuration change takes effect + // within the constructor + $this->handler = new Phergie_Plugin_Handler( + $this->config, + $this->events + ); + + $this->handler->setAutoload(true); + $this->handler->getPlugin('Mock'); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php new file mode 100644 index 000000000..336b25d35 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/IteratorTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Iterator. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_IteratorTest extends PHPUnit_Framework_TestCase +{ + /** + * Iterator instance being tested + * + * @var Phergie_Plugin_Iterator + */ + protected $iterator; + + /** + * List of mock plugin instances to be iterated + * + * @var array + */ + protected $plugins; + + /** + * Initializes the iterator instance being tested. + * + * @return void + */ + public function setUp() + { + $this->plugins = array(); + foreach (range(0, 4) as $index) { + $plugin = $this->getMock('Phergie_Plugin_Abstract'); + $plugin + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($index)); + $this->plugins[] = $plugin; + } + + $this->iterator = new Phergie_Plugin_Iterator( + new ArrayIterator($this->plugins) + ); + } + + /** + * Tests that all plugins are iterated when no filters are applied. + */ + public function testIteratesAllPluginsWithNoFilters() + { + $expected = range(0, 4); + $actual = array(); + foreach ($this->iterator as $plugin) { + $actual[] = $plugin->getName(); + } + $this->assertEquals($expected, $actual); + } + + /** + * Tests that appropriate plugins are iterated when plugin name filters + * are applied. + */ + public function testIteratesPluginsWithNameFilters() + { + // Test acceptance of strings and fluent interface implementation + $returned = $this->iterator->addPluginFilter('0'); + $this->assertSame($this->iterator, $returned); + + // Test acceptance of arrays + $this->iterator->addPluginFilter(array('1', '3')); + + // Test application of filters to iteration + $expected = array('2', '4'); + $actual = array(); + foreach ($this->iterator as $plugin) { + $actual[] = $plugin->getName(); + } + $this->assertEquals($expected, $actual); + } + + /** + * Tests that appropriate plugins are iterated when method name filters + * are applied. + * + * The same method name is used in all cases here because mocked methods + * of mock objects do not appear to be detected by method_exists() or + * ReflectionClass, so filtering by a method defined in the base plugin + * class seems the easiest way to test that method filtering really + * works. + */ + public function testIteratesPluginsWithMethodFilters() + { + // Tests acceptance of strings and fluent interface implementation + $returned = $this->iterator->addMethodFilter('getName'); + $this->assertSame($this->iterator, $returned); + + // Test acceptance of arrays + $this->iterator->addMethodFilter(array('getName', 'getName')); + + // Test application of filters to iteration + $expected = array(); + $actual = array(); + foreach ($this->iterator as $plugin) { + $actual[] = $plugin->getName(); + } + $this->assertEquals($expected, $actual); + } + + /** + * Tests that all plugins are iterated after filters are cleared. + * + * @depends testIteratesPluginsWithNameFilters + * @depends testIteratesPluginsWithMethodFilters + */ + public function testIteratesPluginsAfterClearingFilters() + { + $this->iterator->addPluginFilter('0'); + $this->iterator->addMethodFilter('method1'); + $this->iterator->clearFilters(); + + $expected = range(0, 4); + $actual = array(); + foreach ($this->iterator as $plugin) { + $actual[] = $plugin->getName(); + } + $this->assertEquals($expected, $actual); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php new file mode 100644 index 000000000..6b7231674 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php @@ -0,0 +1,335 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Karma. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_KarmaTest extends Phergie_Plugin_TestCase +{ + /** + * Skips tests if the SQLite PDO driver is not available. + * + * @return void + */ + public function setUp() + { + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->markTestSkipped('PDO or pdo_sqlite extension is required'); + } + + parent::setUp(); + } + + /** + * Configures the plugin to use a temporary copy of the database. + * + * @return PDO Connection to the temporary database + */ + private function createMockDatabase() + { + $dbPath = $this->getPluginsPath('Karma/karma.db'); + $db = $this->getMockDatabase($dbPath); + $this->plugin->setDb($db); + return $db; + } + + /** + * Tests the requirement of the Command plugin. + * + * @return void + */ + public function testRequiresCommandPlugin() + { + $this->assertRequiresPlugin('Command'); + $this->plugin->onLoad(); + } + + /** + * Initiates a karma event with a specified term. + * + * @param string $term Karma term + * + * @return Phergie_Event_Request Initiated mock event + */ + private function initiateKarmaEvent($term) + { + $args = array( + 'receiver' => $this->source, + 'text' => 'karma ' . $term + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + return $event; + } + + /** + * Checks for an expected karma response. + * + * @param Phergie_Event_Request $event Event containing the karma + * request + * @param string $term Karma term + * @param string $response Portion of the response + * message following the term + * from the original event + * + * @return void + */ + private function checkForKarmaResponse($event, $term, $response) + { + $text = $event->getNick() . ': ' . $response; + $this->assertEmitsEvent('privmsg', array($event->getSource(), $text)); + $this->plugin->onCommandKarma($term); + } + + /** + * Tests that a default database is used when none is specified. + * + * @return void + */ + public function testGetDb() + { + $db = $this->plugin->getDb(); + $this->assertType('PDO', $db); + } + + /** + * Tests specifying a custom database for the plugin to use. + * + * @return void + */ + public function testSetDb() + { + $db = $this->createMockDatabase(); + $this->assertSame($db, $this->plugin->getDb()); + } + + /** + * Tests that issuing the karma command with an unknown term returns a + * neutral rating. + * + * @return void + */ + public function testKarmaCommandOnUnknownTerm() + { + $term = 'foo'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has neutral karma.'); + } + + /** + * Tests that issuing the karma command with the term "me" returns the + * the karma rating for the initiating user. + * + * @return void + */ + public function testKarmaCommandOnUser() + { + $term = 'me'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, 'You have neutral karma.'); + } + + /** + * Tests that issuing the karma command with a term that has a fixed + * karma rating results in that rating being returned. + * + * @return void + */ + public function testKarmaCommandWithFixedKarmaTerm() + { + $term = 'phergie'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, 'phergie has karma of awesome.'); + } + + /** + * Supporting method that tests the result of a karma term rating change. + * + * @param string $term Karma term for which the rating is being + * changed + * @param string $operation ++ or -- + * @param int $karma Expected karma rating after the change is + * applied + */ + private function checkForKarmaRatingChange($term, $operation, $karma) + { + $args = array( + 'receiver' => $this->source, + 'text' => $term . $operation + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->plugin->onPrivmsg(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has karma of ' . $karma . '.'); + } + + /** + * Tests incrementing the karma rating of a new term. + * + * @return void + */ + public function testIncrementingKarmaRating() + { + $this->createMockDatabase(); + $this->checkForKarmaRatingChange('foo', '++', 1); + } + + /** + * Tests decrementing the karma rating of a new term. + * + * @return void + */ + public function testDecrementingKarmaRating() + { + $this->createMockDatabase(); + $this->checkForKarmaRatingChange('foo', '--', -1); + } + + /** + * Tests modifying the karma rating of an existing term. + * + * @return void + */ + public function testChangingExistingKarmaRating() + { + $term = 'foo'; + $this->createMockDatabase(); + $this->checkForKarmaRatingChange($term, '++', 1); + $this->checkForKarmaRatingChange($term, '++', 2); + } + + /** + * Tests resetting the karma rating of an existing term to 0. + * + * @return void + */ + public function testResettingExistingKarmaRating() + { + $term = 'foo'; + $this->createMockDatabase(); + $this->checkForKarmaRatingChange($term, '++', 1); + $this->plugin->onCommandReincarnate($term); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has neutral karma.'); + } + + /** + * Data provider for testKarmaComparisons(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameter values for a single call to + * testKarmaComparisons() + */ + public function dataProviderTestKarmaComparisons() + { + $term1 = 'foo'; + $term2 = 'bar'; + + $positive = 'True that.'; + $negative = 'No sir, not at all.'; + + return array( + array($term1, $term2, 1, 0, '>', $positive), + array($term1, $term2, 0, 1, '>', $negative), + array($term1, $term2, 1, 1, '>', $negative), + array($term1, $term2, 1, 0, '<', $negative), + array($term1, $term2, 0, 1, '<', $positive), + array($term1, $term2, 1, 1, '<', $negative), + array($term1, 'phergie', 1, 0, '>', $positive), + array('phergie', $term2, 0, 1, '<', $positive), + array($term1, 'everything', 0, 0, '>', $positive), + array('everything', $term2, 0, 0, '>', $positive), + ); + } + + /** + * Tests comparing the karma ratings of two terms. + * + * @param string $term1 First term + * @param string $term2 Second term + * @param int $karma1 Karma rating of the first time, 0 or 1 + * @param int $karma2 Karma rating of the second term, 0 or 1 + * @param string $operator Comparison operator, > or < + * @param string $response Response to check for + * + * @return void + * @dataProvider dataProviderTestKarmaComparisons + */ + public function testKarmaComparisons($term1, $term2, $karma1, $karma2, + $operator, $response + ) { + $db = $this->createMockDatabase(); + + // Reduce answer tables to expected response + $stmt = $db->prepare('DELETE FROM positive_answers WHERE answer != ?'); + $stmt->execute(array($response)); + $stmt = $db->prepare('DELETE FROM negative_answers WHERE answer != ?'); + $stmt->execute(array($response)); + + if ($karma1) { + $this->checkForKarmaRatingChange($term1, '++', 1); + } + + if ($karma2) { + $this->checkForKarmaRatingChange($term2, '++', 1); + } + + $args = array( + 'receiver' => $this->source, + 'text' => $term1 . ' ' . $operator . ' ' . $term2 + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + + // Test lack of a response for terms with fixed karma ratings + if ($term1 == 'phergie' || $term2 == 'phergie') { + $callback = 'assertDoesNotEmitEvent'; + } else { + $callback = 'assertEmitsEvent'; + } + + $this->$callback('privmsg', array($event->getSource(), $response)); + $this->plugin->onPrivmsg(); + + // Test for karma changes when one term is "everything" + if ($term1 == 'everything' || $term2 == 'everything') { + if ($term1 == 'everything') { + $term = $term2; + $karma = ($operator == '>') ? -1 : 1; + } else { + $term = $term1; + $karma = ($operator == '>') ? 1 : -1; + } + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has karma of ' . $karma . '.'); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php new file mode 100755 index 000000000..44a5d11c7 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/Mock.php @@ -0,0 +1,61 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Creates a plugin on the filesystem that can be used by + * Phergie_Plugin_Handler::addPath() to be located and loaded. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_Mock extends Phergie_Plugin_Abstract +{ + /** + * Arguments passed to the constructor + * + * @var array + */ + protected $arguments; + + /** + * Stores all arguments for later use. + * + * @return void + */ + public function __construct() + { + $this->arguments = func_get_args(); + } + + /** + * Returns all constructor arguments. + * + * @return array Enumerated array containing the arguments passed to the + * constructor in order + */ + public function getArguments() + { + return $this->arguments; + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php new file mode 100644 index 000000000..ac30d46ff --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php @@ -0,0 +1,164 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Ping. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_PingTest extends Phergie_Plugin_TestCase +{ + /** + * Tests that the last ping and event are initialized on connection to + * the server. + * + * @return void + */ + public function testOnConnect() + { + $this->plugin->onConnect(); + + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + $expected = null; + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that the last event is reset when an event occurs. + * + * @return void + */ + public function testPreEvent() + { + $this->plugin->preEvent(); + + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that the last ping is reset when a ping is received. + * + * @return void + */ + public function testOnPingResponse() + { + $this->plugin->onPingResponse(); + + $expected = null; + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that the test suite is able to manipulate the value of the last + * event. + * + * @return void + */ + public function testSetLastEvent() + { + $expected = time() + 1; + $this->plugin->setLastEvent($expected); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + $this->plugin->setLastEvent(); + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + try { + $this->plugin->setLastEvent('foo'); + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { } + } + + /** + * Tests that the test suite is able to manipulate the value of the last + * ping. + * + * @return void + */ + public function testSetLastPing() + { + $expected = time() + 1; + $this->plugin->setLastPing($expected); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + + $this->plugin->setLastPing(); + $expected = time(); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + + try { + $this->plugin->setLastPing('foo'); + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { } + } + + /** + * Tests that a ping event is sent after the appropriate time period has + * lapsed since receiving an event. + * + * @depends testSetLastEvent + * @return void + */ + public function testPing() + { + $pingEvent = 10; + $this->setConfig('ping.event', $pingEvent); + $lastEvent = time() - ($pingEvent + 1); + $this->plugin->setLastEvent($lastEvent); + $expected = time(); + $this->assertEmitsEvent('ping', array($this->nick, $expected)); + $this->plugin->onTick(); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + } + + /** + * Tests that a quit event is sent after the appropriate time period has + * lapsed since sending a ping event. + * + * @depends testPing + * @return void + */ + public function testQuit() + { + $pingPing = 10; + $this->setConfig('ping.ping', $pingPing); + $lastPing = time() - ($pingPing + 1); + $this->plugin->setLastPing($lastPing); + $this->assertEmitsEvent('quit'); + $this->plugin->onTick(); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php new file mode 100644 index 000000000..e8351fef2 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php @@ -0,0 +1,46 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Pong. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_PongTest extends Phergie_Plugin_TestCase +{ + /** + * Test that a pong event is sent when a ping event is received. + * + * @return void + */ + public function testPong() + { + $expected = 'irc.freenode.net'; + $event = $this->getMockEvent('ping', array($expected)); + $this->plugin->setEvent($event); + $this->assertEmitsEvent('pong', array($expected)); + $this->plugin->onPing(); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php new file mode 100644 index 000000000..369a0c644 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php @@ -0,0 +1,166 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_SpellCheck. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_SpellCheckTest extends Phergie_Plugin_TestCase +{ + /** + * Checks for the pspell extension. + * + * @return void + */ + public function setUp() + { + parent::setUp(); + + if (!extension_loaded('pspell')) { + $this->markTestSkipped('pspell extension not available'); + } + } + + /** + * Tests for the plugin failing to load when the language setting is not + * specified. + * + * @return void + */ + public function testLanguageSettingNotSet() + { + try { + $this->plugin->onLoad(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Tests for the plugin requiring the Command plugin as a dependency. + * + * @return void + */ + public function testRequiresCommandPlugin() + { + $this->setConfig('spellcheck.lang', 'en'); + $this->assertRequiresPlugin('Command'); + $this->plugin->onLoad(); + } + + /** + * Tests for the plugin failing to load because of a dictionary error. + * + * @return void + */ + public function testLoadDictionaryError() + { + $this->setConfig('spellcheck.lang', 'foo'); + try { + $this->plugin->onLoad(); + $this->fail('Expected exception not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Initializes a spell check event. + * + * @param string $word Word to be checked + * + * @return void + */ + private function initializeSpellCheckEvent($word) + { + $this->setConfig('spellcheck.lang', 'en'); + $this->plugin->onLoad(); + $args = array( + 'receiver' => $this->source, + 'text' => 'spell ' . $word + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + } + + /** + * Checks for a specified response to a spell check event. + * + * @param string $word Work being checked + * @param string $response Expected response + * + * @return void + */ + private function checkForSpellCheckResponse($word, $response) + { + $this->assertEmitsEvent('privmsg', array($this->source, $response)); + $this->plugin->onCommandSpell($word); + } + + /** + * Tests for the plugin returning a response for a correctly spelled word. + * + * @return void + */ + public function testRespondsForCorrectlySpelledWord() + { + $word = 'test'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': The word "' . $word . '" seems to be spelled correctly.'; + $this->checkForSpellCheckResponse($word, $response); + } + + /** + * Tests for the plugin returning a response when it can't find any + * suggestions for a word. + * + * @return void + */ + public function testRespondsWithoutSuggestions() + { + $word = 'kjlfljlkjljkljlj'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': I could not find any suggestions for "' . $word . '".'; + $this->checkForSpellCheckResponse($word, $response); + } + + /** + * Tests for the plugin returning a response when it is able to find + * suggestions for a word. + * + * @return void + */ + public function testRespondsWithSuggestions() + { + $word = 'teh'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': Suggestions for "' . $word . '": the, Te, tech, Th, eh.'; + $this->checkForSpellCheckResponse($word, $response); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php new file mode 100644 index 000000000..e58ac6f29 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_TerryChay. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_TerryChayTest extends Phergie_Plugin_TestCase +{ + /** + * Chayism used as a consistent response when related events are + * triggered + * + * @var string + */ + private $chayism = 'Terry Chay doesn\'t need a framework; he already knows everyone\'s code'; + + /** + * Configures the mock plugin handler to return a mock Http plugin with + * a mock response object populated with predetermined content. + * + * @return void + */ + public function setUpHttpClient() + { + $response = $this->getMock('Phergie_Plugin_Http_Response'); + $response + ->expects($this->any()) + ->method('getContent') + ->will($this->returnValue($this->chayism)); + + $plugin = $this->getMock('Phergie_Plugin_Http'); + $plugin + ->expects($this->any()) + ->method('get') + ->will($this->returnValue($response)); + + $this->getMockPluginHandler() + ->expects($this->any()) + ->method('getPlugin') + ->with('Http') + ->will($this->returnValue($plugin)); + } + + /** + * Tests that the plugin requires the Http plugin as a dependency. + * + * @return void + */ + public function testRequiresHttpPlugin() + { + $this->assertRequiresPlugin('Http'); + $this->plugin->onLoad(); + } + + /** + * Data provider for testPrivmsgTriggerReturnsChayism(). + * + * @return array Enumerated array of enumerated arrays each containing + * a set of parameters for a single call to + * testPrivmsgTriggerReturnsChayism() + */ + public function dataProviderTestPrivmsgTriggerReturnsChayism() + { + return array( + array('terry chay'), + array('terry chay'), + array('tychay'), + array('!tychay'), + array('! tychay'), + array('foo tychay bar'), + ); + } + + /** + * Tests that appropriate triggers result in a response with a Chayism. + * + * @return void + * @dataProvider dataProviderTestPrivmsgTriggerReturnsChayism + */ + public function testPrivmsgTriggerReturnsChayism($trigger) + { + $this->setConfig('command.prefix', '!'); + $this->setUpHttpClient(); + $args = array( + 'receiver' => $this->source, + 'text' => $trigger + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->assertEmitsEvent('privmsg', array($this->source, 'Fact: ' . $this->chayism)); + $this->plugin->onPrivmsg(); + } + + /** + * Tests that lack of an appropriate trigger results in no response with + * a Chayism. + * + * @return void + */ + public function testNoPrivmsgTriggerDoesNotReturnChayism() + { + $args = array( + 'receiver' => $this->source, + 'text' => 'foo bar baz' + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->assertDoesNotEmitEvent('privmsg', array($this->source, 'Fact: ' . $this->chayism)); + $this->plugin->onPrivmsg(); + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php new file mode 100644 index 000000000..941e7cb41 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php @@ -0,0 +1,435 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for plugin classes. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +abstract class Phergie_Plugin_TestCase extends PHPUnit_Framework_TestCase +{ + /** + * Mock configuration + * + * @var Phergie_Config + */ + protected $config; + + /** + * Associative array for configuration setting values, accessed by the + * mock configuration object using a callback + * + * @var array + */ + protected $settings = array(); + + /** + * Mock connection + * + * @var Phergie_Connection + */ + protected $connection; + + /** + * Mock event handler + * + * @var Phergie_Event_Handler + */ + protected $events; + + /** + * Mock plugin handler + * + * @var Phergie_Plugin_Handler + */ + protected $plugins; + + /** + * Plugin instance being tested + * + * @var Phergie_Plugin_Abstract + */ + protected $plugin; + + /** + * Full name of the plugin class being tested, may be explicitly + * specified in subclasses but is otherwise automatically derived from + * the test case class name + * + * @var string + */ + protected $pluginClass; + + /** + * User nick used in any events requiring one + * + * @var string + */ + protected $nick = 'nick'; + + /** + * Event source used in any events requiring one + * + * @var string + */ + protected $source = '#channel'; + + /** + * Initializes instance properties. + * + * @return void + */ + public function setUp() + { + if (empty($this->pluginClass)) { + $this->pluginClass = preg_replace('/Test$/', '', get_class($this)); + } + + if (empty($this->plugin)) { + $this->plugin = new $this->pluginClass; + } + + $this->plugin->setConfig($this->getMockConfig()); + $this->plugin->setConnection($this->getMockConnection()); + $this->plugin->setEventHandler($this->getMockEventHandler()); + $this->plugin->setPluginHandler($this->getMockPluginHandler()); + } + + /** + * Destroys all initialized instance properties. + * + * @return void + */ + public function tearDown() + { + unset( + $this->plugins, + $this->events, + $this->connection, + $this->config, + $this->plugin + ); + } + + /** + * Returns a mock configuration object. + * + * @return Phergie_Config + */ + protected function getMockConfig() + { + if (empty($this->config)) { + $this->config = $this->getMock('Phergie_Config', array('offsetExists', 'offsetGet')); + $this->config + ->expects($this->any()) + ->method('offsetExists') + ->will($this->returnCallback(array($this, 'configOffsetExists'))); + $this->config + ->expects($this->any()) + ->method('offsetGet') + ->will($this->returnCallback(array($this, 'configOffsetGet'))); + } + return $this->config; + } + + /** + * Returns whether a specific configuration setting has a value. Only + * intended for use by this class, but must be public for PHPUnit to + * call them. + * + * @param string $name Name of the setting + * + * @return boolean TRUE if the setting has a value, FALSE otherwise + */ + public function configOffsetExists($name) + { + return isset($this->settings[$name]); + } + + /** + * Returns the value of a specific configuration setting. Only intended + * for use by this class, but must be public for PHPUnit to call them. + * + * @param string $name Name of the setting + * + * @return mixed Value of the setting + */ + public function configOffsetGet($name) + { + return $this->settings[$name]; + } + + /** + * Returns a mock connection object. + * + * @return Phergie_Connection + */ + protected function getMockConnection() + { + if (empty($this->connection)) { + $this->connection = $this->getMock('Phergie_Connection'); + $this->connection + ->expects($this->any()) + ->method('getNick') + ->will($this->returnValue($this->nick)); + } + return $this->connection; + } + + /** + * Returns a mock event handler object. + * + * @return Phergie_Event_Handler + */ + protected function getMockEventHandler() + { + if (empty($this->events)) { + $this->events = $this->getMock('Phergie_Event_Handler', array('addEvent')); + } + return $this->events; + } + + /** + * Returns a mock plugin handler object. + * + * @return Phergie_Plugin_Handler + */ + protected function getMockPluginHandler() + { + if (empty($this->plugins)) { + $config = $this->getMockConfig(); + $events = $this->getMockEventHandler(); + $this->plugins = $this->getMock( + 'Phergie_Plugin_Handler', + array(), // mock everything + array($config, $events) + ); + } + return $this->plugins; + } + + /** + * Returns a mock event object. + * + * @param string $type Event type + * @param array $args Optional associative array of event arguments + * @param string $nick Optional user nick to associate with the event + * @param string $source Optional user nick or channel name to associate + * with the event as its source + * + * @return Phergie_Event_Request + */ + protected function getMockEvent($type, array $args = array(), + $nick = null, $source = null + ) { + $methods = array('getNick', 'getSource'); + foreach (array_keys($args) as $arg) { + if (is_int($arg) || ctype_digit($arg)) { + $methods[] = 'getArgument'; + } else { + $methods[] = 'get' . ucfirst($arg); + } + } + + $event = $this->getMock( + 'Phergie_Event_Request', + $methods + ); + + $nick = $nick ? $nick : $this->nick; + $event + ->expects($this->any()) + ->method('getNick') + ->will($this->returnValue($nick)); + + $source = $source ? $source : $this->source; + $event + ->expects($this->any()) + ->method('getSource') + ->will($this->returnValue($source)); + + foreach ($args as $key => $value) { + if (is_int($key) || ctype_digit($key)) { + $event + ->expects($this->any()) + ->method('getArgument') + ->with($key) + ->will($this->returnValue($value)); + } else { + $event + ->expects($this->any()) + ->method('get' . ucfirst($key)) + ->will($this->returnValue($value)); + } + } + + return $event; + } + + /** + * Sets the value of a configuration setting. + * + * @param string $setting Name of the setting + * @param mixed $value Value for the setting + * + * @return void + */ + protected function setConfig($setting, $value) + { + $this->settings[$setting] = $value; + } + + /** + * Returns the absolute path to the Phergie/Plugin directory. Useful in + * conjunction with getMockDatabase(). + * + * @param string $subpath Optional path to append to the directory path + * + * @return string Directory path + */ + protected function getPluginsPath($subpath = null) + { + $path = realpath(dirname(__FILE__) . '/../../../Phergie/Plugin'); + if (!empty($subpath)) { + $path .= '/' . ltrim($subpath, '/'); + } + return $path; + } + + /** + * Modifies the event handler to include an expectation of an event + * being added by the plugin being tested. Note that this must be called + * BEFORE executing the plugin code intended to initiate the event. + * + * @param string $type Event type + * @param array $args Optional enumerated array of event arguments + * + * @return void + */ + protected function assertEmitsEvent($type, array $args = array()) + { + $this->events + ->expects($this->at(0)) + ->method('addEvent') + ->with($this->plugin, $type, $args); + } + + /** + * Modifies the event handler to include an expectation of an event NOT + * being added by the plugin being tested. Note that this must be called + * BEFORE executing plugin code that may initiate the event. + * + * @param string $type Event type + * @param array $args Optional enumerated array of event arguments + * + * @return void + */ + protected function assertDoesNotEmitEvent($type, array $args = array()) + { + // Ugly hack to get around an issue in PHPUnit + // @link http://github.com/sebastianbergmann/phpunit-mock-objects/issues/issue/5#issue/5/comment/343524 + $callback = create_function( + '$plugin, $type, $args', + 'if (get_class($plugin) == "' . $this->pluginClass . '" + && $type == "' . $type . '" + && $args == "' . var_export($args, true) . '") { + trigger_error("Instance of ' . $this->pluginClass + . ' unexpectedly emitted event of type ' . $type + . '", E_USER_ERROR); + }' + ); + + $this->events + ->expects($this->any()) + ->method('addEvent') + ->will($this->returnCallback($callback)); + } + + /** + * Modifies the plugin handler to include an expectation of a plugin + * being retrieved, indicating a dependency. Note that this must be + * called BEFORE executing the plugin code that may load that plugin + * dependency, which is usually located in onLoad(). + * + * @param string $name Short name of the plugin required as a dependency + * + * @return void + */ + public function assertRequiresPlugin($name) + { + $this->plugins + ->expects($this->atLeastOnce()) + ->method('getPlugin') + ->with($name); + } + + /** + * Creates an in-memory copy of a specified SQLite database file and + * returns a connection to it. + * + * @param string $path Path to the SQLite file to copy + * + * @return PDO Connection to the database copy + */ + public function getMockDatabase($path) + { + $original = new PDO('sqlite:' . $path); + $copy = new PDO('sqlite::memory:'); + + $result = $original->query('SELECT sql FROM sqlite_master'); + while ($sql = $result->fetchColumn()) { + $copy->exec($sql); + } + + $tables = array(); + $result = $original->query('SELECT name FROM sqlite_master WHERE type = "table"'); + while ($table = $result->fetchColumn()) { + $tables[] = $table; + } + + foreach ($tables as $table) { + $result = $original->query('SELECT * FROM ' . $table); + $insert = null; + $copy->beginTransaction(); + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $columns = array_keys($row); + if (empty($insert)) { + $insert = $copy->prepare( + 'INSERT INTO "' . $table . '" (' . + '"' . implode('", "', $columns) . '"' . + ') VALUES (' . + ':' . implode(', :', $columns) . + ')' + ); + } + $insert->execute($row); + } + $copy->commit(); + unset($insert); + } + + return $copy; + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php new file mode 100755 index 000000000..f9bddd151 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestNonInstantiablePluginFromFile.php @@ -0,0 +1,43 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Creates a plugin on the filesystem that can be used by + * Phergie_Plugin_Handler's addPath utility to be located and loaded. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_TestNonInstantiablePluginFromFile +extends Phergie_Plugin_Abstract +{ + /** + * Private constructor to ensure that this class is not instantiable. + * + * @return void + */ + private function __construct() + { + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/TestHelper.php b/plugins/Irc/extlib/phergie/Tests/TestHelper.php new file mode 100644 index 000000000..e70af4456 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/TestHelper.php @@ -0,0 +1,26 @@ +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +error_reporting(E_ALL | E_STRICT); + +// Phergie components require Phergie_Autoload to function correctly. +require_once dirname(__FILE__) . '/../Phergie/Autoload.php'; +Phergie_Autoload::registerAutoloader(); diff --git a/plugins/Irc/extlib/phergie/Tests/phpunit.xml b/plugins/Irc/extlib/phergie/Tests/phpunit.xml new file mode 100644 index 000000000..b96589e4a --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/phpunit.xml @@ -0,0 +1,26 @@ +<!-- +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +--> +<phpunit colors="true" bootstrap="./TestHelper.php"> + <testsuite name="Phergie Test Suite"> + <directory>./</directory> + </testsuite> +</phpunit> diff --git a/plugins/Irc/extlib/phergie/build.xml b/plugins/Irc/extlib/phergie/build.xml new file mode 100644 index 000000000..7510c75a1 --- /dev/null +++ b/plugins/Irc/extlib/phergie/build.xml @@ -0,0 +1,301 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<project name="Phergie" default="core"> + + <tstamp> + <format property="DSTAMP" pattern="%Y-%m-%d" /> + </tstamp> + + <taskdef name="phergiepkg" classname="PhergiePackageTask" /> + <taskdef name="phpdocumentor" classname="phing.tasks.ext.phpdoc.PhpDocumentorTask" /> + + <available file="./build.properties" property="have_properties_file" /> + + <property name="clean" value="true" /> + + <target name="input"> + + <if> + <equals arg1="${have_properties_file}" arg2="true" /> + <then> + <property file="./build.properties" /> + </then> + <else> + <input propertyname="build.srcdir" defaultvalue="./" message="Source directory" /> + <input propertyname="build.dstdir" defaultvalue="./" message="Destination directory" /> + <input propertyname="build.version.release" message="Release version" /> + <input propertyname="build.version.api" message="API version" /> + <input propertyname="build.stability.release" defaultvalue="stable" message="Release stability" validArgs="snapshot,devel,alpha,beta,stable" /> + <input propertyname="build.stability.api" defaultvalue="stable" message="API stability" validArgs="snapshot,devel,alpha,beta,stable" /> + <input propertyname="build.notes" message="Release notes" /> + <input propertyname="build.phpdep" defaultvalue="5.2.0" message="PHP version required" /> + <input propertyname="build.pearinstallerdep" defaultvalue="1.9.0" message="PEAR installer version required" /> + </else> + </if> + + <fileset dir="${build.srcdir}" id="core"> + <include name="phergie.php" /> + <include name="phergie.bat" /> + <include name="LICENSE" /> + <include name="Settings.php.dist" /> + <include name="Phergie/Autoload.php" /> + <include name="Phergie/Bot.php" /> + <include name="Phergie/Config/Exception.php" /> + <include name="Phergie/Config.php" /> + <include name="Phergie/Connection/Exception.php" /> + <include name="Phergie/Connection/Handler.php" /> + <include name="Phergie/Connection.php" /> + <include name="Phergie/Db/Exception.php" /> + <include name="Phergie/Db/Manager.php" /> + <include name="Phergie/Db/Sqlite.php" /> + <include name="Phergie/Driver/Abstract.php" /> + <include name="Phergie/Driver/Exception.php" /> + <include name="Phergie/Driver/Streams.php" /> + <include name="Phergie/Event/Abstract.php" /> + <include name="Phergie/Event/Command.php" /> + <include name="Phergie/Event/Exception.php" /> + <include name="Phergie/Event/Handler.php" /> + <include name="Phergie/Event/Request.php" /> + <include name="Phergie/Event/Response.php" /> + <include name="Phergie/Exception.php" /> + <include name="Phergie/Hostmask/Exception.php" /> + <include name="Phergie/Hostmask.php" /> + <include name="Phergie/Plugin/Abstract.php" /> + <include name="Phergie/Plugin/Exception.php" /> + <include name="Phergie/Plugin/Handler.php" /> + <include name="Phergie/Ui/Abstract.php" /> + <include name="Phergie/Ui/Console.php" /> + </fileset> + + </target> + + <target name="core" depends="input"> + + <property name="build.tmpdir" value="Phergie-${build.version.release}" /> + <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" /> + + <delete file="${build.tarball}" quiet="true" /> + + <mkdir dir="${build.tmpdir}" /> + + <copy todir="${build.tmpdir}"> + <fileset refid="core" /> + </copy> + + <reflexive file="${build.tmpdir}/Phergie/Bot.php"> + <filterchain> + <replaceregexp> + <regexp + pattern="const VERSION = '[^']+';" + replace="const VERSION = '${build.version.release}';" + /> + </replaceregexp> + </filterchain> + </reflexive> + + <phergiepkg name="Phergie" dir="${build.tmpdir}"> + <fileset refid="core" /> + <option name="baseinstalldir" value="/" /> + <option name="outputdirectory" value="${build.dstdir}" /> + <option name="channel" value="pear.phergie.org" /> + <option name="summary" value="Phergie core library" /> + <option name="description" value="The Phergie package provides all files necessary to run a basic IRC bot." /> + <option name="apiversion" value="${build.version.api}" /> + <option name="apistability" value="${build.stability.api}" /> + <option name="releaseversion" value="${build.version.release}" /> + <option name="releasestability" value="${build.stability.release}" /> + <option name="phpdep" value="${build.phpdep}" /> + <option name="pearinstallerdep" value="${build.pearinstallerdep}" /> + <option name="license" value="http://phergie.org/license New BSD License" /> + <option name="packagetype" value="php" /> + <option name="notes" value="${build.notes}" /> + <mapping name="replacements"> + <element> + <element key="path" value="phergie.php" /> + <element key="type" value="pear-config" /> + <element key="from" value="/usr/bin/env php" /> + <element key="to" value="php_bin" /> + </element> + <element> + <element key="path" value="phergie.bat" /> + <element key="type" value="pear-config" /> + <element key="from" value="@php_bin@" /> + <element key="to" value="php_bin" /> + </element> + <element> + <element key="path" value="phergie.bat" /> + <element key="type" value="pear-config" /> + <element key="from" value="@bin_dir@" /> + <element key="to" value="bin_dir" /> + </element> + </mapping> + <mapping name="exceptions"> + <element key="phergie.php" value="script" /> + <element key="phergie.bat" value="script" /> + </mapping> + <mapping name="releases"> + <element> + <element key="installconditions"> + <element key="os" value="windows" /> + </element> + <element key="filelist"> + <element key="install"> + <element key="phergie.php" value="phergie" /> + </element> + </element> + </element> + <element> + <element key="filelist"> + <element key="install"> + <element key="phergie.php" value="phergie" /> + </element> + <element key="ignore"> + <element value="phergie.bat" /> + </element> + </element> + </element> + </mapping> + <mapping name="deps"> + <element> + <element key="type" value="ext" /> + <element key="name" value="pcre" /> + </element> + <element> + <element key="type" value="ext" /> + <element key="name" value="reflection" /> + </element> + </mapping> + </phergiepkg> + + <phingcall target="build" /> + + <phingcall target="clean" /> + + </target> + + <target name="plugin" depends="input"> + + <if> + <equals arg1="${have_properties_file}" arg2="true" /> + <then> + <property file="./build.properties" /> + </then> + <else> + <input propertyname="build.plugin" message="Short plugin name" /> + <input propertyname="build.summary" message="Plugin summary" /> + <input propertyname="build.description" message="Plugin description" /> + </else> + </if> + + <property name="build.class" value="Phergie_Plugin_${build.plugin}" /> + <property name="build.tmpdir" value="${build.class}-${build.version.release}" /> + <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" /> + + <fileset dir="${build.srcdir}" id="plugin"> + <include name="Phergie/Plugin/${build.plugin}.php" /> + <include name="Phergie/Plugin/${build.plugin}/**" /> + </fileset> + + <delete file="${build.tarball}" quiet="true" /> + + <mkdir dir="${build.tmpdir}" /> + + <copy todir="${build.tmpdir}"> + <fileset refid="plugin" /> + </copy> + + <phergiepkg name="${build.class}" dir="${build.tmpdir}"> + <fileset refid="plugin" /> + <option name="baseinstalldir" value="/" /> + <option name="outputdirectory" value="${build.dstdir}" /> + <option name="channel" value="pear.phergie.org"/> + <option name="summary" value="${build.summary}"/> + <option name="description" value="${build.description}"/> + <option name="apiversion" value="${build.version.api}"/> + <option name="apistability" value="${build.stability.api}"/> + <option name="releaseversion" value="${build.version.release}"/> + <option name="releasestability" value="${build.stability.release}"/> + <option name="phpdep" value="${build.phpdep}" /> + <option name="pearinstallerdep" value="${build.pearinstallerdep}" /> + <option name="license" value="http://phergie.org/license New BSD License"/> + <option name="packagetype" value="php"/> + <option name="notes" value="${build.notes}"/> + </phergiepkg> + + <phingcall target="build" /> + + <phingcall target="clean" /> + + </target> + + <target name="docs" depends="input"> + + <property name="build.tmpdir" value="Phergie_Docs-${build.version.release}" /> + <property name="build.tarball" value="${build.dstdir}${build.tmpdir}.tgz" /> + + <delete file="${build.tarball}" quiet="true" /> + + <mkdir dir="${build.tmpdir}" /> + + <phpdocumentor title="API Documentation" + destdir="${build.tmpdir}/api" + output="HTML:Smarty:PHP"> + <fileset refid="core" /> + <projdocfileset dir="."> + <include name="LICENSE" /> + </projdocfileset> + </phpdocumentor> + + <phergiepkg name="Phergie_Docs" dir="${build.tmpdir}"> + <fileset dir="${build.tmpdir}"> + <include name="api**" /> + </fileset> + <option name="baseinstalldir" value="/" /> + <option name="outputdirectory" value="${build.dstdir}" /> + <option name="channel" value="pear.phergie.org" /> + <option name="summary" value="Phergie core library documentation" /> + <option name="description" value="The Phergie_Docs package provides documentation for the Phergie core libraries." /> + <option name="apiversion" value="${build.version.api}" /> + <option name="apistability" value="${build.stability.api}" /> + <option name="releaseversion" value="${build.version.release}" /> + <option name="releasestability" value="${build.stability.release}" /> + <option name="phpdep" value="${build.phpdep}" /> + <option name="pearinstallerdep" value="${build.pearinstallerdep}" /> + <option name="license" value="http://phergie.org/license New BSD License" /> + <option name="packagetype" value="php" /> + <option name="notes" value="${build.notes}" /> + <mapping name="exceptions"> + <element key="api" value="doc" /> + </mapping> + </phergiepkg> + + <phingcall target="build" /> + + <phingcall target="clean" /> + + </target> + + <target name="build"> + + <tar destfile="${build.tarball}" compression="gzip"> + <fileset dir="${build.dstdir}"> + <include name="${build.tmpdir}**" /> + <include name="package.xml" /> + </fileset> + </tar> + + </target> + + <target name="clean"> + + <if> + <istrue value="${clean}" /> + <then> + <delete dir="${build.tmpdir}" /> + <delete file="${build.dstdir}package.xml" /> + </then> + </if> + + </target> + +</project> diff --git a/plugins/Irc/extlib/phergie/phergie.bat b/plugins/Irc/extlib/phergie/phergie.bat new file mode 100644 index 000000000..4eec11d5c --- /dev/null +++ b/plugins/Irc/extlib/phergie/phergie.bat @@ -0,0 +1,14 @@ +@echo off +REM Phergie +REM +REM PHP version 5 +REM +REM LICENSE +REM +REM This source file is subject to the new BSD license that is bundled +REM with this package in the file LICENSE. +REM It is also available through the world-wide-web at this URL: +REM http://phergie.org/license + +set PHPBIN="@php_bin@" +%PHPBIN% "@bin_dir@\phergie" %* diff --git a/plugins/Irc/extlib/phergie/phergie.php b/plugins/Irc/extlib/phergie/phergie.php new file mode 100755 index 000000000..f0b9f6ced --- /dev/null +++ b/plugins/Irc/extlib/phergie/phergie.php @@ -0,0 +1,54 @@ +#!/usr/bin/env php +<?php +/** + * Phergie + * + * PHP version 5 + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE. + * It is also available through the world-wide-web at this URL: + * http://phergie.org/license + * + * @category Phergie + * @package Phergie + * @author Phergie Development Team <team@phergie.org> + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie + */ + +/** + * @see Phergie_Autoload + */ +require 'Phergie/Autoload.php'; +Phergie_Autoload::registerAutoloader(); + +$bot = new Phergie_Bot; + +if (!isset($argc)) { + echo + 'The PHP setting register_argc_argv must be enabled for Phergie ', + 'configuration files to be specified using command line arguments; ', + 'defaulting to Settings.php in the current working directory', + PHP_EOL; +} else if ($argc > 0) { + // Skip the current file for manual installations + // ex: php phergie.php Settings.php + if (realpath($argv[0]) == __FILE__) { + array_shift($argv); + } + + // If configuration files were specified, override default behavior + if (count($argv) > 0) { + $config = new Phergie_Config; + foreach ($argv as $file) { + $config->read($file); + } + $bot->setConfig($config); + } +} + +$bot->run(); diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php new file mode 100644 index 000000000..606629331 --- /dev/null +++ b/plugins/Irc/ircmanager.php @@ -0,0 +1,357 @@ +<?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); } + +/** + * IRC background connection manager for IRC-using queue handlers, + * allowing them to send outgoing messages on the right connection. + * + * Input is handled during socket select loop, Any incoming messages will be handled. + * + * In a multi-site queuedaemon.php run, one connection will be instantiated + * for each site being handled by the current process that has IRC enabled. + */ + +class IrcManager extends ImManager { + protected $conn = null; + protected $lastPing = null; + protected $messageWaiting = true; + protected $lastMessage = null; + + protected $regChecks = array(); + protected $regChecksLookup = array(); + + protected $connected = false; + + /** + * Initialize connection to server. + * + * @return boolean true on success + */ + public function start($master) { + if (parent::start($master)) { + $this->connect(); + return true; + } else { + return false; + } + } + + /** + * Return any open sockets that the run loop should listen + * for input on. + * + * @return array Array of socket resources + */ + public function getSockets() { + $this->connect(); + if ($this->conn) { + return $this->conn->getSockets(); + } else { + return array(); + } + } + + /** + * Request a maximum timeout for listeners before the next idle period. + * + * @return integer Maximum timeout + */ + public function timeout() { + if ($this->messageWaiting) { + return 1; + } else { + return $this->plugin->pinginterval; + } + } + + /** + * Idle processing for io manager's execution loop. + * + * @return void + */ + public function idle() { + // Send a ping if necessary + if (empty($this->lastPing) || time() - $this->lastPing > $this->plugin->pinginterval) { + $this->sendPing(); + } + + if ($this->connected) { + // Send a waiting message if appropriate + if ($this->messageWaiting && time() - $this->lastMessage > 1) { + $wm = Irc_waiting_message::top(); + if ($wm === NULL) { + $this->messageWaiting = false; + return; + } + + $data = unserialize($wm->data); + $wm->incAttempts(); + + if ($this->send_raw_message($data)) { + $wm->delete(); + } else { + if ($wm->attempts <= common_config('queue', 'max_retries')) { + // Try again next idle + $wm->releaseClaim(); + } else { + // Exceeded the maximum number of retries + $wm->delete(); + } + } + } + } + } + + /** + * Process IRC events that have come in over the wire. + * + * @param resource $socket Socket to handle input on + * @return void + */ + public function handleInput($socket) { + common_log(LOG_DEBUG, 'Servicing the IRC queue.'); + $this->stats('irc_process'); + + try { + $this->conn->handleEvents(); + } catch (Phergie_Driver_Exception $e) { + $this->connected = false; + $this->conn->reconnect(); + } + } + + /** + * Initiate connection + * + * @return void + */ + public function connect() { + if (!$this->conn) { + $this->conn = new Phergie_StatusnetBot; + + $config = new Phergie_Config; + $config->readArray( + array( + 'connections' => array( + array( + 'host' => $this->plugin->host, + 'port' => $this->plugin->port, + 'username' => $this->plugin->username, + 'realname' => $this->plugin->realname, + 'nick' => $this->plugin->nick, + 'password' => $this->plugin->password, + 'transport' => $this->plugin->transporttype, + 'encoding' => $this->plugin->encoding + ) + ), + + 'driver' => 'statusnet', + + 'processor' => 'async', + 'processor.options' => array('sec' => 0, 'usec' => 0), + + 'plugins' => array( + 'Pong', + 'NickServ', + 'AutoJoin', + 'Statusnet', + ), + + 'plugins.autoload' => true, + + // Uncomment to enable debugging output + //'ui.enabled' => true, + + 'nickserv.password' => $this->plugin->nickservpassword, + 'nickserv.identify_message' => $this->plugin->nickservidentifyregexp, + + 'autojoin.channels' => $this->plugin->channels, + + 'statusnet.messagecallback' => array($this, 'handle_irc_message'), + 'statusnet.regcallback' => array($this, 'handle_reg_response'), + 'statusnet.connectedcallback' => array($this, 'handle_connected'), + 'statusnet.unregregexp' => $this->plugin->unregregexp, + 'statusnet.regregexp' => $this->plugin->regregexp + ) + ); + + $this->conn->setConfig($config); + $this->conn->connect(); + $this->lastPing = time(); + $this->lastMessage = time(); + } + return $this->conn; + } + + /** + * Called via a callback when a message is received + * Passes it back to the queuing system + * + * @param array $data Data + * @return boolean + */ + public function handle_irc_message($data) { + $this->plugin->enqueueIncomingRaw($data); + return true; + } + + /** + * Called via a callback when NickServ responds to + * the bots query asking if a nick is registered + * + * @param array $data Data + * @return void + */ + public function handle_reg_response($data) { + // Retrieve data + $screenname = $data['screenname']; + $nickdata = $this->regChecks[$screenname]; + $usernick = $nickdata['user']->nickname; + + if (isset($this->regChecksLookup[$usernick])) { + if ($data['registered']) { + // Send message + $this->plugin->sendConfirmationCode($screenname, $nickdata['code'], $nickdata['user'], true); + } else { + $this->plugin->sendMessage($screenname, _m('Your nickname is not registered so IRC connectivity cannot be enabled')); + + $confirm = new Confirm_address(); + + $confirm->user_id = $user->id; + $confirm->address_type = $this->plugin->transport; + + if ($confirm->find(true)) { + $result = $confirm->delete(); + + if (!$result) { + common_log_db_error($confirm, 'DELETE', __FILE__); + // TRANS: Server error thrown on database error canceling IM address confirmation. + $this->serverError(_('Couldn\'t delete confirmation.')); + return; + } + } + } + + // Unset lookup value + unset($this->regChecksLookup[$usernick]); + + // Unset data + unset($this->regChecks[$screename]); + } + } + + /** + * Called when the connection is established + * + * @return void + */ + public function handle_connected() { + $this->connected = true; + } + + /** + * Enters a message into the database for sending when ready + * + * @param string $command Command + * @param array $args Arguments + * @return boolean + */ + protected function enqueue_waiting_message($data) { + $wm = new Irc_waiting_message(); + + $wm->data = serialize($data); + $wm->prioritise = $data['prioritise']; + $wm->attempts = 0; + $wm->created = common_sql_now(); + $result = $wm->insert(); + + if (!$result) { + common_log_db_error($wm, 'INSERT', __FILE__); + throw new ServerException('DB error inserting IRC waiting queue item'); + } + + return true; + } + + /** + * Send a message using the daemon + * + * @param $data Message data + * @return boolean true on success + */ + public function send_raw_message($data) { + $this->connect(); + if (!$this->conn) { + return false; + } + + if ($data['type'] != 'delayedmessage') { + if ($data['type'] != 'message') { + // Nick checking + $nickdata = $data['nickdata']; + $usernick = $nickdata['user']->nickname; + $screenname = $nickdata['screenname']; + + // Cancel any existing checks for this user + if (isset($this->regChecksLookup[$usernick])) { + unset($this->regChecks[$this->regChecksLookup[$usernick]]); + } + + $this->regChecks[$screenname] = $nickdata; + $this->regChecksLookup[$usernick] = $screenname; + } + + // If there is a backlog or we need to wait, queue the message + if ($this->messageWaiting || time() - $this->lastMessage < 1) { + $this->enqueue_waiting_message( + array( + 'type' => 'delayedmessage', + 'prioritise' => $data['prioritise'], + 'data' => $data['data'] + ) + ); + $this->messageWaiting = true; + return true; + } + } + + try { + $this->conn->send($data['data']['command'], $data['data']['args']); + } catch (Phergie_Driver_Exception $e) { + $this->connected = false; + $this->conn->reconnect(); + return false; + } + + $this->lastMessage = time(); + return true; + } + + /** + * Sends a ping + * + * @return void + */ + protected function sendPing() { + $this->lastPing = time(); + $this->conn->send('PING', $this->lastPing); + } +} diff --git a/plugins/LdapCommon/LdapCommon.php b/plugins/LdapCommon/LdapCommon.php index 159b2d265..579fe4b64 100644 --- a/plugins/LdapCommon/LdapCommon.php +++ b/plugins/LdapCommon/LdapCommon.php @@ -126,11 +126,11 @@ class LdapCommon } throw new Exception('Could not connect to LDAP server: '.$err->getMessage()); } - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { $cacheObj = new MemcacheSchemaCache( array('c'=>$c, - 'cacheKey' => common_cache_key('ldap_schema:' . $config_id))); + 'cacheKey' => Cache::key('ldap_schema:' . $config_id))); $ldap->registerSchemaCache($cacheObj); } self::$ldap_connections[$config_id] = $ldap; diff --git a/plugins/LilUrl/LilUrlPlugin.php b/plugins/LilUrl/LilUrlPlugin.php index bd98026fe..b63cc8a55 100644 --- a/plugins/LilUrl/LilUrlPlugin.php +++ b/plugins/LilUrl/LilUrlPlugin.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php'; - class LilUrlPlugin extends UrlShortenerPlugin { public $serviceUrl; diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php index 020c0818a..d5b4e5052 100644 --- a/plugins/Mapstraction/MapstractionPlugin.php +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -125,7 +125,7 @@ class MapstractionPlugin extends Plugin urlencode($this->apikey))); break; case 'microsoft': - $action->script('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6'); + $action->script((StatusNet::isHTTPS()?'https':'http') + '://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6'); break; case 'openlayers': // Use our included stripped & minified OpenLayers. diff --git a/plugins/Minify/MinifyPlugin.php b/plugins/Minify/MinifyPlugin.php index b37531165..cfed0779b 100644 --- a/plugins/Minify/MinifyPlugin.php +++ b/plugins/Minify/MinifyPlugin.php @@ -118,9 +118,9 @@ class MinifyPlugin extends Plugin function onStartInlineScriptElement($action,&$code,&$type) { if($this->minifyInlineJs && $type=='text/javascript'){ - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cacheKey = common_cache_key(self::cacheKey . ':' . crc32($code)); + $cacheKey = Cache::key(self::cacheKey . ':' . crc32($code)); $out = $c->get($cacheKey); } if(empty($out)) { @@ -138,9 +138,9 @@ class MinifyPlugin extends Plugin function onStartStyleElement($action,&$code,&$type,&$media) { if($this->minifyInlineCss && $type=='text/css'){ - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cacheKey = common_cache_key(self::cacheKey . ':' . crc32($code)); + $cacheKey = Cache::key(self::cacheKey . ':' . crc32($code)); $out = $c->get($cacheKey); } if(empty($out)) { diff --git a/plugins/Minify/minify.php b/plugins/Minify/minify.php index 9a59c4223..e012a4027 100644 --- a/plugins/Minify/minify.php +++ b/plugins/Minify/minify.php @@ -74,9 +74,9 @@ class MinifyAction extends Action { parent::handle($args); - $c = common_memcache(); + $c = Cache::instance(); if (!empty($c)) { - $cacheKey = common_cache_key(MinifyPlugin::cacheKey . ':' . $this->file . '?v=' . empty($this->v)?'':$this->v); + $cacheKey = Cache::key(MinifyPlugin::cacheKey . ':' . $this->file . '?v=' . empty($this->v)?'':$this->v); $out = $c->get($cacheKey); } if(empty($out)) { diff --git a/plugins/Msn/MsnPlugin.php b/plugins/Msn/MsnPlugin.php new file mode 100644 index 000000000..187486eed --- /dev/null +++ b/plugins/Msn/MsnPlugin.php @@ -0,0 +1,216 @@ +<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * Send and receive notices using the MSN network
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category IM
+ * @package StatusNet
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+// We bundle the phpmsnclass library...
+set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpmsnclass');
+
+/**
+ * Plugin for MSN
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class MsnPlugin extends ImPlugin {
+ public $user = null;
+ public $password = null;
+ public $nickname = null;
+ public $transport = 'msn';
+
+ /**
+ * Get the internationalized/translated display name of this IM service
+ *
+ * @return string Name of service
+ */
+ public function getDisplayName() {
+ return _m('MSN');
+ }
+
+ /**
+ * Normalize a screenname for comparison
+ *
+ * @param string $screenname screenname to normalize
+ * @return string an equivalent screenname in normalized form
+ */
+ public function normalize($screenname) {
+ $screenname = str_replace(" ","", $screenname);
+ return strtolower($screenname);
+ }
+
+ /**
+ * Get the screenname of the daemon that sends and receives messages
+ *
+ * @return string Screenname
+ */
+ public function daemonScreenname() {
+ return $this->user;
+ }
+
+ /**
+ * Validate (ensure the validity of) a screenname
+ *
+ * @param string $screenname screenname to validate
+ * @return boolean
+ */
+ public function validate($screenname) {
+ return Validate::email($screenname, common_config('email', 'check_domain'));
+ }
+
+ /**
+ * Load related modules when needed
+ *
+ * @param string $cls Name of the class to be loaded
+ * @return boolean hook value; true means continue processing, false means stop.
+ */
+ public function onAutoload($cls) {
+ $dir = dirname(__FILE__);
+
+ switch ($cls) {
+ case 'MSN':
+ require_once(INSTALLDIR.'/plugins/Msn/extlib/phpmsnclass/msn.class.php');
+ return false;
+ case 'MsnManager':
+ case 'Msn_waiting_message':
+ include_once $dir . '/'.strtolower($cls).'.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /*
+ * Start manager on daemon start
+ *
+ * @return boolean
+ */
+ public function onStartImDaemonIoManagers(&$classes) {
+ parent::onStartImDaemonIoManagers(&$classes);
+ $classes[] = new MsnManager($this); // handles sending/receiving
+ return true;
+ }
+
+ /**
+ * Ensure the database table is present
+ *
+ */
+ public function onCheckSchema() {
+ $schema = Schema::get();
+
+ // For storing messages while sessions become ready
+ $schema->ensureTable('msn_waiting_message',
+ array(new ColumnDef('id', 'integer', null,
+ false, 'PRI', null, null, true),
+ new ColumnDef('screenname', 'varchar', 255, false),
+ new ColumnDef('message', 'text', null, false),
+ new ColumnDef('created', 'datetime', null, false),
+ new ColumnDef('claimed', 'datetime')));
+
+ return true;
+ }
+
+ /**
+ * Get a microid URI for the given screenname
+ *
+ * @param string $screenname
+ * @return string microid URI
+ */
+ public function microiduri($screenname) {
+ return 'msnim:' . $screenname;
+ }
+
+ /**
+ * Send a message to a given screenname
+ *
+ * @param string $screenname Screenname to send to
+ * @param string $body Text to send
+ * @return boolean success value
+ */
+ public function sendMessage($screenname, $body) {
+ $this->enqueueOutgoingRaw(array('to' => $screenname, 'message' => $body));
+ return true;
+ }
+
+ /**
+ * Accept a queued input message.
+ *
+ * @param array $data Data
+ * @return true if processing completed, false if message should be reprocessed
+ */
+ public function receiveRawMessage($data) {
+ $this->handleIncoming($data['sender'], $data['message']);
+ return true;
+ }
+
+ /**
+ * Initialize plugin
+ *
+ * @return boolean
+ */
+ public function initialize() {
+ if (!isset($this->user)) {
+ throw new Exception("Must specify a user");
+ }
+ if (!isset($this->password)) {
+ throw new Exception("Must specify a password");
+ }
+ if (!isset($this->nickname)) {
+ throw new Exception("Must specify a nickname");
+ }
+
+ return true;
+ }
+
+ /**
+ * Get plugin information
+ *
+ * @param array $versions array to insert information into
+ * @return void
+ */
+ public function onPluginVersion(&$versions) {
+ $versions[] = array(
+ 'name' => 'MSN',
+ 'version' => STATUSNET_VERSION,
+ 'author' => 'Luke Fitzgerald',
+ 'homepage' => 'http://status.net/wiki/Plugin:MSN',
+ 'rawdescription' =>
+ _m('The MSN plugin allows users to send and receive notices over the MSN network.')
+ );
+ return true;
+ }
+}
diff --git a/plugins/Msn/README b/plugins/Msn/README new file mode 100644 index 000000000..fa7bc2fb2 --- /dev/null +++ b/plugins/Msn/README @@ -0,0 +1,32 @@ +The MSN plugin allows users to send and receive notices over the MSN network. + +Required PHP extensions: +curl pcre mhash mcrypt bcmath + +Installation +============ +add "addPlugin('msn', + array('setting'=>'value', 'setting2'=>'value2', ...);" +to the bottom of your config.php + +scripts/imdaemon.php included with StatusNet must be running. It will be started by +the plugin along with their other daemons when you run scripts/startdaemons.sh. +See the StatusNet README for more about queuing and daemons. + +Settings +======== +user*: username (screenname) to use when logging into MSN +password*: password for that user +nickname*: nickname for the bot + +* required +default values are in (parenthesis) + +Example +======= +addPlugin('msn', array( + 'user' => '...', + 'password' => '...', + 'nickname' => '...' +)); + diff --git a/plugins/Msn/extlib/phpmsnclass/msn.class.php b/plugins/Msn/extlib/phpmsnclass/msn.class.php new file mode 100644 index 000000000..996c5571c --- /dev/null +++ b/plugins/Msn/extlib/phpmsnclass/msn.class.php @@ -0,0 +1,3210 @@ +<?php
+/*
+
+phpmsnclass ver 2.0s
+Luke Fitzgerald <lw.fitzgerald@googlemail.com>
+
+Based on MSN class ver 2.0 by Tommy Wu, Ricky Su
+License: GPL
+
+Documentation on the MSN protocol can be found at: http://msnpiki.msnfanatic.com/index.php/Main_Page
+
+This class uses MSNP15.
+
+In addition to PHP5, the additional php modules required are:
+curl pcre mcrypt bcmath
+
+*/
+
+class MSN {
+ const PROTOCOL = 'MSNP15';
+ const PASSPORT_URL = 'https://login.live.com/RST.srf';
+ const BUILDVER = '8.1.0178';
+ const PROD_KEY = 'PK}_A_0N_K%O?A9S';
+ const PROD_ID = 'PROD0114ES4Z%Q5W';
+ const LOGIN_METHOD = 'SSO';
+
+ const OIM_SEND_URL = 'https://ows.messenger.msn.com/OimWS/oim.asmx';
+ const OIM_SEND_SOAP = 'http://messenger.live.com/ws/2006/09/oim/Store2';
+
+ const OIM_MAILDATA_URL = 'https://rsi.hotmail.com/rsi/rsi.asmx';
+ const OIM_MAILDATA_SOAP = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/GetMetadata';
+ const OIM_READ_URL = 'https://rsi.hotmail.com/rsi/rsi.asmx';
+ const OIM_READ_SOAP = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/GetMessage';
+ const OIM_DEL_URL = 'https://rsi.hotmail.com/rsi/rsi.asmx';
+ const OIM_DEL_SOAP = 'http://www.hotmail.msn.com/ws/2004/09/oim/rsi/DeleteMessages';
+
+ const MEMBERSHIP_URL = 'https://contacts.msn.com/abservice/SharingService.asmx';
+ const MEMBERSHIP_SOAP = 'http://www.msn.com/webservices/AddressBook/FindMembership';
+
+ const ADDMEMBER_URL = 'https://contacts.msn.com/abservice/SharingService.asmx';
+ const ADDMEMBER_SOAP = 'http://www.msn.com/webservices/AddressBook/AddMember';
+
+ const DELMEMBER_URL = 'https://contacts.msn.com/abservice/SharingService.asmx';
+ const DELMEMBER_SOAP = 'http://www.msn.com/webservices/AddressBook/DeleteMember';
+
+ // the message length (include header) is limited (maybe since WLM 8.5 released)
+ // for WLM: 1664 bytes
+ // for YIM: 518 bytes
+ const MAX_MSN_MESSAGE_LEN = 1664;
+ const MAX_YAHOO_MESSAGE_LEN = 518;
+
+ private $debug;
+ private $timeout;
+
+ private $id;
+ private $ticket;
+ private $user = '';
+ private $password = '';
+ private $NSfp = false;
+ private $passport_policy = '';
+ private $alias;
+ private $psm;
+ private $retry_wait;
+ private $update_pending;
+ private $PhotoStickerFile = false;
+ private $Emotions = false;
+ private $XFRReqTimeout = 60;
+ private $SBStreamTimeout = 2;
+ private $MsnObjArray = array();
+ private $MsnObjMap = array();
+ private $ABAuthHeader;
+ private $ABService;
+ private $Contacts;
+
+ private $server = 'messenger.hotmail.com';
+ private $port = 1863;
+
+ private $clientid = '';
+
+ private $error = '';
+
+ private $authed = false;
+
+ private $oim_try = 3;
+
+ private $font_fn = 'Arial';
+ private $font_co = '333333';
+ private $font_ef = '';
+
+ // Begin added for StatusNet
+
+ private $aContactList = array();
+ private $aADL = array();
+
+ /**
+ * Holds session information indexed by screenname if
+ * session has no socket or socket if socket present
+ *
+ * @var array
+ */
+ private $switchBoardSessions = array();
+
+ /**
+ * Holds sockets indexed by screenname
+ *
+ * @var array
+ */
+ private $switchBoardSessionLookup = array();
+
+ /**
+ * Holds references to sessions waiting for XFR
+ *
+ * @var array
+ */
+ private $waitingForXFR = array();
+
+ /**
+ * Event Handler Functions
+ */
+ private $myEventHandlers = array();
+
+ // End added for StatusNet
+
+ /**
+ * Constructor method
+ *
+ * @param array $Configs Array of configuration options
+ * 'user' - Username
+ * 'password' - Password
+ * 'alias' - Bot nickname
+ * 'psm' - Bot personal status message
+ * 'retry_wait' - Time to wait before trying to reconnect
+ * 'update_pending' - Whether to update pending contacts
+ * 'PhotoSticker' - Photo file to use (?)
+ * 'debug' - Enable/Disable debugging mode
+ * @param integer $timeout Connection timeout
+ * @param integer $client_id Client id (hexadecimal)
+ * @return MSN
+ */
+ public function __construct ($Configs = array(), $timeout = 15, $client_id = 0x7000800C) {
+ $this->user = $Configs['user'];
+ $this->password = $Configs['password'];
+ $this->alias = isset($Configs['alias']) ? $Configs['alias'] : '';
+ $this->psm = isset($Configs['psm']) ? $Configs['psm'] : '';
+ $this->retry_wait = isset($Configs['retry_wait']) ? $Configs['retry_wait'] : 30;
+ $this->update_pending = isset($Configs['update_pending']) ? $Configs['update_pending'] : true;
+ $this->PhotoStickerFile=isset($Configs['PhotoSticker']) ? $Configs['PhotoSticker'] : false;
+
+ if ($this->Emotions = isset($Configs['Emotions']) ? $Configs['Emotions']:false) {
+ foreach($this->Emotions as $EmotionFilePath)
+ $this->MsnObj($EmotionFilePath,$Type=2);
+ }
+ $this->debug = isset($Configs['debug']) ? $Configs['debug'] : false;
+ $this->timeout = $timeout;
+
+ // Check support
+ if (!function_exists('curl_init')) throw new Exception("curl module not found!\n");
+ if (!function_exists('preg_match')) throw new Exception("pcre module not found!\n");
+ if (!function_exists('mcrypt_cbc')) throw new Exception("mcrypt module not found!\n");
+ if (!function_exists('bcmod')) throw new Exception("bcmath module not found!\n");
+
+ /*
+ http://msnpiki.msnfanatic.com/index.php/Client_ID
+ Client ID for MSN:
+ normal MSN 8.1 clientid is:
+ 01110110 01001100 11000000 00101100
+ = 0x764CC02C
+
+ we just use following:
+ * 0x04: Your client can send/receive Ink (GIF format)
+ * 0x08: Your client can send/recieve Ink (ISF format)
+ * 0x8000: This means you support Winks receiving (If not set the official Client will warn with 'contact has an older client and is not capable of receiving Winks')
+ * 0x70000000: This is the value for MSNC7 (WL Msgr 8.1)
+ = 0x7000800C;
+ */
+ $this->clientid = $client_id;
+ $this->ABService = new SoapClient(realpath(dirname(__FILE__)).'/soap/msnab_sharingservice.wsdl', array('trace' => 1));
+ }
+
+ /**
+ * Signon methods
+ */
+
+ /**
+ * Connect to the NS server
+ *
+ * @param String $user Username
+ * @param String $password Password
+ * @param String $redirect_server Redirect server
+ * @param Integer $redirect_port Redirect port
+ * @return Boolean Returns true if successful
+ */
+ private function connect($user, $password, $redirect_server = '', $redirect_port = 1863) {
+ $this->id = 1;
+ if ($redirect_server === '') {
+ $this->NSfp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout);
+ if (!$this->NSfp) {
+ $this->error = "!!! Could not connect to $this->server:$this->port, error => $errno, $errstr";
+ return false;
+ }
+ }
+ else {
+ $this->NSfp = @fsockopen($redirect_server, $redirect_port, $errno, $errstr, $this->timeout);
+ if (!$this->NSfp) {
+ $this->error = "!!! Could not connect to $redirect_server:$redirect_port, error => $errno, $errstr";
+ return false;
+ }
+ }
+ $this->authed = false;
+ // MSNP9
+ // NS: >> VER {id} MSNP9 CVR0
+ // MSNP15
+ // NS: >>> VER {id} MSNP15 CVR0
+ $this->ns_writeln("VER $this->id ".self::PROTOCOL.' CVR0');
+
+ $start_tm = time();
+ while (!self::socketcheck($this->NSfp)) {
+ $data = $this->ns_readln();
+ // no data?
+ if ($data === false) {
+ // logout now
+ // NS: >>> OUT
+ $this->ns_writeln("OUT");
+ @fclose($this->NSfp);
+ $this->error = 'Timeout, maybe protocol changed!';
+ return false;
+ }
+
+ $code = substr($data, 0, 3);
+ $start_tm = time();
+
+ switch ($code) {
+ case 'VER':
+ // MSNP9
+ // NS: <<< VER {id} MSNP9 CVR0
+ // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 6.0.0602 msmsgs {user}
+ // MSNP15
+ // NS: <<< VER {id} MSNP15 CVR0
+ // NS: >>> CVR {id} 0x0409 winnt 5.1 i386 MSMSGS 8.1.0178 msmsgs {user}
+ $this->ns_writeln("CVR $this->id 0x0409 winnt 5.1 i386 MSMSGS ".self::BUILDVER." msmsgs $user");
+ break;
+
+ case 'CVR':
+ // MSNP9
+ // NS: <<< CVR {id} {ver_list} {download_serve} ....
+ // NS: >>> USR {id} TWN I {user}
+ // MSNP15
+ // NS: <<< CVR {id} {ver_list} {download_serve} ....
+ // NS: >>> USR {id} SSO I {user}
+ $this->ns_writeln("USR $this->id ".self::LOGIN_METHOD." I $user");
+ break;
+
+ case 'USR':
+ // already login for passport site, finish the login process now.
+ // NS: <<< USR {id} OK {user} {verify} 0
+ if ($this->authed) return true;
+ // max. 16 digits for password
+ if (strlen($password) > 16)
+ $password = substr($password, 0, 16);
+
+ $this->user = $user;
+ $this->password = $password;
+ // NS: <<< USR {id} SSO S {policy} {nonce}
+ @list(/* USR */, /* id */, /* SSO */, /* S */, $policy, $nonce) = @explode(' ', $data);
+
+ $this->passport_policy = $policy;
+ $aTickets = $this->get_passport_ticket();
+ if (!$aTickets || !is_array($aTickets)) {
+ // logout now
+ // NS: >>> OUT
+ $this->ns_writeln("OUT");
+ @fclose($this->NSfp);
+ $this->error = 'Passport authentication failed!';
+ return false;
+ }
+
+ $ticket = $aTickets['ticket'];
+ $secret = $aTickets['secret'];
+ $this->ticket = $aTickets;
+ $login_code = $this->generateLoginBLOB($secret, $nonce);
+
+ // NS: >>> USR {id} SSO S {ticket} {login_code}
+ $this->ns_writeln("USR $this->id ".self::LOGIN_METHOD." S $ticket $login_code");
+ $this->authed = true;
+ break;
+
+ case 'XFR':
+ // main login server will redirect to anther NS after USR command
+ // MSNP9
+ // NS: <<< XFR {id} NS {server} 0 {server}
+ // MSNP15
+ // NS: <<< XFR {id} NS {server} U D
+ @list(/* XFR */, /* id */, $Type, $server) = @explode(' ', $data);
+ if ($Type!='NS') break;
+ @list($ip, $port) = @explode(':', $server);
+ // this connection will close after XFR
+ @fclose($this->NSfp);
+
+ $this->NSfp = @fsockopen($ip, $port, $errno, $errstr, $this->timeout);
+ if (!$this->NSfp) {
+ $this->error = "Can't connect to $ip:$port, error => $errno, $errstr";
+ return false;
+ }
+
+ // MSNP9
+ // NS: >> VER {id} MSNP9 CVR0
+ // MSNP15
+ // NS: >>> VER {id} MSNP15 CVR0
+ $this->ns_writeln("VER $this->id ".self::PROTOCOL.' CVR0');
+ break;
+
+ case 'GCF':
+ // return some policy data after 'USR {id} SSO I {user}' command
+ // NS: <<< GCF 0 {size}
+ @list(/* GCF */, /* 0 */, $size) = @explode(' ', $data);
+ // we don't need the data, just read it and drop
+ if (is_numeric($size) && $size > 0)
+ $this->ns_readdata($size);
+ break;
+
+ default:
+ // we'll quit if got any error
+ if (is_numeric($code)) {
+ // logout now
+ // NS: >>> OUT
+ $this->ns_writeln("OUT");
+ @fclose($this->NSfp);
+ $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
+ return false;
+ }
+ // unknown response from server, just ignore it
+ break;
+ }
+ }
+ // never goto here
+ }
+
+ /**
+ * Sign onto the NS server and retrieve the address book
+ *
+ * @return void
+ */
+ public function signon() {
+ /* FIXME Don't implement the signon as a loop or we could hang
+ * the queue handler! */
+ $this->debug_message('*** Trying to connect to MSN network');
+
+ // Remove any remaining switchboard sessions
+ $this->switchBoardSessions = array();
+ $this->switchBoardSessionLookup = array();
+
+ while (true) {
+ // Connect
+ if (!$this->connect($this->user, $this->password)) {
+ $this->signonFailure("!!! Could not connect to server: $this->error");
+ continue;
+ }
+
+ // Update contacts
+ if ($this->UpdateContacts() === false) {
+ $this->signonFailure('');
+ continue;
+ }
+
+ // Get membership lists
+ if (($this->aContactList = $this->getMembershipList()) === false) {
+ $this->signonFailure('!!! Get membership list failed');
+ continue;
+ }
+
+ if ($this->update_pending) {
+ if (is_array($this->aContactList)) {
+ $pending = 'Pending';
+ foreach ($this->aContactList as $u_domain => $aUserList) {
+ foreach ($aUserList as $u_name => $aNetworks) {
+ foreach ($aNetworks as $network => $aData) {
+ if (isset($aData[$pending])) {
+ // pending list
+ $cnt = 0;
+ foreach (array('Allow', 'Reverse') as $list) {
+ if (isset($aData[$list]))
+ $cnt++;
+ else {
+ if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) {
+ $this->aContactList[$u_domain][$u_name][$network][$list] = false;
+ $cnt++;
+ }
+ }
+ }
+ if ($cnt >= 2) {
+ $id = $aData[$pending];
+ // we can delete it from pending now
+ if ($this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $pending))
+ unset($this->aContactList[$u_domain][$u_name][$network][$pending]);
+ }
+ }
+ else {
+ // sync list
+ foreach (array('Allow', 'Reverse') as $list) {
+ if (!isset($aData[$list])) {
+ if ($this->addMemberToList($u_name.'@'.$u_domain, $network, $list))
+ $this->aContactList[$u_domain][$u_name][$network][$list] = false;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ $n = 0;
+ $sList = '';
+ $len = 0;
+ if (is_array($this->aContactList)) {
+ foreach ($this->aContactList as $u_domain => $aUserList) {
+ $str = '<d n="'.$u_domain.'">';
+ $len += strlen($str);
+ if ($len > 7400) {
+ $this->aADL[$n] = '<ml l="1">'.$sList.'</ml>';
+ $n++;
+ $sList = '';
+ $len = strlen($str);
+ }
+ $sList .= $str;
+ foreach ($aUserList as $u_name => $aNetworks) {
+ foreach ($aNetworks as $network => $status) {
+ $str = '<c n="'.$u_name.'" l="3" t="'.$network.'" />';
+ $len += strlen($str);
+ // max: 7500, but <ml l="1"></d></ml> is 19,
+ // so we use 7475
+ if ($len > 7475) {
+ $sList .= '</d>';
+ $this->aADL[$n] = '<ml l="1">'.$sList.'</ml>';
+ $n++;
+ $sList = '<d n="'.$u_domain.'">'.$str;
+ $len = strlen($sList);
+ }
+ else
+ $sList .= $str;
+ }
+ }
+ $sList .= '</d>';
+ }
+ }
+ $this->aADL[$n] = '<ml l="1">'.$sList.'</ml>';
+ // NS: >>> BLP {id} BL
+ $this->ns_writeln("BLP $this->id BL");
+ foreach ($this->aADL as $str) {
+ $len = strlen($str);
+ // NS: >>> ADL {id} {size}
+ $this->ns_writeln("ADL $this->id $len");
+ $this->ns_writedata($str);
+ }
+ // NS: >>> PRP {id} MFN name
+ if ($this->alias == '') $this->alias = $user;
+ $aliasname = rawurlencode($this->alias);
+ $this->ns_writeln("PRP $this->id MFN $aliasname");
+ //è¨å®šå€‹äººå¤§é è²¼
+ //$MsnObj=$this->PhotoStckObj();
+ // NS: >>> CHG {id} {status} {clientid} {msnobj}
+ $this->ns_writeln("CHG $this->id NLN $this->clientid");
+ if ($this->PhotoStickerFile !== false)
+ $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile)));
+ // NS: >>> UUX {id} length
+ $str = '<Data><PSM>'.htmlspecialchars($this->psm).'</PSM><CurrentMedia></CurrentMedia><MachineGuid></MachineGuid></Data>';
+ $len = strlen($str);
+ $this->ns_writeln("UUX $this->id $len");
+ $this->ns_writedata($str);
+ if (!self::socketcheck($this->NSfp)) {
+ $this->debug_message('*** Connected, waiting for commands');
+ break;
+ } else {
+ $this->NSRetryWait($this->retry_wait);
+ }
+ }
+ }
+
+ /**
+ * Called if there is an error during signon
+ *
+ * @param string $message Error message to log
+ * @return void
+ */
+ private function signonFailure($message) {
+ if(!empty($message)) {
+ $this->debug_message($message);
+ }
+ $this->callHandler('ConnectFailed', $message);
+ $this->NSRetryWait($this->retry_wait);
+ }
+
+ /**
+ * Log out and close the NS connection
+ *
+ * @return void
+ */
+ private function nsLogout() {
+ if (is_resource($this->NSfp) && !feof($this->NSfp)) {
+ // logout now
+ // NS: >>> OUT
+ $this->ns_writeln("OUT");
+ fclose($this->NSfp);
+ $this->NSfp = false;
+ $this->debug_message("*** Logged out");
+ }
+ }
+
+ /**
+ * NS and SB command handling methods
+ */
+
+ /**
+ * Read and handle incoming command from NS
+ *
+ * @return void
+ */
+ private function nsReceive() {
+ // Sign in again if not signed in or socket failed
+ if (!is_resource($this->NSfp) || self::socketcheck($this->NSfp)) {
+ $this->callHandler('Reconnect');
+ $this->NSRetryWait($this->retry_wait);
+ $this->signon();
+ return;
+ }
+
+ $data = $this->ns_readln();
+ if ($data === false) {
+ // There was no data / an error when reading from the socket so reconnect
+ $this->callHandler('Reconnect');
+ $this->NSRetryWait($this->retry_wait);
+ $this->signon();
+ return;
+ }
+
+ switch (substr($data, 0, 3)) {
+ case 'SBS':
+ // after 'USR {id} OK {user} {verify} 0' response, the server will send SBS and profile to us
+ // NS: <<< SBS 0 null
+ break;
+
+ case 'RFS':
+ // FIXME:
+ // NS: <<< RFS ???
+ // refresh ADL, so we re-send it again
+ if (is_array($this->aADL)) {
+ foreach ($this->aADL as $str) {
+ $len = strlen($str);
+ // NS: >>> ADL {id} {size}
+ $this->ns_writeln("ADL $this->id $len");
+ $this->ns_writedata($str);
+ }
+ }
+ break;
+
+ case 'LST':
+ // NS: <<< LST {email} {alias} 11 0
+ @list(/* LST */, $email) = @explode(' ', $data);
+ @list($u_name, $u_domain) = @explode('@', $email);
+ if (!isset($this->aContactList[$u_domain][$u_name][1])) {
+ $this->aContactList[$u_domain][$u_name][1]['Allow'] = 'Allow';
+ $this->debug_message("*** Added to contact list: $u_name@$u_domain");
+ }
+ break;
+
+ case 'ADL':
+ // randomly, we get ADL command, someone add us to their contact list for MSNP15
+ // NS: <<< ADL 0 {size}
+ @list(/* ADL */, /* 0 */, $size) = @explode(' ', $data);
+ if (is_numeric($size) && $size > 0) {
+ $data = $this->ns_readdata($size);
+ preg_match('#<ml><d n="([^"]+)"><c n="([^"]+)"(.*) t="(\d*)"(.*) /></d></ml>#', $data, $matches);
+ if (is_array($matches) && count($matches) > 0) {
+ $u_domain = $matches[1];
+ $u_name = $matches[2];
+ $network = $matches[4];
+ if (isset($this->aContactList[$u_domain][$u_name][$network]))
+ $this->debug_message("*** Someone (network: $network) added us to their list (but already in our list): $u_name@$u_domain");
+ else {
+ $re_login = false;
+ $cnt = 0;
+ foreach (array('Allow', 'Reverse') as $list) {
+ if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) {
+ if ($re_login) {
+ $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list");
+ continue;
+ }
+ $aTickets = $this->get_passport_ticket();
+ if (!$aTickets || !is_array($aTickets)) {
+ // failed to login? ignore it
+ $this->debug_message("*** Could not re-login, something wrong here");
+ $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list");
+ continue;
+ }
+ $re_login = true;
+ $this->ticket = $aTickets;
+ $this->debug_message("**** Got new ticket, trying again");
+ if (!$this->addMemberToList($u_name.'@'.$u_domain, $network, $list)) {
+ $this->debug_message("*** Could not add $u_name@$u_domain (network: $network) to $list list");
+ continue;
+ }
+ }
+ $this->aContactList[$u_domain][$u_name][$network][$list] = false;
+ $cnt++;
+ }
+ $this->debug_message("*** Someone (network: $network) added us to their list: $u_name@$u_domain");
+ }
+ $str = '<ml l="1"><d n="'.$u_domain.'"><c n="'.$u_name.'" l="3" t="'.$network.'" /></d></ml>';
+ $len = strlen($str);
+
+ $this->callHandler('AddedToList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network));
+ }
+ else
+ $this->debug_message("*** Someone added us to their list: $data");
+ }
+ break;
+
+ case 'RML':
+ // randomly, we get RML command, someome remove us to their contact list for MSNP15
+ // NS: <<< RML 0 {size}
+ @list(/* RML */, /* 0 */, $size) = @explode(' ', $data);
+ if (is_numeric($size) && $size > 0) {
+ $data = $this->ns_readdata($size);
+ preg_match('#<ml><d n="([^"]+)"><c n="([^"]+)"(.*) t="(\d*)"(.*) /></d></ml>#', $data, $matches);
+ if (is_array($matches) && count($matches) > 0) {
+ $u_domain = $matches[1];
+ $u_name = $matches[2];
+ $network = $matches[4];
+ if (isset($this->aContactList[$u_domain][$u_name][$network])) {
+ $aData = $this->aContactList[$u_domain][$u_name][$network];
+
+ foreach ($aData as $list => $id)
+ $this->delMemberFromList($id, $u_name.'@'.$u_domain, $network, $list);
+
+ unset($this->aContactList[$u_domain][$u_name][$network]);
+ $this->debug_message("*** Someone (network: $network) removed us from their list: $u_name@$u_domain");
+ }
+ else
+ $this->debug_message("*** Someone (network: $network) removed us from their list (but not in our list): $u_name@$u_domain");
+
+ $this->callHandler('RemovedFromList', array('screenname' => $u_name.'@'.$u_domain, 'network' => $network));
+ }
+ else
+ $this->debug_message("*** Someone removed us from their list: $data");
+ }
+ break;
+
+ case 'MSG':
+ // randomly, we get MSG notification from server
+ // NS: <<< MSG Hotmail Hotmail {size}
+ @list(/* MSG */, /* Hotmail */, /* Hotmail */, $size) = @explode(' ', $data);
+ if (is_numeric($size) && $size > 0) {
+ $data = $this->ns_readdata($size);
+ $aLines = @explode("\n", $data);
+ $header = true;
+ $ignore = false;
+ $maildata = '';
+ foreach ($aLines as $line) {
+ $line = rtrim($line);
+ if ($header) {
+ if ($line === '') {
+ $header = false;
+ continue;
+ }
+ if (strncasecmp($line, 'Content-Type:', 13) == 0) {
+ if (strpos($line, 'text/x-msmsgsinitialmdatanotification') === false && strpos($line, 'text/x-msmsgsoimnotification') === false) {
+ // we just need text/x-msmsgsinitialmdatanotification
+ // or text/x-msmsgsoimnotification
+ $ignore = true;
+ break;
+ }
+ }
+ continue;
+ }
+ if (strncasecmp($line, 'Mail-Data:', 10) == 0) {
+ $maildata = trim(substr($line, 10));
+ break;
+ }
+ }
+ if ($ignore) {
+ $this->debug_message("*** Ignoring MSG for: $line");
+ break;
+ }
+ if ($maildata == '') {
+ $this->debug_message("*** Ignoring MSG not for OIM");
+ break;
+ }
+ $re_login = false;
+ if (strcasecmp($maildata, 'too-large') == 0) {
+ $this->debug_message("*** Large mail-data, need to get the data via SOAP");
+ $maildata = $this->getOIM_maildata();
+ if ($maildata === false) {
+ $this->debug_message("*** Could not get mail-data via SOAP");
+
+ // maybe we need to re-login again
+ $aTickets = $this->get_passport_ticket();
+ if (!$aTickets || !is_array($aTickets)) {
+ // failed to login? ignore it
+ $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM");
+ break;
+ }
+ $re_login = true;
+ $this->ticket = $aTickets;
+ $this->debug_message("*** Got new ticket, trying again");
+ $maildata = $this->getOIM_maildata();
+ if ($maildata === false) {
+ $this->debug_message("*** Could not get mail-data via SOAP, and re-login already attempted, ignoring this OIM");
+ break;
+ }
+ }
+ }
+ // could be a lots of <M>...</M>, so we can't use preg_match here
+ $p = $maildata;
+ $aOIMs = array();
+ while (1) {
+ $start = strpos($p, '<M>');
+ $end = strpos($p, '</M>');
+ if ($start === false || $end === false || $start > $end) break;
+ $end += 4;
+ $sOIM = substr($p, $start, $end - $start);
+ $aOIMs[] = $sOIM;
+ $p = substr($p, $end);
+ }
+ if (count($aOIMs) == 0) {
+ $this->debug_message("*** Ignoring empty OIM");
+ break;
+ }
+ foreach ($aOIMs as $maildata) {
+ // T: 11 for MSN, 13 for Yahoo
+ // S: 6 for MSN, 7 for Yahoo
+ // RT: the datetime received by server
+ // RS: already read or not
+ // SZ: size of message
+ // E: sender
+ // I: msgid
+ // F: always 00000000-0000-0000-0000-000000000009
+ // N: sender alias
+ preg_match('#<T>(.*)</T>#', $maildata, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message("*** Ignoring OIM maildata without <T>type</T>");
+ continue;
+ }
+ $oim_type = $matches[1];
+ if ($oim_type = 13)
+ $network = 32;
+ else
+ $network = 1;
+ preg_match('#<E>(.*)</E>#', $maildata, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message("*** Ignoring OIM maildata without <E>sender</E>");
+ continue;
+ }
+ $oim_sender = $matches[1];
+ preg_match('#<I>(.*)</I>#', $maildata, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message("*** Ignoring OIM maildata without <I>msgid</I>");
+ continue;
+ }
+ $oim_msgid = $matches[1];
+ preg_match('#<SZ>(.*)</SZ>#', $maildata, $matches);
+ $oim_size = (count($matches) == 0) ? 0 : $matches[1];
+ preg_match('#<RT>(.*)</RT>#', $maildata, $matches);
+ $oim_time = (count($matches) == 0) ? 0 : $matches[1];
+ $this->debug_message("*** OIM received from $oim_sender, Time: $oim_time, MSGID: $oim_msgid, size: $oim_size");
+ $sMsg = $this->getOIM_message($oim_msgid);
+ if ($sMsg === false) {
+ $this->debug_message("*** Could not get OIM, msgid = $oim_msgid");
+ if ($re_login) {
+ $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM");
+ continue;
+ }
+ $aTickets = $this->get_passport_ticket();
+ if (!$aTickets || !is_array($aTickets)) {
+ // failed to login? ignore it
+ $this->debug_message("*** Could not re-login, something wrong here, ignoring this OIM");
+ continue;
+ }
+ $re_login = true;
+ $this->ticket = $aTickets;
+ $this->debug_message("*** get new ticket, try it again");
+ $sMsg = $this->getOIM_message($oim_msgid);
+ if ($sMsg === false) {
+ $this->debug_message("*** Could not get OIM via SOAP, and re-login already attempted, ignoring this OIM");
+ continue;
+ }
+ }
+ $this->debug_message("*** MSG (Offline) from $oim_sender (network: $network): $sMsg");
+ $this->callHandler('IMin', array('sender' => $oim_sender, 'message' => $sMsg, 'network' => $network, 'offline' => true));
+ }
+ }
+ break;
+
+ case 'UBM':
+ // randomly, we get UBM, this is the message from other network, like Yahoo!
+ // NS: <<< UBM {email} $network $type {size}
+ @list(/* UBM */, $from_email, $network, $type, $size) = @explode(' ', $data);
+ if (is_numeric($size) && $size > 0) {
+ $data = $this->ns_readdata($size);
+ $aLines = @explode("\n", $data);
+ $header = true;
+ $ignore = false;
+ $sMsg = '';
+ foreach ($aLines as $line) {
+ $line = rtrim($line);
+ if ($header) {
+ if ($line === '') {
+ $header = false;
+ continue;
+ }
+ if (strncasecmp($line, 'TypingUser:', 11) == 0) {
+ $ignore = true;
+ break;
+ }
+ continue;
+ }
+ $aSubLines = @explode("\r", $line);
+ foreach ($aSubLines as $str) {
+ if ($sMsg !== '')
+ $sMsg .= "\n";
+ $sMsg .= $str;
+ }
+ }
+ if ($ignore) {
+ $this->debug_message("*** Ignoring message from $from_email: $line");
+ break;
+ }
+ $this->debug_message("*** MSG from $from_email (network: $network): $sMsg");
+ $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => $network, 'offline' => false));
+ }
+ break;
+
+ case 'UBX':
+ // randomly, we get UBX notification from server
+ // NS: <<< UBX email {network} {size}
+ @list(/* UBX */, /* email */, /* network */, $size) = @explode(' ', $data);
+ // we don't need the notification data, so just ignore it
+ if (is_numeric($size) && $size > 0)
+ $this->ns_readdata($size);
+ break;
+
+ case 'CHL':
+ // randomly, we'll get challenge from server
+ // NS: <<< CHL 0 {code}
+ @list(/* CHL */, /* 0 */, $chl_code) = @explode(' ', $data);
+ $fingerprint = $this->getChallenge($chl_code);
+ // NS: >>> QRY {id} {product_id} 32
+ // NS: >>> fingerprint
+ $this->ns_writeln("QRY $this->id ".self::PROD_ID.' 32');
+ $this->ns_writedata($fingerprint);
+ $this->ns_writeln("CHG $this->id NLN $this->clientid");
+ if ($this->PhotoStickerFile !== false)
+ $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile)));
+ break;
+ case 'CHG':
+ // NS: <<< CHG {id} {status} {code}
+ // ignore it
+ // change our status to online first
+ break;
+
+ case 'XFR':
+ // sometimes, NS will redirect to another NS
+ // MSNP9
+ // NS: <<< XFR {id} NS {server} 0 {server}
+ // MSNP15
+ // NS: <<< XFR {id} NS {server} U D
+ // for normal switchboard XFR
+ // NS: <<< XFR {id} SB {server} CKI {cki} U messenger.msn.com 0
+ @list(/* XFR */, /* {id} */, $server_type, $server, /* CKI */, $cki_code) = @explode(' ', $data);
+ @list($ip, $port) = @explode(':', $server);
+ if ($server_type != 'SB') {
+ // maybe exit?
+ // this connection will close after XFR
+ $this->nsLogout();
+ continue;
+ }
+
+ $this->debug_message("NS: <<< XFR SB");
+ $session = array_shift($this->waitingForXFR);
+ $this->connectToSBSession('Active', $ip, $port, $session['to'], array('cki' => $cki_code));
+ break;
+ case 'QNG':
+ // NS: <<< QNG {time}
+ @list(/* QNG */, $ping_wait) = @explode(' ', $data);
+ $this->callHandler('Pong', $ping_wait);
+ break;
+
+ case 'RNG':
+ if ($this->PhotoStickerFile !== false)
+ $this->ns_writeln("CHG $this->id NLN $this->clientid ".rawurlencode($this->MsnObj($this->PhotoStickerFile)));
+ else
+ $this->ns_writeln("CHG $this->id NLN $this->clientid");
+ // someone is trying to talk to us
+ // NS: <<< RNG {session_id} {server} {auth_type} {ticket} {email} {alias} U {client} 0
+ $this->debug_message("NS: <<< RNG $data");
+ @list(/* RNG */, $sid, $server, /* auth_type */, $ticket, $email, $name) = @explode(' ', $data);
+ @list($sb_ip, $sb_port) = @explode(':', $server);
+ $this->debug_message("*** RING from $email, $sb_ip:$sb_port");
+ $this->addContact($email, 1, $email, true);
+ $this->connectToSBSession('Passive', $sb_ip, $sb_port, $email, array('sid' => $sid, 'ticket' => $ticket));
+ break;
+
+ case 'NLN':
+ // NS: <<< NLN {status} {email} {networkid} {nickname} {clientid} {dpobj}
+ @list(/* NLN */, $status, $email, $network, $nickname) = @explode(' ', $data);
+ $this->callHandler('StatusChange', array('screenname' => $email, 'status' => $status, 'network' => $network, 'nickname' => $nickname));
+ break;
+
+ case 'OUT':
+ // force logout from NS
+ // NS: <<< OUT xxx
+ $this->debug_message("*** LOGOUT from NS");
+ return $this->nsLogout();
+
+ default:
+ $code = substr($data,0,3);
+ if (is_numeric($code)) {
+ $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
+ $this->debug_message("*** NS: $this->error");
+
+ return $this->nsLogout();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Read and handle incoming command/message from
+ * a switchboard session socket
+ */
+ private function sbReceive($socket) {
+ $intsocket = (int) $socket;
+ $session = &$this->switchBoardSessions[$intsocket];
+
+ if (feof($socket)) {
+ // Unset session lookup value
+ unset($this->switchBoardSessionLookup[$session['to']]);
+
+ // Unset session itself
+ unset($this->switchBoardSessions[$intsocket]);
+ return;
+ }
+
+ $id = &$session['id'];
+
+ $data = $this->sb_readln($socket);
+ $code = substr($data, 0, 3);
+ switch($code) {
+ case 'IRO':
+ // SB: <<< IRO {id} {rooster} {roostercount} {email} {alias} {clientid}
+ @list(/* IRO */, /* id */, $cur_num, $total, $email, $alias, $clientid) = @explode(' ', $data);
+ $this->debug_message("*** $email joined session");
+ if ($email == $session['to']) {
+ $session['joined'] = true;
+ $this->callHandler('SessionReady', array('to' => $email));
+ }
+ break;
+ case 'BYE':
+ $this->debug_message("*** Quit for BYE");
+ $this->endSBSession($socket);
+ break;
+ case 'USR':
+ // SB: <<< USR {id} OK {user} {alias}
+ // we don't need the data, just ignore it
+ // request user to join this switchboard
+ // SB: >>> CAL {id} {user}
+ $this->sb_writeln($socket, $id, "CAL $id ".$session['to']);
+ break;
+ case 'CAL':
+ // SB: <<< CAL {id} RINGING {?}
+ // we don't need this, just ignore, and wait for other response
+ $session['id']++;
+ break;
+ case 'JOI':
+ // SB: <<< JOI {user} {alias} {clientid?}
+ // someone join us
+ @list(/* JOI */, $email) = @explode(' ', $data);
+ if ($email == $session['to']) {
+ $session['joined'] = true;
+ $this->callHandler('SessionReady', array('to' => $email));
+ }
+ break;
+ case 'MSG':
+ // SB: <<< MSG {email} {alias} {len}
+ @list(/* MSG */, $from_email, /* alias */, $len) = @explode(' ', $data);
+ $len = trim($len);
+ $data = $this->sb_readdata($socket, $len);
+ $aLines = @explode("\n", $data);
+ $header = true;
+ $ignore = false;
+ $is_p2p = false;
+ $sMsg = '';
+ foreach ($aLines as $line) {
+ $line = rtrim($line);
+ if ($header) {
+ if ($line === '') {
+ $header = false;
+ continue;
+ }
+ if (strncasecmp($line, 'TypingUser:', 11) == 0) {
+ // typing notification, just ignore
+ $ignore = true;
+ break;
+ }
+ if (strncasecmp($line, 'Chunk:', 6) == 0) {
+ // we don't handle any split message, just ignore
+ $ignore = true;
+ break;
+ }
+ if (strncasecmp($line, 'Content-Type: application/x-msnmsgrp2p', 38) == 0) {
+ // p2p message, ignore it, but we need to send acknowledgement for it...
+ $is_p2p = true;
+ $p = strstr($data, "\n\n");
+ $sMsg = '';
+ if ($p === false) {
+ $p = strstr($data, "\r\n\r\n");
+ if ($p !== false)
+ $sMsg = substr($p, 4);
+ }
+ else
+ $sMsg = substr($p, 2);
+ break;
+ }
+ if (strncasecmp($line, 'Content-Type: application/x-', 28) == 0) {
+ // ignore all application/x-... message
+ // for example:
+ // application/x-ms-ink => ink message
+ $ignore = true;
+ break;
+ }
+ if (strncasecmp($line, 'Content-Type: text/x-', 21) == 0) {
+ // ignore all text/x-... message
+ // for example:
+ // text/x-msnmsgr-datacast => nudge, voice clip....
+ // text/x-mms-animemoticon => customized animemotion word
+ $ignore = true;
+ break;
+ }
+ continue;
+ }
+ if ($sMsg !== '')
+ $sMsg .= "\n";
+ $sMsg .= $line;
+ }
+ if ($ignore) {
+ $this->debug_message("*** Ignoring SB data from $from_email: $line");
+ break;
+ }
+ if ($is_p2p) {
+ // we will ignore any p2p message after sending acknowledgement
+ $ignore = true;
+ $len = strlen($sMsg);
+ $this->debug_message("*** p2p message from $from_email, size $len");
+ // header = 48 bytes
+ // content >= 0 bytes
+ // footer = 4 bytes
+ // so it need to >= 52 bytes
+ /*if ($len < 52) {
+ $this->debug_message("*** p2p: size error, less than 52!");
+ break;
+ }*/
+ $aDwords = @unpack("V12dword", $sMsg);
+ if (!is_array($aDwords)) {
+ $this->debug_message("*** p2p: header unpack error!");
+ break;
+ }
+ $this->debug_message("*** p2p: dump received message:\n".$this->dump_binary($sMsg));
+ $hdr_SessionID = $aDwords['dword1'];
+ $hdr_Identifier = $aDwords['dword2'];
+ $hdr_DataOffsetLow = $aDwords['dword3'];
+ $hdr_DataOffsetHigh = $aDwords['dword4'];
+ $hdr_TotalDataSizeLow = $aDwords['dword5'];
+ $hdr_TotalDataSizeHigh = $aDwords['dword6'];
+ $hdr_MessageLength = $aDwords['dword7'];
+ $hdr_Flag = $aDwords['dword8'];
+ $hdr_AckID = $aDwords['dword9'];
+ $hdr_AckUID = $aDwords['dword10'];
+ $hdr_AckSizeLow = $aDwords['dword11'];
+ $hdr_AckSizeHigh = $aDwords['dword12'];
+ $this->debug_message("*** p2p: header SessionID = $hdr_SessionID");
+ $this->debug_message("*** p2p: header Inentifier = $hdr_Identifier");
+ $this->debug_message("*** p2p: header Data Offset Low = $hdr_DataOffsetLow");
+ $this->debug_message("*** p2p: header Data Offset High = $hdr_DataOffsetHigh");
+ $this->debug_message("*** p2p: header Total Data Size Low = $hdr_TotalDataSizeLow");
+ $this->debug_message("*** p2p: header Total Data Size High = $hdr_TotalDataSizeHigh");
+ $this->debug_message("*** p2p: header MessageLength = $hdr_MessageLength");
+ $this->debug_message("*** p2p: header Flag = $hdr_Flag");
+ $this->debug_message("*** p2p: header AckID = $hdr_AckID");
+ $this->debug_message("*** p2p: header AckUID = $hdr_AckUID");
+ $this->debug_message("*** p2p: header AckSize Low = $hdr_AckSizeLow");
+ $this->debug_message("*** p2p: header AckSize High = $hdr_AckSizeHigh");
+ if ($hdr_Flag == 2) {
+ //This is an ACK from SB ignore....
+ $this->debug_message("*** p2p: //This is an ACK from SB ignore....:\n");
+ break;
+ }
+ $MsgBody = $this->linetoArray(substr($sMsg, 48, -4));
+ $this->debug_message("*** p2p: body".print_r($MsgBody, true));
+ if (($MsgBody['EUF-GUID']=='{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}')&&($PictureFilePath=$this->GetPictureFilePath($MsgBody['Context']))) {
+ while (true) {
+ if ($this->sb_readln($socket) === false) break;
+ }
+ $this->debug_message("*** p2p: Inv hdr:\n".$this->dump_binary(substr($sMsg, 0, 48)));
+ preg_match('/{([0-9A-F\-]*)}/i', $MsgBody['Via'], $Matches);
+ $BranchGUID = $Matches[1];
+ //it's an invite to send a display picture.
+ $new_id = ~$hdr_Identifier;
+ $hdr = pack(
+ "LLLLLLLLLLLL", $hdr_SessionID,
+ $new_id,
+ 0, 0,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh,
+ 0,
+ 2,
+ $hdr_Identifier,
+ $hdr_AckID,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh
+ );
+ $footer = pack("L", 0);
+ $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer";
+ $len = strlen($message);
+ $this->sb_writeln($socket, $id, "MSG $id D $len");
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: send display picture acknowledgement for $hdr_SessionID");
+ $this->debug_message("*** p2p: Invite ACK message:\n".$this->dump_binary($message));
+ $this->sb_readln($socket); // Read ACK;
+ $this->debug_message("*** p2p: Invite ACK Hdr:\n".$this->dump_binary($hdr));
+ $new_id -= 3;
+ //Send 200 OK message
+ $MessageContent="SessionID: ".$MsgBody['SessionID']."\r\n\r\n".pack("C", 0);
+ $MessagePayload=
+ "MSNSLP/1.0 200 OK\r\n".
+ "To: <msnmsgr:".$from_email.">\r\n".
+ "From: <msnmsgr:".$this->user.">\r\n".
+ "Via: ".$MsgBody['Via']."\r\n".
+ "CSeq: ".($MsgBody['CSeq']+1)."\r\n".
+ "Call-ID: ".$MsgBody['Call-ID']."\r\n".
+ "Max-Forwards: 0\r\n".
+ "Content-Type: application/x-msnmsgr-sessionreqbody\r\n".
+ "Content-Length: ".strlen($MessageContent)."\r\n\r\n".
+ $MessageContent;
+ $hdr_TotalDataSizeLow=strlen($MessagePayload);
+ $hdr_TotalDataSizeHigh=0;
+ $hdr = pack(
+ "LLLLLLLLLLLL", $hdr_SessionID,
+ $new_id,
+ 0, 0,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh,
+ strlen($MessagePayload),
+ 0,
+ rand(),
+ 0,
+ 0, 0
+ );
+
+ $message =
+ "MIME-Version: 1.0\r\n".
+ "Content-Type: application/x-msnmsgrp2p\r\n".
+ "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer";
+ $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message));
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: dump 200 ok message:\n".$this->dump_binary($message));
+ $this->sb_readln($socket); // Read ACK;
+
+ $this->debug_message("*** p2p: 200 ok:\n".$this->dump_binary($hdr));
+ // send data preparation message
+ // send 4 null bytes as data
+ $hdr_TotalDataSizeLow = 4;
+ $hdr_TotalDataSizeHigh = 0 ;
+ $new_id++;
+ $hdr = pack(
+ "LLLLLLLLLLLL",
+ $MsgBody['SessionID'],
+ $new_id,
+ 0, 0,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh,
+ $hdr_TotalDataSizeLow,
+ 0,
+ rand(),
+ 0,
+ 0, 0
+ );
+ $message =
+ "MIME-Version: 1.0\r\n".
+ "Content-Type: application/x-msnmsgrp2p\r\n".
+ "P2P-Dest: $from_email\r\n\r\n$hdr".pack('L', 0)."$footer";
+ $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message));
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: dump send Data preparation message:\n".$this->dump_binary($message));
+ $this->debug_message("*** p2p: Data Prepare Hdr:\n".$this->dump_binary($hdr));
+ $this->sb_readln($socket); // Read ACK;
+
+ // send Data Content..
+ $footer=pack('N',1);
+ $new_id++;
+ $FileSize=filesize($PictureFilePath);
+ if ($hTitle=fopen($PictureFilePath,'rb')) {
+ $Offset = 0;
+ //$new_id++;
+ while (!feof($hTitle)) {
+ $FileContent = fread($hTitle, 1024);
+ $FileContentSize = strlen($FileContent);
+ $hdr = pack(
+ "LLLLLLLLLLLL",
+ $MsgBody['SessionID'],
+ $new_id,
+ $Offset, 0,
+ $FileSize, 0,
+ $FileContentSize,
+ 0x20,
+ rand(),
+ 0,
+ 0, 0
+ );
+ $message =
+ "MIME-Version: 1.0\r\n".
+ "Content-Type: application/x-msnmsgrp2p\r\n".
+ "P2P-Dest: $from_email\r\n\r\n$hdr$FileContent$footer";
+ $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message));
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: dump send Data Content message $Offset / $FileSize :\n".$this->dump_binary($message));
+ $this->debug_message("*** p2p: Data Content Hdr:\n".$this->dump_binary($hdr));
+ //$this->SB_readln($socket);//Read ACK;
+ $Offset += $FileContentSize;
+ }
+ }
+ //Send Bye
+ /*
+ $MessageContent="\r\n".pack("C", 0);
+ $MessagePayload=
+ "BYE MSNMSGR:MSNSLP/1.0\r\n".
+ "To: <msnmsgr:$from_email>\r\n".
+ "From: <msnmsgr:".$this->user.">\r\n".
+ "Via: MSNSLP/1.0/TLP ;branch={".$BranchGUID."}\r\n".
+ "CSeq: 0\r\n".
+ "Call-ID: ".$MsgBody['Call-ID']."\r\n".
+ "Max-Forwards: 0\r\n".
+ "Content-Type: application/x-msnmsgr-sessionclosebody\r\n".
+ "Content-Length: ".strlen($MessageContent)."\r\n\r\n".$MessageContent;
+ $footer=pack('N',0);
+ $hdr_TotalDataSizeLow=strlen($MessagePayload);
+ $hdr_TotalDataSizeHigh=0;
+ $new_id++;
+ $hdr = pack("LLLLLLLLLLLL",
+ 0,
+ $new_id,
+ 0, 0,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh,
+ 0,
+ 0,
+ rand(),
+ 0,
+ 0,0);
+ $message =
+ "MIME-Version: 1.0\r\n".
+ "Content-Type: application/x-msnmsgrp2p\r\n".
+ "P2P-Dest: $from_email\r\n\r\n$hdr$MessagePayload$footer";
+ $this->sb_writeln($socket, $id, "MSG $id D ".strlen($message));
+ $id++;
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: dump send BYE message :\n".$this->dump_binary($message));
+ */
+ break;
+ }
+ //TODO:
+ //if ($hdr_Flag == 2) {
+ // just send ACK...
+ // $this->sb_writeln($socket, $id, "ACK $id");
+ // break;
+ //}
+ if ($hdr_SessionID == 4) {
+ // ignore?
+ $this->debug_message("*** p2p: ignore flag 4");
+ break;
+ }
+ $finished = false;
+ if ($hdr_TotalDataSizeHigh == 0) {
+ // only 32 bites size
+ if (($hdr_MessageLength + $hdr_DataOffsetLow) == $hdr_TotalDataSizeLow)
+ $finished = true;
+ }
+ else {
+ // we won't accept any file transfer
+ // so I think we won't get any message size need to use 64 bits
+ // 64 bits size here, can't count directly...
+ $totalsize = base_convert(sprintf("%X%08X", $hdr_TotalDataSizeHigh, $hdr_TotalDataSizeLow), 16, 10);
+ $dataoffset = base_convert(sprintf("%X%08X", $hdr_DataOffsetHigh, $hdr_DataOffsetLow), 16, 10);
+ $messagelength = base_convert(sprintf("%X", $hdr_MessageLength), 16, 10);
+ $now_size = bcadd($dataoffset, $messagelength);
+ if (bccomp($now_size, $totalsize) >= 0)
+ $finished = true;
+ }
+ if (!$finished) {
+ // ignore not finished split packet
+ $this->debug_message("*** p2p: ignore split packet, not finished");
+ break;
+ }
+ //$new_id = ~$hdr_Identifier;
+ /*
+ $new_id++;
+ $hdr = pack("LLLLLLLLLLLL", $hdr_SessionID,
+ $new_id,
+ 0, 0,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh,
+ 0,
+ 2,
+ $hdr_Identifier,
+ $hdr_AckID,
+ $hdr_TotalDataSizeLow, $hdr_TotalDataSizeHigh);
+ $footer = pack("L", 0);
+ $message = "MIME-Version: 1.0\r\nContent-Type: application/x-msnmsgrp2p\r\nP2P-Dest: $from_email\r\n\r\n$hdr$footer";
+ $len = strlen($message);
+ $this->sb_writeln($socket, $id, "MSG $id D $len");
+ $id++;
+ $this->sb_writedata($socket, $message);
+ $this->debug_message("*** p2p: send acknowledgement for $hdr_SessionID");
+ $this->debug_message("*** p2p: dump sent message:\n".$this->dump_binary($hdr.$footer));
+ */
+ break;
+ }
+ $this->debug_message("*** MSG from $from_email: $sMsg");
+ $this->callHandler('IMin', array('sender' => $from_email, 'message' => $sMsg, 'network' => 1, 'offline' => false));
+ break;
+ case '217':
+ $this->debug_message('*** User '.$session['to'].' is offline. Trying OIM.');
+ $session['offline'] = true;
+ break;
+ default:
+ if (is_numeric($code)) {
+ $this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
+ $this->debug_message("*** SB: $this->error");
+ }
+ break;
+ }
+ }
+
+ /**
+ * Checks for new data and calls appropriate methods
+ *
+ * This method is usually called in an infinite loop to keep checking for new data
+ *
+ * @return void
+ */
+ public function receive() {
+ // First, get an array of sockets that have data that is ready to be read
+ $ready = array();
+ $ready = $this->getSockets();
+ $numrdy = stream_select($ready, $w = NULL, $x = NULL, NULL);
+
+ // Now that we've waited for something, go through the $ready
+ // array and read appropriately
+
+ foreach ($ready as $socket) {
+ if ($socket == $this->NSfp) {
+ $this->nsReceive();
+ } else {
+ $this->sbReceive($socket);
+ }
+ }
+ }
+
+ /**
+ * Switchboard related methods
+ */
+
+ /**
+ * Send a request for a switchboard session
+ *
+ * @param string $to Target email for switchboard session
+ */
+ private function reqSBSession($to) {
+ $this->debug_message("*** Request SB for $to");
+ $this->ns_writeln("XFR $this->id SB");
+
+ // Add to the queue of those waiting for a switchboard session reponse
+ $this->switchBoardSessions[$to] = array(
+ 'to' => $to,
+ 'socket' => NULL,
+ 'id' => 1,
+ 'joined' => false,
+ 'offline' => false,
+ 'XFRReqTime' => time()
+ );
+ $this->waitingForXFR[$to] = &$this->switchBoardSessions[$to];
+ }
+
+ /**
+ * Following an XFR or RNG, connect to the switchboard session
+ *
+ * @param string $mode Mode, either 'Active' (in the case of XFR) or 'Passive' (in the case of RNG)
+ * @param string $ip IP of Switchboard
+ * @param integer $port Port of Switchboard
+ * @param string $to User on other end of Switchboard
+ * @param array $param Array of parameters - 'cki', 'ticket', 'sid'
+ * @return boolean true if successful
+ */
+ private function connectToSBSession($mode, $ip, $port, $to, $param) {
+ $this->debug_message("*** SB: Trying to connect to switchboard server $ip:$port");
+
+ $socket = @fsockopen($ip, $port, $errno, $errstr, $this->timeout);
+ if (!$socket) {
+ $this->debug_message("*** SB: Can't connect to $ip:$port, error => $errno, $errstr");
+ return false;
+ }
+
+ // Store the socket in the lookup array
+ $this->switchBoardSessionLookup[$to] = $socket;
+
+ // Store the socket in the sessions array
+ $this->switchBoardSessions[$to] = array(
+ 'to' => $to,
+ 'socket' => $socket,
+ 'id' => 1,
+ 'joined' => false,
+ 'offline' => false,
+ 'XFRReqTime' => time()
+ );
+
+ // Change the index of the session to the socket
+ $intsocket = (int) $socket;
+ $this->switchBoardSessions[$intsocket] = $this->switchBoardSessions[$to];
+ unset($this->switchBoardSessions[$to]);
+
+ $id = &$this->switchBoardSessions[$intsocket]['id'];
+
+ if ($mode == 'Active') {
+ $cki_code = $param['cki'];
+
+ // SB: >>> USR {id} {user} {cki}
+ $this->sb_writeln($socket, $id, "USR $id $this->user $cki_code");
+ } else {
+ // Passive
+ $ticket = $param['ticket'];
+ $sid = $param['sid'];
+
+ // SB: >>> ANS {id} {user} {ticket} {session_id}
+ $this->sb_writeln($socket, $id, "ANS $id $this->user $ticket $sid");
+ }
+ }
+
+ /**
+ * Called when we want to end a switchboard session
+ * or a switchboard session ends
+ *
+ * @param resource $socket Socket
+ * @param boolean $killsession Whether to delete the session
+ * @return void
+ */
+ private function endSBSession($socket) {
+ if (!self::socketcheck($socket)) {
+ $this->sb_writeln($socket, $fake = 0, 'OUT');
+ }
+ @fclose($socket);
+
+ // Unset session lookup value
+ $intsocket = (int) $socket;
+ unset($this->switchBoardSessionLookup[$this->switchBoardSessions[$intsocket]['to']]);
+
+ // Unset session itself
+ unset($this->switchBoardSessions[$intsocket]);
+ }
+
+ /**
+ * Send a message via an existing SB session
+ *
+ * @param string $to Recipient for message
+ * @param string $message Message
+ * @return boolean true on success
+ */
+ private function sendMessageViaSB($to, $message) {
+ $socket = $this->switchBoardSessionLookup[$to];
+ if (self::socketcheck($socket)) {
+ return false;
+ }
+
+ $id = &$this->switchBoardSessions[(int) $socket]['id'];
+
+ $aMessage = $this->getMessage($message);
+ // CheckEmotion...
+ $MsnObjDefine = $this->GetMsnObjDefine($aMessage);
+ if ($MsnObjDefine !== '') {
+ $SendString = "MIME-Version: 1.0\r\nContent-Type: text/x-mms-emoticon\r\n\r\n$MsnObjDefine";
+ $len = strlen($SendString);
+
+ if ($this->sb_writeln($socket, $id, "MSG $id N $len") === false ||
+ $this->sb_writedata($socket, $SendString) === false) {
+ $this->endSBSession($socket);
+ return false;
+ }
+ }
+ $len = strlen($aMessage);
+
+ if ($this->sb_writeln($socket, $id, "MSG $id N $len") === false ||
+ $this->sb_writedata($socket, $aMessage) === false) {
+ $this->endSBSession($socket);
+ return false;
+ }
+
+ // Don't close the SB session, we might as well leave it open
+ return true;
+ }
+
+ /**
+ * Send a message to a user on another network
+ *
+ * @param string $to Intended recipient
+ * @param string $message Message
+ * @param integer $network Network
+ * @return void
+ */
+ private function sendOtherNetworkMessage($to, $message, $network) {
+ $message = $this->getMessage($message, $network);
+ $len = strlen($message);
+ if ($this->ns_writeln("UUM $this->id $to $network 1 $len") === false ||
+ $this->ns_writedata($Message) === false) {
+ return false;
+ }
+ $this->debug_message("*** Sent to $to (network: $network):\n$Message");
+ return true;
+ }
+
+ /**
+ * Send a message
+ *
+ * @param string $to To address in form user@host.com(@network)
+ * where network is 1 for MSN, 32 for Yahoo
+ * and 'Offline' for offline messages
+ * @param string $message Message
+ * @param boolean &$waitForSession Boolean passed by reference,
+ * if set to true on return, message
+ * did not fail to send but is
+ * waiting for a valid session
+ *
+ * @return boolean true on success
+ */
+ public function sendMessage($to, $message, &$waitForSession) {
+ if ($message != '') {
+ $toParts = explode('@', $to);
+ if(count($toParts) < 3) {
+ list($name, $host) = $toParts;
+ $network = 1;
+ } else {
+ list($name, $host, $network) = $toParts;
+ }
+
+ $recipient = $name.'@'.$host;
+
+ if ($network === 1) {
+ if (!isset($this->switchBoardSessionLookup[$recipient])) {
+ if (!isset($this->switchBoardSessions[$recipient]) || time() - $this->switchBoardSessions[$recipient]['XFRReqTime'] > $this->XFRReqTimeout) {
+ $this->debug_message("*** No existing SB session or request has timed out");
+ $this->reqSBSession($recipient);
+ }
+
+ $waitForSession = true;
+ return false;
+ } else {
+ $socket = $this->switchBoardSessionLookup[$recipient];
+ $intsocket = (int) $socket;
+ if ($this->switchBoardSessions[$intsocket]['offline']) {
+ $this->debug_message("*** Contact ($recipient) offline, sending OIM");
+ $this->endSBSession($socket);
+ $waitForSession = false;
+ return $this->sendMessage($recipient.'@Offline', $message);
+ } else {
+ if ($this->switchBoardSessions[$intsocket]['joined'] !== true) {
+ $this->debug_message("*** Recipient has not joined session, returning false");
+ $waitForSession = true;
+ return false;
+ }
+
+ $this->debug_message("*** Attempting to send message to $recipient using existing SB session");
+
+ if ($this->sendMessageViaSB($recipient, $message)) {
+ $this->debug_message('*** Message sent successfully');
+ return true;
+ }
+
+ $waitForSession = false;
+ return false;
+ }
+ }
+ } elseif ($network == 'Offline') {
+ //Send OIM
+ //FIXME: ä¿®æ£Send OIM
+ $lockkey = '';
+ $re_login = false;
+ for ($i = 0; $i < $this->oim_try; $i++) {
+ if (($oim_result = $this->sendOIM($recipient, $message, $lockkey)) === true) break;
+ if (is_array($oim_result) && $oim_result['challenge'] !== false) {
+ // need challenge lockkey
+ $this->debug_message("*** Need challenge code for ".$oim_result['challenge']);
+ $lockkey = $this->getChallenge($oim_result['challenge']);
+ continue;
+ }
+ if ($oim_result === false || $oim_result['auth_policy'] !== false) {
+ if ($re_login) {
+ $this->debug_message("*** Can't send OIM, but we already re-logged-in again, so returning false");
+ return false;
+ }
+ $this->debug_message("*** Can't send OIM, maybe ticket expired, trying to login again");
+
+ // Maybe we need to re-login again
+ if (!$this->get_passport_ticket()) {
+ $this->debug_message("*** Can't re-login, something went wrong here, returning false");
+ return false;
+ }
+ $this->debug_message("*** Getting new ticket and trying again");
+ continue;
+ }
+ }
+ return true;
+ } else {
+ // Other network
+ return $this->sendOtherNetworkMessage($recipient, $message, $network);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * OIM methods
+ */
+
+ /**
+ * Get OIM mail data
+ *
+ * @return string mail data or false on failure
+ */
+ function getOIM_maildata() {
+ preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches);
+ if (count($matches) == 0) {
+ $this->debug_message('*** No web ticket?');
+ return false;
+ }
+ $t = htmlspecialchars($matches[1]);
+ $p = htmlspecialchars($matches[2]);
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+<soap:Header>
+ <PassportCookie xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">
+ <t>'.$t.'</t>
+ <p>'.$p.'</p>
+ </PassportCookie>
+</soap:Header>
+<soap:Body>
+ <GetMetadata xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi" />
+</soap:Body>
+</soap:Envelope>';
+
+ $header_array = array(
+ 'SOAPAction: '.self::OIM_MAILDATA_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.self::BUILDVER.')'
+ );
+
+ $this->debug_message('*** URL: '.self::OIM_MAILDATA_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::OIM_MAILDATA_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) {
+ $this->debug_message("*** Could not get OIM maildata! http code: $http_code");
+ return false;
+ }
+
+ // <GetMetadataResponse xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">See #XML_Data</GetMetadataResponse>
+ preg_match('#<GetMetadataResponse([^>]*)>(.*)</GetMetadataResponse>#', $data, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message('*** Could not get OIM maildata');
+ return false;
+ }
+ return $matches[2];
+ }
+
+ /**
+ * Fetch OIM message with given id
+ *
+ * @param string $msgid
+ * @return string Message or false on failure
+ */
+ function getOIM_message($msgid) {
+ preg_match('#t=(.*)&p=(.*)#', $this->ticket['web_ticket'], $matches);
+ if (count($matches) == 0) {
+ $this->debug_message('*** No web ticket?');
+ return false;
+ }
+ $t = htmlspecialchars($matches[1]);
+ $p = htmlspecialchars($matches[2]);
+
+ // read OIM
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+<soap:Header>
+ <PassportCookie xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">
+ <t>'.$t.'</t>
+ <p>'.$p.'</p>
+ </PassportCookie>
+</soap:Header>
+<soap:Body>
+ <GetMessage xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">
+ <messageId>'.$msgid.'</messageId>
+ <alsoMarkAsRead>false</alsoMarkAsRead>
+ </GetMessage>
+</soap:Body>
+</soap:Envelope>';
+
+ $header_array = array(
+ 'SOAPAction: '.self::OIM_READ_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.self::BUILDVER.')'
+ );
+
+ $this->debug_message('*** URL: '.self::OIM_READ_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::OIM_READ_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) {
+ $this->debug_message("*** Can't get OIM: $msgid, http code = $http_code");
+ return false;
+ }
+
+ // why can't use preg_match('#<GetMessageResult>(.*)</GetMessageResult>#', $data, $matches)?
+ // multi-lines?
+ $start = strpos($data, '<GetMessageResult>');
+ $end = strpos($data, '</GetMessageResult>');
+ if ($start === false || $end === false || $start > $end) {
+ $this->debug_message("*** Can't get OIM: $msgid");
+ return false;
+ }
+ $lines = substr($data, $start + 18, $end - $start);
+ $aLines = @explode("\n", $lines);
+ $header = true;
+ $ignore = false;
+ $sOIM = '';
+ foreach ($aLines as $line) {
+ $line = rtrim($line);
+ if ($header) {
+ if ($line === '') {
+ $header = false;
+ continue;
+ }
+ continue;
+ }
+ // stop at empty lines
+ if ($line === '') break;
+ $sOIM .= $line;
+ }
+ $sMsg = base64_decode($sOIM);
+ //$this->debug_message("*** we get OIM ($msgid): $sMsg");
+
+ // delete OIM
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+<soap:Header>
+ <PassportCookie xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">
+ <t>'.$t.'</t>
+ <p>'.$p.'</p>
+ </PassportCookie>
+</soap:Header>
+<soap:Body>
+ <DeleteMessages xmlns="http://www.hotmail.msn.com/ws/2004/09/oim/rsi">
+ <messageIds>
+ <messageId>'.$msgid.'</messageId>
+ </messageIds>
+ </DeleteMessages>
+</soap:Body>
+</soap:Envelope>';
+
+ $header_array = array(
+ 'SOAPAction: '.self::OIM_DEL_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.self::BUILDVER.')'
+ );
+
+ $this->debug_message('*** URL: '.self::OIM_DEL_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::OIM_DEL_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200)
+ $this->debug_message("*** Could not delete OIM: $msgid, http code = $http_code");
+ else
+ $this->debug_message("*** OIM ($msgid) deleted");
+ return $sMsg;
+ }
+
+ /**
+ * Send offline message
+ *
+ * @param string $to Intended recipient
+ * @param string $sMessage Message
+ * @param string $lockkey Lock key
+ * @return mixed true on success or error data
+ */
+ private function sendOIM($to, $sMessage, $lockkey) {
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
+<soap:Header>
+ <From memberName="'.$this->user.'"
+ friendlyName="=?utf-8?B?'.base64_encode($this->user).'?="
+ xml:lang="zh-TW"
+ proxy="MSNMSGR"
+ xmlns="http://messenger.msn.com/ws/2004/09/oim/"
+ msnpVer="'.self::PROTOCOL.'"
+ buildVer="'.self::BUILDVER.'"/>
+ <To memberName="'.$to.'" xmlns="http://messenger.msn.com/ws/2004/09/oim/"/>
+ <Ticket passport="'.htmlspecialchars($this->ticket['oim_ticket']).'"
+ appid="'.self::PROD_ID.'"
+ lockkey="'.$lockkey.'"
+ xmlns="http://messenger.msn.com/ws/2004/09/oim/"/>
+ <Sequence xmlns="http://schemas.xmlsoap.org/ws/2003/03/rm">
+ <Identifier xmlns="http://schemas.xmlsoap.org/ws/2002/07/utility">http://messenger.msn.com</Identifier>
+ <MessageNumber>1</MessageNumber>
+ </Sequence>
+</soap:Header>
+<soap:Body>
+ <MessageType xmlns="http://messenger.msn.com/ws/2004/09/oim/">text</MessageType>
+ <Content xmlns="http://messenger.msn.com/ws/2004/09/oim/">MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: base64
+X-OIM-Message-Type: OfflineMessage
+X-OIM-Run-Id: {DAB68CFA-38C9-449B-945E-38AFA51E50A7}
+X-OIM-Sequence-Num: 1
+
+'.chunk_split(base64_encode($sMessage)).'
+ </Content>
+</soap:Body>
+</soap:Envelope>';
+
+ $header_array = array(
+ 'SOAPAction: '.self::OIM_SEND_SOAP,
+ 'Content-Type: text/xml',
+ 'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger '.self::BUILDVER.')'
+ );
+
+ $this->debug_message('*** URL: '.self::OIM_SEND_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::OIM_SEND_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code == 200) {
+ $this->debug_message("*** OIM sent for $to");
+ return true;
+ }
+
+ $challenge = false;
+ $auth_policy = false;
+ // the lockkey is invalid, authenticated fail, we need challenge it again
+ // <LockKeyChallenge xmlns="http://messenger.msn.com/ws/2004/09/oim/">364763969</LockKeyChallenge>
+ preg_match("#<LockKeyChallenge (.*)>(.*)</LockKeyChallenge>#", $data, $matches);
+ if (count($matches) != 0) {
+ // yes, we get new LockKeyChallenge
+ $challenge = $matches[2];
+ $this->debug_message("*** OIM need new challenge ($challenge) for $to");
+ }
+ // auth policy error
+ // <RequiredAuthPolicy xmlns="http://messenger.msn.com/ws/2004/09/oim/">MBI_SSL</RequiredAuthPolicy>
+ preg_match("#<RequiredAuthPolicy (.*)>(.*)</RequiredAuthPolicy>#", $data, $matches);
+ if (count($matches) != 0) {
+ $auth_policy = $matches[2];
+ $this->debug_message("*** OIM need new auth policy ($auth_policy) for $to");
+ }
+ if ($auth_policy === false && $challenge === false) {
+ //<faultcode xmlns:q0="http://messenger.msn.com/ws/2004/09/oim/">q0:AuthenticationFailed</faultcode>
+ preg_match("#<faultcode (.*)>(.*)</faultcode>#", $data, $matches);
+ if (count($matches) == 0) {
+ // no error, we assume the OIM is sent
+ $this->debug_message("*** OIM sent for $to");
+ return true;
+ }
+ $err_code = $matches[2];
+ //<faultstring>Exception of type 'System.Web.Services.Protocols.SoapException' was thrown.</faultstring>
+ preg_match("#<faultstring>(.*)</faultstring>#", $data, $matches);
+ if (count($matches) > 0)
+ $err_msg = $matches[1];
+ else
+ $err_msg = '';
+ $this->debug_message("*** OIM failed for $to");
+ $this->debug_message("*** OIM Error code: $err_code");
+ $this->debug_message("*** OIM Error Message: $err_msg");
+ return false;
+ }
+ return array('challenge' => $challenge, 'auth_policy' => $auth_policy);
+ }
+
+ /**
+ * Contact / Membership list methods
+ */
+
+ /**
+ * Fetch contact list
+ *
+ * @return boolean true on success
+ */
+ private function UpdateContacts() {
+ $ABApplicationHeaderArray = array(
+ 'ABApplicationHeader' => array(
+ ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'),
+ 'ApplicationId' => 'CFE80F9D-180F-4399-82AB-413F33A1FA11',
+ 'IsMigration' => false,
+ 'PartnerScenario' => 'ContactSave'
+ )
+ );
+
+ $ABApplicationHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABApplicationHeader', $this->Array2SoapVar($ABApplicationHeaderArray));
+ $ABFindAllArray = array(
+ 'ABFindAll' => array(
+ ':' => array('xmlns'=>'http://www.msn.com/webservices/AddressBook'),
+ 'abId' => '00000000-0000-0000-0000-000000000000',
+ 'abView' => 'Full',
+ 'lastChange' => '0001-01-01T00:00:00.0000000-08:00',
+ )
+ );
+ $ABFindAll = new SoapParam($this->Array2SoapVar($ABFindAllArray), 'ABFindAll');
+ $this->ABService->__setSoapHeaders(array($ABApplicationHeader, $this->ABAuthHeader));
+ $this->Contacts = array();
+ try {
+ $this->debug_message('*** Updating Contacts...');
+ $Result = $this->ABService->ABFindAll($ABFindAll);
+ $this->debug_message("*** Result:\n".print_r($Result, true)."\n".$this->ABService->__getLastResponse());
+ foreach($Result->ABFindAllResult->contacts->Contact as $Contact)
+ $this->Contacts[$Contact->contactInfo->passportName] = $Contact;
+ } catch(Exception $e) {
+ $this->debug_message("*** Update Contacts Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add contact
+ *
+ * @param string $email
+ * @param integer $network
+ * @param string $display
+ * @param boolean $sendADL
+ * @return boolean true on success
+ */
+ private function addContact($email, $network, $display = '', $sendADL = false) {
+ if ($network != 1) return true;
+ if (isset($this->Contacts[$email])) return true;
+
+ $ABContactAddArray = array(
+ 'ABContactAdd' => array(
+ ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'),
+ 'abId' => '00000000-0000-0000-0000-000000000000',
+ 'contacts' => array(
+ 'Contact' => array(
+ ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'),
+ 'contactInfo' => array(
+ 'contactType' => 'LivePending',
+ 'passportName' => $email,
+ 'isMessengerUser' => true,
+ 'MessengerMemberInfo' => array(
+ 'DisplayName' => $email
+ )
+ )
+ )
+ ),
+ 'options' => array(
+ 'EnableAllowListManagement' => true
+ )
+ )
+ );
+ $ABContactAdd = new SoapParam($this->Array2SoapVar($ABContactAddArray), 'ABContactAdd');
+ try {
+ $this->debug_message("*** Adding Contact $email...");
+ $this->ABService->ABContactAdd($ABContactAdd);
+ } catch(Exception $e) {
+ $this->debug_message("*** Add Contact Error \nRequest:".$this->ABService->__getLastRequest()."\nError:".$e->getMessage());
+ return false;
+ }
+ if ($sendADL && !feof($this->NSfp)) {
+ @list($u_name, $u_domain) = @explode('@', $email);
+ foreach (array('1', '2') as $l) {
+ $str = '<ml l="1"><d n="'.$u_domain.'"><c n="'.$u_name.'" l="'.$l.'" t="'.$network.'" /></d></ml>';
+ $len = strlen($str);
+ // NS: >>> ADL {id} {size}
+ $this->ns_writeln("ADL $this->id $len");
+ $this->ns_writedata($str);
+ }
+ }
+ $this->UpdateContacts();
+ return true;
+ }
+
+ /**
+ * Remove contact from list
+ *
+ * @param integer $memberID
+ * @param string $email
+ * @param integer $network
+ * @param string $list
+ */
+ function delMemberFromList($memberID, $email, $network, $list) {
+ if ($network != 1 && $network != 32) return true;
+ if ($memberID === false) return true;
+ $user = $email;
+ $ticket = htmlspecialchars($this->ticket['contact_ticket']);
+ if ($network == 1)
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
+<soap:Header>
+ <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ApplicationId>996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <IsMigration>false</IsMigration>
+ <PartnerScenario>ContactMsgrAPI</PartnerScenario>
+ </ABApplicationHeader>
+ <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ManagedGroupRequest>false</ManagedGroupRequest>
+ <TicketToken>'.$ticket.'</TicketToken>
+ </ABAuthHeader>
+</soap:Header>
+<soap:Body>
+ <DeleteMember xmlns="http://www.msn.com/webservices/AddressBook">
+ <serviceHandle>
+ <Id>0</Id>
+ <Type>Messenger</Type>
+ <ForeignId></ForeignId>
+ </serviceHandle>
+ <memberships>
+ <Membership>
+ <MemberRole>'.$list.'</MemberRole>
+ <Members>
+ <Member xsi:type="PassportMember" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Type>Passport</Type>
+ <MembershipId>'.$memberID.'</MembershipId>
+ <State>Accepted</State>
+ </Member>
+ </Members>
+ </Membership>
+ </memberships>
+ </DeleteMember>
+</soap:Body>
+</soap:Envelope>';
+ else
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
+<soap:Header>
+ <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ApplicationId>996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <IsMigration>false</IsMigration>
+ <PartnerScenario>ContactMsgrAPI</PartnerScenario>
+ </ABApplicationHeader>
+ <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ManagedGroupRequest>false</ManagedGroupRequest>
+ <TicketToken>'.$ticket.'</TicketToken>
+ </ABAuthHeader>
+</soap:Header>
+<soap:Body>
+ <DeleteMember xmlns="http://www.msn.com/webservices/AddressBook">
+ <serviceHandle>
+ <Id>0</Id>
+ <Type>Messenger</Type>
+ <ForeignId></ForeignId>
+ </serviceHandle>
+ <memberships>
+ <Membership>
+ <MemberRole>'.$list.'</MemberRole>
+ <Members>
+ <Member xsi:type="EmailMember" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Type>Email</Type>
+ <MembershipId>'.$memberID.'</MembershipId>
+ <State>Accepted</State>
+ </Member>
+ </Members>
+ </Membership>
+ </memberships>
+ </DeleteMember>
+</soap:Body>
+</soap:Envelope>';
+
+ $header_array = array(
+ 'SOAPAction: '.self::DELMEMBER_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)'
+ );
+
+ $this->debug_message('*** URL: '.self::DELMEMBER_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::DELMEMBER_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) {
+ preg_match('#<faultcode>(.*)</faultcode><faultstring>(.*)</faultstring>#', $data, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list");
+ return false;
+ }
+ $faultcode = trim($matches[1]);
+ $faultstring = trim($matches[2]);
+ if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member does not exist') === false) {
+ $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, error code: $faultcode, $faultstring");
+ return false;
+ }
+ $this->debug_message("*** Could not delete member (network: $network) $email ($memberID) from $list list, not present in list");
+ return true;
+ }
+ $this->debug_message("*** Member successfully deleted (network: $network) $email ($memberID) from $list list");
+ return true;
+ }
+
+ /**
+ * Add contact to list
+ *
+ * @param string $email
+ * @param integer $network
+ * @param string $list
+ */
+ function addMemberToList($email, $network, $list) {
+ if ($network != 1 && $network != 32) return true;
+ $ticket = htmlspecialchars($this->ticket['contact_ticket']);
+ $user = $email;
+
+ if ($network == 1)
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
+<soap:Header>
+ <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ApplicationId>996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <IsMigration>false</IsMigration>
+ <PartnerScenario>ContactMsgrAPI</PartnerScenario>
+ </ABApplicationHeader>
+ <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ManagedGroupRequest>false</ManagedGroupRequest>
+ <TicketToken>'.$ticket.'</TicketToken>
+ </ABAuthHeader>
+</soap:Header>
+<soap:Body>
+ <AddMember xmlns="http://www.msn.com/webservices/AddressBook">
+ <serviceHandle>
+ <Id>0</Id>
+ <Type>Messenger</Type>
+ <ForeignId></ForeignId>
+ </serviceHandle>
+ <memberships>
+ <Membership>
+ <MemberRole>'.$list.'</MemberRole>
+ <Members>
+ <Member xsi:type="PassportMember" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Type>Passport</Type>
+ <State>Accepted</State>
+ <PassportName>'.$user.'</PassportName>
+ </Member>
+ </Members>
+ </Membership>
+ </memberships>
+ </AddMember>
+</soap:Body>
+</soap:Envelope>';
+ else
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
+<soap:Header>
+ <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ApplicationId>996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <IsMigration>false</IsMigration>
+ <PartnerScenario>ContactMsgrAPI</PartnerScenario>
+ </ABApplicationHeader>
+ <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ManagedGroupRequest>false</ManagedGroupRequest>
+ <TicketToken>'.$ticket.'</TicketToken>
+ </ABAuthHeader>
+</soap:Header>
+<soap:Body>
+ <AddMember xmlns="http://www.msn.com/webservices/AddressBook">
+ <serviceHandle>
+ <Id>0</Id>
+ <Type>Messenger</Type>
+ <ForeignId></ForeignId>
+ </serviceHandle>
+ <memberships>
+ <Membership>
+ <MemberRole>'.$list.'</MemberRole>
+ <Members>
+ <Member xsi:type="EmailMember" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <Type>Email</Type>
+ <State>Accepted</State>
+ <Email>'.$user.'</Email>
+ <Annotations>
+ <Annotation>
+ <Name>MSN.IM.BuddyType</Name>
+ <Value>32:YAHOO</Value>
+ </Annotation>
+ </Annotations>
+ </Member>
+ </Members>
+ </Membership>
+ </memberships>
+ </AddMember>
+</soap:Body>
+</soap:Envelope>';
+ $header_array = array(
+ 'SOAPAction: '.self::ADDMEMBER_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)'
+ );
+
+ $this->debug_message('*** URL: '.self::ADDMEMBER_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::ADDMEMBER_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) {
+ preg_match('#<faultcode>(.*)</faultcode><faultstring>(.*)</faultstring>#', $data, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message("*** Could not add member (network: $network) $email to $list list");
+ return false;
+ }
+ $faultcode = trim($matches[1]);
+ $faultstring = trim($matches[2]);
+ if (strcasecmp($faultcode, 'soap:Client') || stripos($faultstring, 'Member already exists') === false) {
+ $this->debug_message("*** Could not add member (network: $network) $email to $list list, error code: $faultcode, $faultstring");
+ return false;
+ }
+ $this->debug_message("*** Could not add member (network: $network) $email to $list list, already present");
+ return true;
+ }
+ $this->debug_message("*** Member successfully added (network: $network) $email to $list list");
+ return true;
+ }
+
+ /**
+ * Get membership lists
+ *
+ * @param mixed $returnData Membership list or false on failure
+ */
+ function getMembershipList($returnData = false) {
+ $ticket = htmlspecialchars($this->ticket['contact_ticket']);
+ $XML = '<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
+<soap:Header>
+ <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ApplicationId>996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <IsMigration>false</IsMigration>
+ <PartnerScenario>Initial</PartnerScenario>
+ </ABApplicationHeader>
+ <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
+ <ManagedGroupRequest>false</ManagedGroupRequest>
+ <TicketToken>'.$ticket.'</TicketToken>
+ </ABAuthHeader>
+</soap:Header>
+<soap:Body>
+ <FindMembership xmlns="http://www.msn.com/webservices/AddressBook">
+ <serviceFilter>
+ <Types>
+ <ServiceType>Messenger</ServiceType>
+ <ServiceType>Invitation</ServiceType>
+ <ServiceType>SocialNetwork</ServiceType>
+ <ServiceType>Space</ServiceType>
+ <ServiceType>Profile</ServiceType>
+ </Types>
+ </serviceFilter>
+ </FindMembership>
+</soap:Body>
+</soap:Envelope>';
+ $header_array = array(
+ 'SOAPAction: '.self::MEMBERSHIP_SOAP,
+ 'Content-Type: text/xml; charset=utf-8',
+ 'User-Agent: MSN Explorer/9.0 (MSN 8.0; TmstmpExt)'
+ );
+ $this->debug_message('*** URL: '.self::MEMBERSHIP_URL);
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, self::MEMBERSHIP_URL);
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) return false;
+ $p = $data;
+ $aMemberships = array();
+ while (1) {
+ //$this->debug_message("search p = $p");
+ $start = strpos($p, '<Membership>');
+ $end = strpos($p, '</Membership>');
+ if ($start === false || $end === false || $start > $end) break;
+ //$this->debug_message("start = $start, end = $end");
+ $end += 13;
+ $sMembership = substr($p, $start, $end - $start);
+ $aMemberships[] = $sMembership;
+ //$this->debug_message("add sMembership = $sMembership");
+ $p = substr($p, $end);
+ }
+ //$this->debug_message("aMemberships = ".var_export($aMemberships, true));
+
+ $aContactList = array();
+ foreach ($aMemberships as $sMembership) {
+ //$this->debug_message("sMembership = $sMembership");
+ if (isset($matches)) unset($matches);
+ preg_match('#<MemberRole>(.*)</MemberRole>#', $sMembership, $matches);
+ if (count($matches) == 0) continue;
+ $sMemberRole = $matches[1];
+ //$this->debug_message("MemberRole = $sMemberRole");
+ if ($sMemberRole != 'Allow' && $sMemberRole != 'Reverse' && $sMemberRole != 'Pending') continue;
+ $p = $sMembership;
+ if (isset($aMembers)) unset($aMembers);
+ $aMembers = array();
+ while (1) {
+ //$this->debug_message("search p = $p");
+ $start = strpos($p, '<Member xsi:type="');
+ $end = strpos($p, '</Member>');
+ if ($start === false || $end === false || $start > $end) break;
+ //$this->debug_message("start = $start, end = $end");
+ $end += 9;
+ $sMember = substr($p, $start, $end - $start);
+ $aMembers[] = $sMember;
+ //$this->debug_message("add sMember = $sMember");
+ $p = substr($p, $end);
+ }
+ //$this->debug_message("aMembers = ".var_export($aMembers, true));
+ foreach ($aMembers as $sMember) {
+ //$this->debug_message("sMember = $sMember");
+ if (isset($matches)) unset($matches);
+ preg_match('#<Member xsi\:type="([^"]*)">#', $sMember, $matches);
+ if (count($matches) == 0) continue;
+ $sMemberType = $matches[1];
+ //$this->debug_message("MemberType = $sMemberType");
+ $network = -1;
+ preg_match('#<MembershipId>(.*)</MembershipId>#', $sMember, $matches);
+ if (count($matches) == 0) continue;
+ $id = $matches[1];
+ if ($sMemberType == 'PassportMember') {
+ if (strpos($sMember, '<Type>Passport</Type>') === false) continue;
+ $network = 1;
+ preg_match('#<PassportName>(.*)</PassportName>#', $sMember, $matches);
+ }
+ else if ($sMemberType == 'EmailMember') {
+ if (strpos($sMember, '<Type>Email</Type>') === false) continue;
+ // Value is 32: or 32:YAHOO
+ preg_match('#<Annotation><Name>MSN.IM.BuddyType</Name><Value>(.*):(.*)</Value></Annotation>#', $sMember, $matches);
+ if (count($matches) == 0) continue;
+ if ($matches[1] != 32) continue;
+ $network = 32;
+ preg_match('#<Email>(.*)</Email>#', $sMember, $matches);
+ }
+ if ($network == -1) continue;
+ if (count($matches) > 0) {
+ $email = $matches[1];
+ @list($u_name, $u_domain) = @explode('@', $email);
+ if ($u_domain == NULL) continue;
+ $aContactList[$u_domain][$u_name][$network][$sMemberRole] = $id;
+ $this->debug_message("*** Adding new contact (network: $network, status: $sMemberRole): $u_name@$u_domain ($id)");
+ }
+ }
+ }
+ return $aContactList;
+ }
+
+ /**
+ * MsnObj related methods
+ */
+
+ /**
+ *
+ * @param $FilePath 圖檔路徑
+ * @param $Type 檔案類型 3=>大é è²¼,2表情圖案
+ * @return array
+ */
+ private function MsnObj($FilePath, $Type = 3) {
+ if (!($FileSize=filesize($FilePath))) return '';
+ $Location = md5($FilePath);
+ $Friendly = md5($FilePath.$Type);
+ if (isset($this->MsnObjMap[$Location])) return $this->MsnObjMap[$Location];
+ $sha1d = base64_encode(sha1(file_get_contents($FilePath), true));
+ $sha1c = base64_encode(sha1("Creator".$this->user."Size$FileSize"."Type$Type"."Location$Location"."Friendly".$Friendly."SHA1D$sha1d", true));
+ $this->MsnObjArray[$Location] = $FilePath;
+ $MsnObj = '<msnobj Creator="'.$this->user.'" Size="'.$FileSize.'" Type="'.$Type.'" Location="'.$Location.'" Friendly="'.$Friendly.'" SHA1D="'.$sha1d.'" SHA1C="'.$sha1c.'"/>';
+ $this->MsnObjMap[$Location] = $MsnObj;
+ $this->debug_message("*** p2p: addMsnObj $FilePath::$MsnObj\n");
+ return $MsnObj;
+ }
+
+ private function GetPictureFilePath($Context) {
+ $MsnObj = base64_decode($Context);
+ if (preg_match('/location="(.*?)"/i', $MsnObj, $Match))
+ $location = $Match[1];
+ $this->debug_message("*** p2p: PictureFile[$location] ::All".print_r($this->MsnObjArray,true)."\n");
+ if ($location && isset($this->MsnObjArray[$location]))
+ return $this->MsnObjArray[$location];
+ return false;
+ }
+
+ private function GetMsnObjDefine($Message) {
+ $DefineString = '';
+ if (is_array($this->Emotions))
+ foreach ($this->Emotions as $Pattern => $FilePath) {
+ if (strpos($Message, $Pattern) !== false)
+ $DefineString .= "$Pattern\t".$this->MsnObj($FilePath, 2)."\t";
+ }
+ return $DefineString;
+ }
+
+ /**
+ * Socket methods
+ */
+
+ /**
+ * Read data of specified size from NS socket
+ *
+ * @param integer $size Size to read
+ * @return string Data read
+ */
+ private function ns_readdata($size) {
+ $data = '';
+ $count = 0;
+ while (!feof($this->NSfp)) {
+ $buf = @fread($this->NSfp, $size - $count);
+ $data .= $buf;
+ $count += strlen($buf);
+ if ($count >= $size) break;
+ }
+ $this->debug_message("NS: data ($size/$count) <<<\n$data");
+ return $data;
+ }
+
+ /**
+ * Read line from the NS socket
+ *
+ * @return string Data read
+ */
+ private function ns_readln() {
+ $data = @fgets($this->NSfp, 4096);
+ if ($data !== false) {
+ $data = trim($data);
+ $this->debug_message("NS: <<< $data");
+ }
+ return $data;
+ }
+
+ /**
+ * Write line to NS socket
+ *
+ * Also increments id
+ *
+ * @param string $data Line to write to socket
+ * @return mixed Bytes written or false on failure
+ */
+ private function ns_writeln($data) {
+ $result = @fwrite($this->NSfp, $data."\r\n");
+ if ($result !== false) {
+ $this->debug_message("NS: >>> $data");
+ $this->id++;
+ }
+ return $result;
+ }
+
+ /**
+ * Write data to NS socket
+ *
+ * @param string $data Data to write to socket
+ * @return mixed Bytes written or false on failure
+ */
+ private function ns_writedata($data) {
+ $result = @fwrite($this->NSfp, $data);
+ if ($result !== false) {
+ $this->debug_message("NS: >>> $data");
+ }
+ return $result;
+ }
+
+ /**
+ * Read data of specified size from given SB socket
+ *
+ * @param resource $socket SB socket
+ * @param integer $size Size to read
+ * @return string Data read
+ */
+ private function sb_readdata($socket, $size) {
+ $data = '';
+ $count = 0;
+ while (!feof($socket)) {
+ $buf = @fread($socket, $size - $count);
+ $data .= $buf;
+ $count += strlen($buf);
+ if ($count >= $size) break;
+ }
+ $this->debug_message("SB: data ($size/$count) <<<\n$data");
+ return $data;
+ }
+
+ /**
+ * Read line from given SB socket
+ *
+ * @param resource $socket SB Socket
+ * @return string Line read
+ */
+ private function sb_readln($socket) {
+ $data = @fgets($socket, 4096);
+ if ($data !== false) {
+ $data = trim($data);
+ $this->debug_message("SB: <<< $data");
+ }
+ return $data;
+ }
+
+ /**
+ * Write line to given SB socket
+ *
+ * Also increments id
+ *
+ * @param resource $socket SB socket
+ * @param integer $id Reference to SB id
+ * @param string $data Line to write
+ * @return mixed Bytes written or false on error
+ */
+ private function sb_writeln($socket, &$id, $data) {
+ $result = @fwrite($socket, $data."\r\n");
+ if ($result !== false) {
+ $this->debug_message("SB: >>> $data");
+ $id++;
+ }
+ return $result;
+ }
+
+ /**
+ * Write data to given SB socket
+ *
+ * @param resource $socket SB socket
+ * @param $data Data to write to socket
+ * @return mixed Bytes written or false on error
+ */
+ private function sb_writedata($socket, $data) {
+ $result = @fwrite($socket, $data);
+ if ($result !== false) {
+ $this->debug_message("SB: >>> $data");
+ }
+ return $result;
+ }
+
+ /**
+ * Get all the sockets currently in use
+ *
+ * @return array Array of socket resources
+ */
+ public function getSockets() {
+ return array_merge(array($this->NSfp), $this->switchBoardSessionLookup);
+ }
+
+ /**
+ * Checks socket for end of file
+ *
+ * @param resource $socket Socket to check
+ * @return boolean true if end of file (socket)
+ */
+ private static function socketcheck($socket){
+ $info = stream_get_meta_data($socket);
+ return $info['eof'];
+ }
+
+ /**
+ * Key generation methods
+ */
+
+ private function derive_key($key, $magic) {
+ $hash1 = $this->mhash_sha1($magic, $key);
+ $hash2 = $this->mhash_sha1($hash1.$magic, $key);
+ $hash3 = $this->mhash_sha1($hash1, $key);
+ $hash4 = $this->mhash_sha1($hash3.$magic, $key);
+ return $hash2.substr($hash4, 0, 4);
+ }
+
+ private function generateLoginBLOB($key, $challenge) {
+ $key1 = base64_decode($key);
+ $key2 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY HASH');
+ $key3 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY ENCRYPTION');
+
+ // get hash of challenge using key2
+ $hash = $this->mhash_sha1($challenge, $key2);
+
+ // get 8 bytes random data
+ $iv = substr(base64_encode(rand(1000,9999).rand(1000,9999)), 2, 8);
+
+ $cipher = mcrypt_cbc(MCRYPT_3DES, $key3, $challenge."\x08\x08\x08\x08\x08\x08\x08\x08", MCRYPT_ENCRYPT, $iv);
+
+ $blob = pack('LLLLLLL', 28, 1, 0x6603, 0x8004, 8, 20, 72);
+ $blob .= $iv;
+ $blob .= $hash;
+ $blob .= $cipher;
+
+ return base64_encode($blob);
+ }
+
+ /**
+ * Generate challenge response
+ *
+ * @param string $code
+ * @return string challenge response code
+ */
+ private function getChallenge($code) {
+ // MSNP15
+ // http://msnpiki.msnfanatic.com/index.php/MSNP11:Challenges
+ // Step 1: The MD5 Hash
+ $md5Hash = md5($code.self::PROD_KEY);
+ $aMD5 = @explode("\0", chunk_split($md5Hash, 8, "\0"));
+ for ($i = 0; $i < 4; $i++) {
+ $aMD5[$i] = implode('', array_reverse(@explode("\0", chunk_split($aMD5[$i], 2, "\0"))));
+ $aMD5[$i] = (0 + base_convert($aMD5[$i], 16, 10)) & 0x7FFFFFFF;
+ }
+
+ // Step 2: A new string
+ $chl_id = $code.self::PROD_ID;
+ $chl_id .= str_repeat('0', 8 - (strlen($chl_id) % 8));
+
+ $aID = @explode("\0", substr(chunk_split($chl_id, 4, "\0"), 0, -1));
+ for ($i = 0; $i < count($aID); $i++) {
+ $aID[$i] = implode('', array_reverse(@explode("\0", chunk_split($aID[$i], 1, "\0"))));
+ $aID[$i] = 0 + base_convert(bin2hex($aID[$i]), 16, 10);
+ }
+
+ // Step 3: The 64 bit key
+ $magic_num = 0x0E79A9C1;
+ $str7f = 0x7FFFFFFF;
+ $high = 0;
+ $low = 0;
+ for ($i = 0; $i < count($aID); $i += 2) {
+ $temp = $aID[$i];
+ $temp = bcmod(bcmul($magic_num, $temp), $str7f);
+ $temp = bcadd($temp, $high);
+ $temp = bcadd(bcmul($aMD5[0], $temp), $aMD5[1]);
+ $temp = bcmod($temp, $str7f);
+
+ $high = $aID[$i+1];
+ $high = bcmod(bcadd($high, $temp), $str7f);
+ $high = bcadd(bcmul($aMD5[2], $high), $aMD5[3]);
+ $high = bcmod($high, $str7f);
+
+ $low = bcadd(bcadd($low, $high), $temp);
+ }
+
+ $high = bcmod(bcadd($high, $aMD5[1]), $str7f);
+ $low = bcmod(bcadd($low, $aMD5[3]), $str7f);
+
+ $new_high = bcmul($high & 0xFF, 0x1000000);
+ $new_high = bcadd($new_high, bcmul($high & 0xFF00, 0x100));
+ $new_high = bcadd($new_high, bcdiv($high & 0xFF0000, 0x100));
+ $new_high = bcadd($new_high, bcdiv($high & 0xFF000000, 0x1000000));
+ // we need integer here
+ $high = 0+$new_high;
+
+ $new_low = bcmul($low & 0xFF, 0x1000000);
+ $new_low = bcadd($new_low, bcmul($low & 0xFF00, 0x100));
+ $new_low = bcadd($new_low, bcdiv($low & 0xFF0000, 0x100));
+ $new_low = bcadd($new_low, bcdiv($low & 0xFF000000, 0x1000000));
+ // we need integer here
+ $low = 0+$new_low;
+
+ // we just use 32 bits integer, don't need the key, just high/low
+ // $key = bcadd(bcmul($high, 0x100000000), $low);
+
+ // Step 4: Using the key
+ $md5Hash = md5($code.self::PROD_KEY);
+ $aHash = @explode("\0", chunk_split($md5Hash, 8, "\0"));
+
+ $hash = '';
+ $hash .= sprintf("%08x", (0 + base_convert($aHash[0], 16, 10)) ^ $high);
+ $hash .= sprintf("%08x", (0 + base_convert($aHash[1], 16, 10)) ^ $low);
+ $hash .= sprintf("%08x", (0 + base_convert($aHash[2], 16, 10)) ^ $high);
+ $hash .= sprintf("%08x", (0 + base_convert($aHash[3], 16, 10)) ^ $low);
+
+ return $hash;
+ }
+
+ /**
+ * Utility methods
+ */
+
+ private function Array2SoapVar($Array, $ReturnSoapVarObj = true, $TypeName = null, $TypeNameSpace = null) {
+ $ArrayString = '';
+ foreach($Array as $Key => $Val) {
+ if ($Key{0} == ':') continue;
+ $Attrib = '';
+ if (is_array($Val[':'])) {
+ foreach ($Val[':'] as $AttribName => $AttribVal)
+ $Attrib .= " $AttribName = '$AttribVal'";
+ }
+ if ($Key{0} == '!') {
+ //List Type Define
+ $Key = substr($Key,1);
+ foreach ($Val as $ListKey => $ListVal) {
+ if ($ListKey{0} == ':') continue;
+ if (is_array($ListVal)) $ListVal = $this->Array2SoapVar($ListVal, false);
+ elseif (is_bool($ListVal)) $ListVal = $ListVal ? 'true' : 'false';
+ $ArrayString .= "<$Key$Attrib>$ListVal</$Key>";
+ }
+ continue;
+ }
+ if (is_array($Val)) $Val = $this->Array2SoapVar($Val, false);
+ elseif (is_bool($Val)) $Val = $Val ? 'true' : 'false';
+ $ArrayString .= "<$Key$Attrib>$Val</$Key>";
+ }
+ if ($ReturnSoapVarObj) return new SoapVar($ArrayString, XSD_ANYXML, $TypeName, $TypeNameSpace);
+ return $ArrayString;
+ }
+
+ private function linetoArray($lines) {
+ $lines = str_replace("\r", '', $lines);
+ $lines = explode("\n", $lines);
+ foreach ($lines as $line) {
+ if (!isset($line{3})) continue;
+ list($Key, $Val) = explode(':', $line);
+ $Data[trim($Key)] = trim($Val);
+ }
+ return $Data;
+ }
+
+ /**
+ * Get Passport ticket
+ *
+ * @param string $url URL string (Optional)
+ * @return mixed Array of tickets or false on failure
+ */
+ private function get_passport_ticket($url = '') {
+ $user = $this->user;
+ $password = htmlspecialchars($this->password);
+
+ if ($url === '')
+ $passport_url = self::PASSPORT_URL;
+ else
+ $passport_url = $url;
+
+ $XML = '<?xml version="1.0" encoding="UTF-8"?>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext"
+ xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
+ xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy"
+ xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
+ xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing"
+ xmlns:wssc="http://schemas.xmlsoap.org/ws/2004/04/sc"
+ xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust">
+<Header>
+ <ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="PPAuthInfo">
+ <ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp>
+ <ps:BinaryVersion>4</ps:BinaryVersion>
+ <ps:UIVersion>1</ps:UIVersion>
+ <ps:Cookies></ps:Cookies>
+ <ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
+ </ps:AuthInfo>
+ <wsse:Security>
+ <wsse:UsernameToken Id="user">
+ <wsse:Username>'.$user.'</wsse:Username>
+ <wsse:Password>'.$password.'</wsse:Password>
+ </wsse:UsernameToken>
+ </wsse:Security>
+</Header>
+<Body>
+ <ps:RequestMultipleSecurityTokens xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="RSTS">
+ <wst:RequestSecurityToken Id="RST0">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>http://Passport.NET/tb</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST1">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>messengerclear.live.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="'.$this->passport_policy.'"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST2">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>messenger.msn.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="?id=507"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST3">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>contacts.msn.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST4">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>messengersecure.live.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="MBI_SSL"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST5">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>spaces.live.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ <wst:RequestSecurityToken Id="RST6">
+ <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
+ <wsp:AppliesTo>
+ <wsa:EndpointReference>
+ <wsa:Address>storage.msn.com</wsa:Address>
+ </wsa:EndpointReference>
+ </wsp:AppliesTo>
+ <wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
+ </wst:RequestSecurityToken>
+ </ps:RequestMultipleSecurityTokens>
+</Body>
+</Envelope>';
+
+ $this->debug_message("*** URL: $passport_url");
+ $this->debug_message("*** Sending SOAP:\n$XML");
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $passport_url);
+ if ($this->debug) curl_setopt($curl, CURLOPT_HEADER, 1);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
+ $data = curl_exec($curl);
+ $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+ $this->debug_message("*** Get Result:\n$data");
+
+ if ($http_code != 200) {
+ // sometimes, redirect to another URL
+ // MSNP15
+ //<faultcode>psf:Redirect</faultcode>
+ //<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
+ //<faultstring>Authentication Failure</faultstring>
+ if (strpos($data, '<faultcode>psf:Redirect</faultcode>') === false) {
+ $this->debug_message("*** Could not get passport ticket! http code = $http_code");
+ return false;
+ }
+ preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
+ if (count($matches) == 0) {
+ $this->debug_message('*** Redirected, but could not get redirect URL!');
+ return false;
+ }
+ $redirect_url = $matches[1];
+ if ($redirect_url == $passport_url) {
+ $this->debug_message('*** Redirected, but to same URL!');
+ return false;
+ }
+ $this->debug_message("*** Redirected to $redirect_url");
+ return $this->get_passport_ticket($redirect_url);
+ }
+
+ // sometimes, redirect to another URL, also return 200
+ // MSNP15
+ //<faultcode>psf:Redirect</faultcode>
+ //<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
+ //<faultstring>Authentication Failure</faultstring>
+ if (strpos($data, '<faultcode>psf:Redirect</faultcode>') !== false) {
+ preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
+ if (count($matches) != 0) {
+ $redirect_url = $matches[1];
+ if ($redirect_url == $passport_url) {
+ $this->debug_message('*** Redirected, but to same URL!');
+ return false;
+ }
+ $this->debug_message("*** Redirected to $redirect_url");
+ return $this->get_passport_ticket($redirect_url);
+ }
+ }
+
+ // no Redurect faultcode or URL
+ // we should get the ticket here
+
+ // we need ticket and secret code
+ // RST1: messengerclear.live.com
+ // <wsse:BinarySecurityToken Id="Compact1">t=tick&p=</wsse:BinarySecurityToken>
+ // <wst:BinarySecret>binary secret</wst:BinarySecret>
+ // RST2: messenger.msn.com
+ // <wsse:BinarySecurityToken Id="PPToken2">t=tick</wsse:BinarySecurityToken>
+ // RST3: contacts.msn.com
+ // <wsse:BinarySecurityToken Id="Compact3">t=tick&p=</wsse:BinarySecurityToken>
+ // RST4: messengersecure.live.com
+ // <wsse:BinarySecurityToken Id="Compact4">t=tick&p=</wsse:BinarySecurityToken>
+ // RST5: spaces.live.com
+ // <wsse:BinarySecurityToken Id="Compact5">t=tick&p=</wsse:BinarySecurityToken>
+ // RST6: storage.msn.com
+ // <wsse:BinarySecurityToken Id="Compact6">t=tick&p=</wsse:BinarySecurityToken>
+ preg_match("#".
+ "<wsse\:BinarySecurityToken Id=\"Compact1\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "<wst\:BinarySecret>(.*)</wst\:BinarySecret>(.*)".
+ "<wsse\:BinarySecurityToken Id=\"PPToken2\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "<wsse\:BinarySecurityToken Id=\"Compact3\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "<wsse\:BinarySecurityToken Id=\"Compact4\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "<wsse\:BinarySecurityToken Id=\"Compact5\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "<wsse\:BinarySecurityToken Id=\"Compact6\">(.*)</wsse\:BinarySecurityToken>(.*)".
+ "#",
+ $data, $matches);
+
+ // no ticket found!
+ if (count($matches) == 0) {
+ $this->debug_message('*** Could not get passport ticket!');
+ return false;
+ }
+
+ //$this->debug_message(var_export($matches, true));
+ // matches[0]: all data
+ // matches[1]: RST1 (messengerclear.live.com) ticket
+ // matches[2]: ...
+ // matches[3]: RST1 (messengerclear.live.com) binary secret
+ // matches[4]: ...
+ // matches[5]: RST2 (messenger.msn.com) ticket
+ // matches[6]: ...
+ // matches[7]: RST3 (contacts.msn.com) ticket
+ // matches[8]: ...
+ // matches[9]: RST4 (messengersecure.live.com) ticket
+ // matches[10]: ...
+ // matches[11]: RST5 (spaces.live.com) ticket
+ // matches[12]: ...
+ // matches[13]: RST6 (storage.live.com) ticket
+ // matches[14]: ...
+
+ // so
+ // ticket => $matches[1]
+ // secret => $matches[3]
+ // web_ticket => $matches[5]
+ // contact_ticket => $matches[7]
+ // oim_ticket => $matches[9]
+ // space_ticket => $matches[11]
+ // storage_ticket => $matches[13]
+
+ // yes, we get ticket
+ $aTickets = array(
+ 'ticket' => html_entity_decode($matches[1]),
+ 'secret' => html_entity_decode($matches[3]),
+ 'web_ticket' => html_entity_decode($matches[5]),
+ 'contact_ticket' => html_entity_decode($matches[7]),
+ 'oim_ticket' => html_entity_decode($matches[9]),
+ 'space_ticket' => html_entity_decode($matches[11]),
+ 'storage_ticket' => html_entity_decode($matches[13])
+ );
+ $this->ticket = $aTickets;
+ //$this->debug_message(var_export($aTickets, true));
+ $ABAuthHeaderArray = array(
+ 'ABAuthHeader' => array(
+ ':' => array('xmlns' => 'http://www.msn.com/webservices/AddressBook'),
+ 'ManagedGroupRequest' => false,
+ 'TicketToken' => htmlspecialchars($this->ticket['contact_ticket']),
+ )
+ );
+ $this->ABAuthHeader = new SoapHeader('http://www.msn.com/webservices/AddressBook', 'ABAuthHeader', $this->Array2SoapVar($ABAuthHeaderArray));
+ return $aTickets;
+ }
+
+ /**
+ * Generate the data to send a message
+ *
+ * @param string $sMessage Message
+ * @param integer $network Network
+ * @return string Message data
+ */
+ private function getMessage($sMessage, $network = 1) {
+ $msg_header = "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nX-MMS-IM-Format: FN=$this->font_fn; EF=$this->font_ef; CO=$this->font_co; CS=0; PF=22\r\n\r\n";
+ $msg_header_len = strlen($msg_header);
+ if ($network == 1)
+ $maxlen = self::MAX_MSN_MESSAGE_LEN - $msg_header_len;
+ else
+ $maxlen = self::MAX_YAHOO_MESSAGE_LEN - $msg_header_len;
+ $sMessage = str_replace("\r", '', $sMessage);
+ $msg = substr($sMessage, 0, $maxlen);
+ return $msg_header.$msg;
+ }
+
+ /**
+ * Sleep for the given number of seconds
+ *
+ * @param integer $wait Number of seconds to sleep for
+ */
+ private function NSRetryWait($wait) {
+ $this->debug_message("*** Sleeping for $wait seconds before retrying");
+ sleep($wait);
+ }
+
+ /**
+ * Sends a ping command
+ *
+ * Should be called about every 50 seconds
+ *
+ * @return void
+ */
+ public function sendPing() {
+ // NS: >>> PNG
+ $this->ns_writeln("PNG");
+ }
+
+ /**
+ * Methods to add / call callbacks
+ */
+
+ /**
+ * Calls User Handler
+ *
+ * Calls registered handler for a specific event.
+ *
+ * @param string $event Command (event) name (Rvous etc)
+ * @param array $data Data
+ * @see registerHandler
+ * @return void
+ */
+ private function callHandler($event, $data = NULL) {
+ if (isset($this->myEventHandlers[$event])) {
+ if ($data !== NULL) {
+ call_user_func($this->myEventHandlers[$event], $data);
+ } else {
+ call_user_func($this->myEventHandlers[$event]);
+ }
+ }
+ }
+
+ /**
+ * Registers a user handler
+ *
+ * Handler List
+ * IMIn, SessionReady, Pong, ConnectFailed, Reconnect,
+ * AddedToList, RemovedFromList, StatusChange
+ *
+ * @param string $event Event name
+ * @param string $handler User function to call
+ * @see callHandler
+ * @return boolean true if successful
+ */
+ public function registerHandler($event, $handler) {
+ if (is_callable($handler)) {
+ $this->myEventHandlers[$event] = $handler;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Debugging methods
+ */
+
+ /**
+ * Print message if debugging is enabled
+ *
+ * @param string $str Message to print
+ */
+ private function debug_message($str) {
+ if (!$this->debug) return;
+ echo $str."\n";
+ }
+
+ /**
+ * Dump binary data
+ *
+ * @param string $str Data string
+ * @return Binary data
+ */
+ private function dump_binary($str) {
+ $buf = '';
+ $a_str = '';
+ $h_str = '';
+ $len = strlen($str);
+ for ($i = 0; $i < $len; $i++) {
+ if (($i % 16) == 0) {
+ if ($buf !== '') {
+ $buf .= "$h_str $a_str\n";
+ }
+ $buf .= sprintf("%04X:", $i);
+ $a_str = '';
+ $h_str = '';
+ }
+ $ch = ord($str[$i]);
+ if ($ch < 32)
+ $a_str .= '.';
+ else
+ $a_str .= chr($ch);
+ $h_str .= sprintf(" %02X", $ch);
+ }
+ if ($h_str !== '')
+ $buf .= "$h_str $a_str\n";
+ return $buf;
+ }
+
+ function mhash_sha1($data, $key)
+ {
+ if (extension_loaded("mhash"))
+ return mhash(MHASH_SHA1, $data, $key);
+
+ if (function_exists("hash_hmac"))
+ return hash_hmac('sha1', $data, $key, true);
+
+ // RFC 2104 HMAC implementation for php. Hacked by Lance Rushing
+ $b = 64;
+ if (strlen($key) > $b)
+ $key = pack("H*", sha1($key));
+ $key = str_pad($key, $b, chr(0x00));
+ $ipad = str_pad("", $b, chr(0x36));
+ $opad = str_pad("", $b, chr(0x5c));
+ $k_ipad = $key ^ $ipad ;
+ $k_opad = $key ^ $opad;
+
+ $sha1_value = sha1($k_opad . pack("H*", sha1($k_ipad . $data)));
+
+ $hash_data = '';
+ $str = join('',explode('\x', $sha1_value));
+ $len = strlen($str);
+ for ($i = 0; $i < $len; $i += 2)
+ $hash_data .= chr(hexdec(substr($str, $i, 2)));
+ return $hash_data;
+ }
+}
diff --git a/plugins/Msn/extlib/phpmsnclass/msnbot.php b/plugins/Msn/extlib/phpmsnclass/msnbot.php new file mode 100755 index 000000000..7a9f66ca2 --- /dev/null +++ b/plugins/Msn/extlib/phpmsnclass/msnbot.php @@ -0,0 +1,63 @@ +#!/usr/bin/php +<?php +global $msn; +function ChildSignalFunction($signal) +{ + global $msn; + switch($signal) + { + case SIGTRAP: + case SIGTERM: + case SIGHUP: + if(is_object($msn)) $msn->End(); + return; + } +} + +// network: +// 1: WLM/MSN +// 2: LCS +// 4: Mobile Phones +// 32: Yahoo! +function getNetworkName($network) +{ + switch ($network) + { + case 1: + return 'WLM/MSN'; + case 2: + return 'LCS'; + case 4: + return 'Mobile Phones'; + case 32: + return 'Yahoo!'; + } + return "Unknown ($network)"; +} + + +require_once('config.php'); +include_once('msn.class.php'); + +$msn = new MSN(array( + 'user' => 'xxx@hotmail.com', + 'password' => 'mypassword', + 'alias' => 'myalias', + 'psm' => 'psm', +// 'PhotoSticker' => 'msntitle.jpg', + 'debug'=> true, +/* 'Emotions' => array( + 'aaa' => 'emotion.gif' + ),*/ +)); + +$fp=fopen(MSN_CLASS_LOG_DIR.DIRECTORY_SEPARATOR.'msnbot.pid', 'wt'); +if($fp) +{ + fputs($fp,posix_getpid()); + fclose($fp); +} +declare(ticks = 1); +$msn->Run(); +$msn->log_message("done!"); +@unlink(dirname($_SERVER['argv'][0]).DIRECTORY_SEPARATOR.'log'.DIRECTORY_SEPARATOR.'msnbot.pid'); diff --git a/plugins/Msn/extlib/phpmsnclass/soap/msnab_datatypes.xsd b/plugins/Msn/extlib/phpmsnclass/soap/msnab_datatypes.xsd new file mode 100644 index 000000000..46fc23f91 --- /dev/null +++ b/plugins/Msn/extlib/phpmsnclass/soap/msnab_datatypes.xsd @@ -0,0 +1,832 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- edited with XMLSpy v2008 sp1 (http://www.altova.com) by wp (freezingsoft) -->
+<xsd:schema xmlns:msnab="http://www.msn.com/webservices/AddressBook" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" targetNamespace="http://www.msn.com/webservices/AddressBook" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0">
+ <xsd:complexType name="abInfoType">
+ <xsd:sequence>
+ <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="ownerPuid" type="xsd:string"/>
+ <xsd:element name="OwnerCID" type="xsd:integer" minOccurs="0"/>
+ <xsd:element name="ownerEmail" type="xsd:string"/>
+ <xsd:element name="fDefault" type="xsd:boolean"/>
+ <xsd:element name="joinedNamespace" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="IsBot" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="IsParentManaged" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="SubscribeExternalPartner" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="NotifyExternalPartner" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="AddressBookType" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="MessengerApplicationServiceCreated" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="IsBetaMigrated" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="HandleType">
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:integer"/>
+ <xsd:element name="Type" type="xsd:string" default="Messenger"/>
+ <xsd:element name="ForeignId" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ServiceType">
+ <xsd:sequence>
+ <xsd:element name="Memberships" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Membership" type="msnab:Membership" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Info" type="msnab:InfoType"/>
+ <xsd:element name="Changes" type="xsd:string"/>
+ <xsd:element name="LastChange" type="xsd:dateTime" default="0001-01-01T00:00:00"/>
+ <xsd:element name="Deleted" type="xsd:boolean" default="false"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="Membership">
+ <xsd:sequence>
+ <xsd:element name="MemberRole" type="xsd:string"/>
+ <xsd:element name="Members">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Member" type="msnab:BaseMember" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="MembershipIsComplete" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="BaseMember">
+ <xsd:sequence>
+ <xsd:element name="MembershipId" type="xsd:positiveInteger" minOccurs="0"/>
+ <xsd:element name="Type" type="xsd:string"/>
+ <xsd:element name="Location" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Id" type="msnab:Guid"/>
+ <xsd:element name="IsPassportNameHidden" type="xsd:boolean"/>
+ <xsd:element name="CID" type="xsd:long"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="DisplayName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="State" type="msnab:MemberState"/>
+ <xsd:element name="Annotations" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Annotation" type="msnab:Annotation" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Deleted" type="xsd:boolean" default="false" minOccurs="0"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="JoinedDate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="ExpirationDate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="Changes" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CircleMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="CircleId" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="PassportMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="PassportName" type="xsd:string"/>
+ <xsd:element name="IsPassportNameHidden" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="PassportId" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="CID" type="xsd:long" minOccurs="0"/>
+ <xsd:element name="PassportChanges" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="EmailMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="Email" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="PhoneMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="PhoneNumber" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="RoleMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:string"/>
+ <xsd:element name="DefiningService">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:integer"/>
+ <xsd:element name="Type" type="xsd:string"/>
+ <xsd:element name="ForeignId" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="MaxRoleRecursionDepth" type="xsd:integer"/>
+ <xsd:element name="MaxDegreesSeparation" type="xsd:integer"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="ServiceMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="Service" type="msnab:HandleType"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="DomainMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="DomainName" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="EveryoneMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember"/>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="GroupMember" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseMember">
+ <xsd:sequence>
+ <xsd:element name="Id" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:simpleType name="Guid">
+ <xsd:restriction base="xsd:string">
+ <xsd:pattern value="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:element name="MemberType">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="Allow"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:element>
+ <xsd:simpleType name="MemberState">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="Accepted"/>
+ <xsd:enumeration value="Pending"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:complexType name="Annotation">
+ <xsd:sequence>
+ <xsd:element name="Name" type="xsd:string"/>
+ <xsd:element name="Value" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContactType">
+ <xsd:sequence>
+ <xsd:element name="contactId" type="msnab:Guid" minOccurs="0"/>
+ <xsd:element name="contactInfo" type="msnab:contactInfoType" minOccurs="0"/>
+ <xsd:element name="propertiesChanged" type="xsd:string" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>
+ A space (ASCII #32) separated list of properties that
+ have changed as part of an update request. The property
+ names don't always match the name of the associated
+ element.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="fDeleted" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="lastChange" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="CreateDate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="LastModifiedBy" type="xsd:integer" minOccurs="0"/>
+ <xsd:element name="CreatedBy" type="xsd:integer" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContactIdType">
+ <xsd:sequence>
+ <xsd:element name="contactId" type="msnab:Guid" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="contactInfoType">
+ <xsd:sequence>
+ <xsd:element name="emails" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ContactEmail" type="msnab:contactEmailType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="phones" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ContactPhone" type="msnab:contactPhoneType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="locations" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ContactLocation" type="msnab:contactLocationType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="webSites" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ContactWebSite" type="msnab:contactWebSiteType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="annotations" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Annotation" type="msnab:Annotation" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupIds" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupIdsDeleted" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="contactType" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="quickName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="firstName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="MiddleName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="lastName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="Suffix" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="NameTitle" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="passportName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="IsPassportNameHidden" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="displayName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="puid" type="xsd:long" minOccurs="0"/>
+ <xsd:element name="CID" type="xsd:long" minOccurs="0"/>
+ <xsd:element name="BrandIdList" type="xsd:anyType" minOccurs="0"/>
+ <xsd:element name="comment" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="IsNotMobileVisible" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="isMobileIMEnabled" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="isMessengerUser" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="isFavorite" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="isSmtp" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="hasSpace" type="xsd:boolean" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>
+ Indicates whether the contact has a Windows Live
+ Space or not.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="spotWatchState" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="birthdate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="primaryEmailType" type="msnab:ContactEmailTypeType" minOccurs="0"/>
+ <xsd:element name="PrimaryLocation" type="msnab:ContactLocationTypeType" minOccurs="0"/>
+ <xsd:element name="PrimaryPhone" type="msnab:ContactPhoneTypeType" minOccurs="0"/>
+ <xsd:element name="IsPrivate" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="Anniversary" type="xsd:string" minOccurs="0">
+ <xsd:annotation>
+ <xsd:documentation>
+ Seen is YYYY/MM/DD format.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="Gender" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="TimeZone" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="NetworkInfoList" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="NetworkInfo" type="msnab:NetworkInfoType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="PublicDisplayName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="IsAutoUpdateDisabled" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="MessengerMemberInfo" type="msnab:MessengerMemberInfo" minOccurs="0"/>
+ <xsd:element name="PropertiesChanged" type="xsd:anyType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="contactEmailType">
+ <xsd:sequence>
+ <xsd:element name="contactEmailType" type="msnab:ContactEmailTypeType"/>
+ <xsd:element name="email" type="xsd:string"/>
+ <xsd:element name="isMessengerEnabled" type="xsd:boolean"/>
+ <xsd:element name="Capability" type="xsd:integer"/>
+ <xsd:element name="MessengerEnabledExternally" type="xsd:boolean"/>
+ <xsd:element name="propertiesChanged" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:simpleType name="ContactEmailTypeType">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="ContactEmailPersonal"/>
+ <xsd:enumeration value="ContactEmailBusiness"/>
+ <xsd:enumeration value="ContactEmailOther"/>
+ <xsd:enumeration value="ContactEmailMessenger"/>
+ <xsd:enumeration value="Messenger2"/>
+ <xsd:enumeration value="Messenger3"/>
+ <xsd:enumeration value="Messenger4"/>
+ <xsd:enumeration value="Passport"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:complexType name="contactPhoneType">
+ <xsd:sequence>
+ <xsd:element name="contactPhoneType" type="msnab:ContactPhoneTypeType"/>
+ <xsd:element name="number" type="xsd:string"/>
+ <xsd:element name="isMessengerEnabled" type="xsd:boolean"/>
+ <xsd:element name="propertiesChanged" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:simpleType name="ContactPhoneTypeType">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="ContactPhonePersonal"/>
+ <xsd:enumeration value="ContactPhoneBusiness"/>
+ <xsd:enumeration value="ContactPhoneMobile"/>
+ <xsd:enumeration value="ContactPhonePager"/>
+ <xsd:enumeration value="ContactPhoneOther"/>
+ <xsd:enumeration value="ContactPhoneFax"/>
+ <xsd:enumeration value="Personal2"/>
+ <xsd:enumeration value="Business2"/>
+ <xsd:enumeration value="BusinessFax"/>
+ <xsd:enumeration value="BusinessMobile"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:complexType name="contactLocationType">
+ <xsd:sequence>
+ <xsd:element name="contactLocationType" type="msnab:ContactLocationTypeType"/>
+ <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="street" type="xsd:string"/>
+ <xsd:element name="city" type="xsd:string"/>
+ <xsd:element name="state" type="xsd:string"/>
+ <xsd:element name="country" type="xsd:string"/>
+ <xsd:element name="postalCode" type="xsd:string"/>
+ <xsd:element name="Department" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="Changes" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:simpleType name="ContactLocationTypeType">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="ContactLocationPersonal"/>
+ <xsd:enumeration value="ContactLocationBusiness"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:complexType name="contactWebSiteType">
+ <xsd:sequence>
+ <xsd:element name="contactWebSiteType" type="msnab:ContactWebSiteTypeType"/>
+ <xsd:element name="webURL" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:simpleType name="ContactWebSiteTypeType">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="ContactWebSitePersonal"/>
+ <xsd:enumeration value="ContactWebSiteBusiness"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:complexType name="GroupType">
+ <xsd:sequence>
+ <xsd:element name="groupId" type="msnab:Guid"/>
+ <xsd:element name="groupInfo" type="msnab:groupInfoType"/>
+ <xsd:element name="propertiesChanged" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ A space (ASCII #32) separated list of properties that
+ have changed as part of an update request. The property
+ names don't always match the name of the associated
+ element.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="fDeleted" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="lastChange" type="xsd:dateTime" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="groupInfoType">
+ <xsd:sequence>
+ <xsd:element name="annotations" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Annotation" type="msnab:Annotation" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupType" type="msnab:Guid" default="C8529CE2-6EAD-434d-881F-341E17DB3FF8" minOccurs="0"/>
+ <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="IsNotMobileVisible" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="IsPrivate" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="IsFavorite" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="fMessenger" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="groupFilterType">
+ <xsd:sequence>
+ <xsd:element name="groupIds">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="InvalidPassportUser">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="errorcode" type="xsd:string"/>
+ <xsd:element name="errorstring" type="xsd:string"/>
+ <xsd:element name="additionalDetails">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="originalExceptionErrorMessage" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:complexType name="MessengerMemberInfo">
+ <xsd:sequence>
+ <xsd:element name="PendingAnnotations" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Annotation" type="msnab:Annotation" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="DisplayName" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="InfoType">
+ <xsd:sequence>
+ <xsd:element name="Handle" type="msnab:HandleType"/>
+ <xsd:element name="DisplayName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="InverseRequired" type="xsd:boolean" default="false"/>
+ <xsd:element name="AuthorizationCriteria" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="RSSUrl" type="xsd:anyURI" minOccurs="0"/>
+ <xsd:element name="IsBot" type="xsd:boolean" default="false"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="NotificationDataType">
+ <xsd:sequence>
+ <xsd:element name="StoreService" type="msnab:ServiceType"/>
+ <xsd:element name="Status" type="xsd:string"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime"/>
+ <xsd:element name="Gleam" type="xsd:boolean" default="false"/>
+ <xsd:element name="InstanceId" type="xsd:string" default="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="BaseDynamicItemType">
+ <xsd:sequence>
+ <xsd:element name="Type" type="xsd:string"/>
+ <xsd:element name="Deleted" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="Notifications" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="NotificationData" type="msnab:NotificationDataType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Changes" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CircleDynamicItem" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseDynamicItemType">
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="PassportDynamicItem" block="" mixed="false">
+ <xsd:complexContent mixed="false">
+ <xsd:extension base="msnab:BaseDynamicItemType">
+ <xsd:sequence>
+ <xsd:element name="CID" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="PassportName" type="xsd:string"/>
+ <xsd:element name="PassportId" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="SpaceStatus" type="xsd:string"/>
+ <xsd:element name="SpaceLastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="SpaceLastViewed" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="SpaceGleam" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="ProfileLastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="ProfileLastView" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="ProfileStatus" type="xsd:string"/>
+ <xsd:element name="ProfileGleam" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="ContactProfileStatus" type="xsd:string"/>
+ <xsd:element name="ContactProfileLastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="ContactProfileLastViewed" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="LiveContactLastChanged" type="xsd:dateTime" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="abType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid"/>
+ <xsd:element name="abInfo" type="msnab:abInfoType"/>
+ <xsd:element name="lastChange" type="xsd:dateTime"/>
+ <xsd:element name="DynamicItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="RecentActivityItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="createDate" type="xsd:dateTime"/>
+ <xsd:element name="propertiesChanged" type="xsd:anyType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CircleResultType">
+ <xsd:sequence>
+ <xsd:element name="Circles" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="CircleInverseInfo" type="msnab:CircleInverseInfoType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CircleTicket" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="NetworkInfoType">
+ <xsd:sequence>
+ <xsd:element name="DomainId" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="DomainTag" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="UserTileURL" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="ProfileURL" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="DisplayName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="RelationshipType" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="RelationshipState" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="RelationshipStateDate" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="RelationshipRole" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="NDRCount" type="xsd:int" minOccurs="0"/>
+ <xsd:element name="InviterCID" type="xsd:long" minOccurs="0"/>
+ <xsd:element name="CreateDate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="PropertiesChanged" type="xsd:anyType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContactFilterType">
+ <xsd:sequence>
+ <xsd:element name="IncludeHiddenContacts" type="xsd:boolean"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="filterOptionsType">
+ <xsd:sequence>
+ <xsd:element name="DeltasOnly" type="xsd:boolean"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="ContactFilter" type="msnab:ContactFilterType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="entityHandle">
+ <xsd:sequence>
+ <xsd:element name="Cid" type="xsd:long"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="NotationType">
+ <xsd:sequence>
+ <xsd:element name="Name" type="xsd:string"/>
+ <xsd:element name="Value" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ListTemplateVariableItemType">
+ <xsd:sequence>
+ <xsd:element name="Values">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Value" type="msnab:SimpleTemplateVariableBaseType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="TemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Name" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="SimpleTemplateVariableBaseType">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:TemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Value" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="PublisherIdTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:TemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:string"/>
+ <xsd:element name="NameHint" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="TargetIdTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:PublisherIdTemplateVariable">
+ <xsd:sequence>
+ <xsd:element name="IdOwner" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="TextTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:SimpleTemplateVariableBaseType"/>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="HlinkTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:SimpleTemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Text" type="xsd:string"/>
+ <xsd:element name="Notations">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Notation" type="msnab:NotationType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="ListTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:TemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Items">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ListTemplateVariableItem" type="msnab:ListTemplateVariableItemType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="ImageTemplateVariable">
+ <xsd:complexContent>
+ <xsd:extension base="msnab:SimpleTemplateVariableBaseType">
+ <xsd:sequence>
+ <xsd:element name="Href" type="xsd:anyURI"/>
+ <xsd:element name="Notations">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Notation" type="msnab:NotationType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="ActivityDetailsType">
+ <xsd:sequence>
+ <xsd:element name="OwnerCID" type="xsd:string"/>
+ <xsd:element name="ObjectId" type="xsd:string"/>
+ <xsd:element name="ApplicationId" type="xsd:string"/>
+ <xsd:element name="ChangeType" type="xsd:string"/>
+ <xsd:element name="PublishDate" type="xsd:dateTime"/>
+ <xsd:element name="TemplateVariables">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="TemplateVariable" type="msnab:TemplateVariableBaseType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="RecentActivityTemplateType">
+ <xsd:sequence>
+ <xsd:element name="Cardinality" type="xsd:string"/>
+ <xsd:element name="Data" type="xsd:string"/>
+ <xsd:element name="Title" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="RequestedLocalesType">
+ <xsd:sequence>
+ <xsd:element name="string" type="xsd:string" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="RecentActivityTemplateContainerType">
+ <xsd:sequence>
+ <xsd:element name="ApplicationId" type="xsd:string"/>
+ <xsd:element name="ApplicationName" type="xsd:string"/>
+ <xsd:element name="ChangeType" type="xsd:integer"/>
+ <xsd:element name="Locale" type="xsd:string"/>
+ <xsd:element name="RequestedLocales" type="msnab:RequestedLocalesType"/>
+ <xsd:element name="TemplateRevision" type="xsd:integer"/>
+ <xsd:element name="Templates">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="RecentActivityTemplate" type="msnab:RecentActivityTemplateType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CollapseCondition" type="msnab:CollapseConditionType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CollapseConditionType">
+ <xsd:sequence>
+ <xsd:element name="string" type="xsd:string" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CirclePersonalMembershipType">
+ <xsd:sequence>
+ <xsd:element name="Role" type="xsd:string"/>
+ <xsd:element name="State" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="abHandleType">
+ <xsd:sequence>
+ <xsd:element name="ABId" type="xsd:string"/>
+ <xsd:element name="Puid" type="xsd:long"/>
+ <xsd:element name="Cid" type="xsd:long"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="contactHandleType">
+ <xsd:sequence>
+ <xsd:element name="Email" type="xsd:string"/>
+ <xsd:element name="Puid" type="xsd:long"/>
+ <xsd:element name="Cid" type="xsd:long"/>
+ <xsd:element name="CircleId" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="MembershipInfoType">
+ <xsd:sequence>
+ <xsd:element name="CirclePersonalMembership" type="msnab:CirclePersonalMembershipType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="PersonalInfoType">
+ <xsd:sequence>
+ <xsd:element name="MembershipInfo" type="msnab:MembershipInfoType"/>
+ <xsd:element name="Name" type="xsd:string"/>
+ <xsd:element name="IsNotMobileVisible" type="xsd:boolean"/>
+ <xsd:element name="IsFavorite" type="xsd:boolean"/>
+ <xsd:element name="IsFamily" type="xsd:boolean"/>
+ <xsd:element name="Changes" type="xsd:anyType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContentInfoType">
+ <xsd:sequence>
+ <xsd:element name="Domain" type="xsd:int"/>
+ <xsd:element name="HostedDomain" type="xsd:string"/>
+ <xsd:element name="Type" type="xsd:int"/>
+ <xsd:element name="MembershipAccess" type="xsd:int"/>
+ <xsd:element name="IsPresenceEnabled" type="xsd:boolean"/>
+ <xsd:element name="RequestMembershipOption" type="xsd:int"/>
+ <xsd:element name="DisplayName" type="xsd:string"/>
+ <xsd:element name="ProfileLastUpdated" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="Changes" type="xsd:anyType" minOccurs="0"/>
+ <xsd:element name="CreateDate" type="xsd:dateTime" minOccurs="0"/>
+ <xsd:element name="LastChanged" type="xsd:dateTime" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContentHandleType">
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ContentType">
+ <xsd:sequence>
+ <xsd:element name="Handle" type="msnab:ContentHandleType"/>
+ <xsd:element name="Info" type="msnab:ContentInfoType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CircleInverseInfoType">
+ <xsd:sequence>
+ <xsd:element name="Content" type="msnab:ContentType"/>
+ <xsd:element name="PersonalInfo" type="msnab:PersonalInfoType"/>
+ <xsd:element name="Deleted" type="xsd:boolean"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="callerInfoType">
+ <xsd:sequence>
+ <xsd:element name="PublicDisplayName" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+</xsd:schema>
diff --git a/plugins/Msn/extlib/phpmsnclass/soap/msnab_servicetypes.xsd b/plugins/Msn/extlib/phpmsnclass/soap/msnab_servicetypes.xsd new file mode 100644 index 000000000..3fa9798b6 --- /dev/null +++ b/plugins/Msn/extlib/phpmsnclass/soap/msnab_servicetypes.xsd @@ -0,0 +1,567 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- edited with XMLSpy v2008 sp1 (http://www.altova.com) by wp (freezingsoft) -->
+<xsd:schema xmlns:msnab="http://www.msn.com/webservices/AddressBook" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.msn.com/webservices/AddressBook" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.0">
+ <xsd:include schemaLocation="msnab_datatypes.xsd"/>
+ <xsd:element name="ABApplicationHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ApplicationId" type="msnab:Guid" fixed="09607671-1C32-421F-A6A6-CBFAA51AB5F4"/>
+ <xsd:element name="IsMigration" type="xsd:boolean" default="false"/>
+ <xsd:element name="PartnerScenario" type="xsd:string" default="Initial"/>
+ <xsd:element name="CacheKey" type="xsd:token" minOccurs="0"/>
+ <xsd:element name="BrandId" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABAuthHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ManagedGroupRequest" type="xsd:boolean" default="false"/>
+ <xsd:element name="TicketToken" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ServiceHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Version" type="xsd:token"/>
+ <xsd:element name="CacheKey" type="xsd:token" minOccurs="0"/>
+ <xsd:element name="CacheKeyChanged" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="PreferredHostName" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="SessionId" type="msnab:Guid" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="FindMembership" type="msnab:FindMembershipRequestType"/>
+ <xsd:complexType name="FindMembershipRequestType">
+ <xsd:sequence>
+ <xsd:element name="serviceFilter">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Types">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ServiceType" type="xsd:string" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="View" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="deltasOnly" type="xsd:boolean" default="false" minOccurs="0"/>
+ <xsd:element name="lastChange" type="xsd:dateTime" default="0001-01-01T00:00:00.0000000-08:00" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="FindMembershipResultType">
+ <xsd:sequence>
+ <xsd:element name="Services">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Service" type="msnab:ServiceType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="OwnerNamespace">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Info">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Handle">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Id" type="xsd:string"/>
+ <xsd:element name="IsPassportNameHidden" type="xsd:boolean"/>
+ <xsd:element name="CID" type="xsd:integer"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CreatorPuid" type="xsd:integer"/>
+ <xsd:element name="CreatorCID" type="xsd:integer"/>
+ <xsd:element name="CreatorPassportName" type="xsd:string"/>
+ <xsd:element name="CircleAttributes">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="IsPresenceEnabled" type="xsd:boolean"/>
+ <xsd:element name="IsEvent" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="Domain" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="MessengerApplicationServiceCreated" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Changes" type="xsd:string"/>
+ <xsd:element name="CreateDate" type="xsd:dateTime"/>
+ <xsd:element name="LastChange" type="xsd:dateTime"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="FindMembershipResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="FindMembershipResult" type="msnab:FindMembershipResultType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABFindAll" type="msnab:ABFindAllRequestType"/>
+ <xsd:complexType name="ABFindAllRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="abView" type="xsd:string" minOccurs="0"/>
+ <xsd:element name="deltasOnly" type="xsd:boolean" default="false" minOccurs="0"/>
+ <xsd:element name="lastChange" type="xsd:dateTime" default="0001-01-01T00:00:00.0000000-08:00" minOccurs="0"/>
+ <xsd:element name="dynamicItemView" type="xsd:string" fixed="Gleam" minOccurs="0"/>
+ <xsd:element name="dynamicItemLastChange" type="xsd:dateTime" default="0001-01-01T00:00:00.0000000-08:00" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ABFindAllResultType">
+ <xsd:sequence>
+ <xsd:element name="groups" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Group" type="msnab:GroupType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="DynamicItems" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="DynamicItem" type="msnab:BaseDynamicItemType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CircleResult">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="CircleTicket" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ab">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid"/>
+ <xsd:element name="abInfo" type="msnab:abInfoType"/>
+ <xsd:element name="lastChange" type="xsd:dateTime"/>
+ <xsd:element name="DynamicItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="RecentActivityItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="createDate" type="xsd:dateTime"/>
+ <xsd:element name="propertiesChanged" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABFindAllResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ABFindAllResult" type="msnab:ABFindAllResultType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABContactAdd" type="msnab:ABContactAddRequestType"/>
+ <xsd:complexType name="ABContactAddRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="options" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="EnableAllowListManagement" type="xsd:boolean"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ABContactAddResultType">
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABContactAddResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ABContactAddResult" type="msnab:ABContactAddResultType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABContactDelete" type="msnab:ABContactDeleteRequestType"/>
+ <xsd:complexType name="ABContactDeleteRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactIdType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABContactDeleteResponse"/>
+ <xsd:element name="ABGroupContactAdd" type="msnab:ABGroupContactAddRequestType"/>
+ <xsd:complexType name="ABGroupContactAddRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="groupFilter" type="msnab:groupFilterType"/>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupContactAddOptions" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="fGenerateMissingQuickName" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="EnableAllowListManagement" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ABGroupContactAddResultType">
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABGroupContactAddResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ABGroupContactAddResult" type="msnab:ABGroupContactAddResultType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABGroupAdd" type="msnab:ABGroupAddRequestType"/>
+ <xsd:complexType name="ABGroupAddRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="groupAddOptions">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="fRenameOnMsgrConflict" type="xsd:boolean" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupInfo">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="GroupInfo" type="msnab:groupInfoType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ABGroupAddResultType">
+ <xsd:sequence>
+ <xsd:element name="guid" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABGroupAddResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ABGroupAddResult" type="msnab:ABGroupAddResultType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ABGroupUpdate" type="msnab:ABGroupUpdateRequestType"/>
+ <xsd:complexType name="ABGroupUpdateRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="groups">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Group" type="msnab:GroupType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABGroupUpdateResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:element name="ABGroupDelete" type="msnab:ABGroupDeleteRequestType"/>
+ <xsd:complexType name="ABGroupDeleteRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="groupFilter" type="msnab:groupFilterType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABGroupDeleteResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:element name="ABContactUpdate" type="msnab:ABContactUpdateRequestType"/>
+ <xsd:complexType name="ABContactUpdateRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABContactUpdateResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:element name="ABGroupContactDelete" type="msnab:ABGroupContactDeleteRequestType"/>
+ <xsd:complexType name="ABGroupContactDeleteRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid" fixed="00000000-0000-0000-0000-000000000000"/>
+ <xsd:element name="contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="groupFilter" type="msnab:groupFilterType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABGroupContactDeleteResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:element name="AddMember" type="msnab:AddMemberRequestType"/>
+ <xsd:complexType name="AddMemberRequestType">
+ <xsd:sequence>
+ <xsd:element name="serviceHandle" type="msnab:HandleType"/>
+ <xsd:element name="memberships">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Membership" type="msnab:Membership" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="AddMemberResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:element name="DeleteMember" type="msnab:DeleteMemberRequestType"/>
+ <xsd:complexType name="DeleteMemberRequestType">
+ <xsd:sequence>
+ <xsd:element name="serviceHandle" type="msnab:HandleType"/>
+ <xsd:element name="memberships">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Membership" type="msnab:Membership" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="nsHandle" type="msnab:ContentHandleType" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="DeleteMemberResponse">
+ <xsd:complexType/>
+ </xsd:element>
+ <xsd:complexType name="ABAddResponseType">
+ <xsd:sequence>
+ <xsd:element name="ABAddResult" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABAddResponse" type="msnab:ABAddResponseType"/>
+ <xsd:element name="ABAdd" type="msnab:ABAddRequestType"/>
+ <xsd:complexType name="ABAddRequestType">
+ <xsd:sequence>
+ <xsd:element name="abInfo" type="msnab:abInfoType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="UpdateDynamicItemRequestType">
+ <xsd:sequence>
+ <xsd:element name="abId" type="xsd:string"/>
+ <xsd:element name="dynamicItems">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="DynamicItem" type="msnab:BaseDynamicItemType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="UpdateDynamicItem" type="msnab:UpdateDynamicItemRequestType"/>
+ <xsd:element name="UpdateDynamicItemResponse"/>
+ <xsd:element name="ABFindContactsPaged" type="msnab:ABFindContactsPagedRequestType"/>
+ <xsd:complexType name="ABFindContactsPagedRequestType">
+ <xsd:sequence>
+ <xsd:element name="filterOptions" type="msnab:filterOptionsType"/>
+ <xsd:element name="abView" type="xsd:string"/>
+ <xsd:element name="extendedContent" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ABFindContactsPagedResultType">
+ <xsd:sequence>
+ <xsd:element name="Groups" minOccurs="0">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Group" type="msnab:GroupType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Contacts">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Contact" type="msnab:ContactType" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CircleResult" type="msnab:CircleResultType"/>
+ <xsd:element name="Ab">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="abId" type="msnab:Guid"/>
+ <xsd:element name="abInfo" type="msnab:abInfoType"/>
+ <xsd:element name="lastChange" type="xsd:dateTime"/>
+ <xsd:element name="DynamicItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="RecentActivityItemLastChanged" type="xsd:dateTime"/>
+ <xsd:element name="createDate" type="xsd:dateTime"/>
+ <xsd:element name="propertiesChanged" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ABFindContactsPagedResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ABFindContactsPagedResult" type="msnab:ABFindContactsPagedResultType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="WNApplicationHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ApplicationId" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="WNAuthHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="TicketToken" type="xsd:string"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="WNServiceHeader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="Version" type="xsd:token"/>
+ <xsd:element name="CacheKey" type="xsd:token" minOccurs="0"/>
+ <xsd:element name="CacheKeyChanged" type="xsd:boolean" minOccurs="0"/>
+ <xsd:element name="PreferredHostName" type="xsd:string" minOccurs="0"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="GetContactsRecentActivity" type="msnab:GetContactsRecentActivityRequestType"/>
+ <xsd:complexType name="GetContactsRecentActivityRequestType">
+ <xsd:sequence>
+ <xsd:element name="entityHandle" type="msnab:entityHandle"/>
+ <xsd:element name="locales">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="string" type="xsd:string" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="count" type="xsd:int"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="GetContactsRecentActivityResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="GetContactsRecentActivityResult" type="msnab:GetContactsRecentActivityResultType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:complexType name="GetContactsRecentActivityResultType">
+ <xsd:sequence>
+ <xsd:element name="Activities">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ActivityDetails" type="msnab:ActivityDetailsType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="Templates">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="RecentActivityTemplateContainer" type="msnab:RecentActivityTemplateContainerType" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="FeedUrl" type="xsd:anyURI"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ManageWLConnection" type="msnab:ManageWLConnectionRequestType"/>
+ <xsd:complexType name="ManageWLConnectionRequestType">
+ <xsd:sequence>
+ <xsd:element name="abHandle" type="msnab:abHandleType"/>
+ <xsd:element name="contactId" type="xsd:string"/>
+ <xsd:element name="connection" type="xsd:boolean"/>
+ <xsd:element name="presence" type="xsd:boolean"/>
+ <xsd:element name="action" type="xsd:integer"/>
+ <xsd:element name="relationshipType" type="xsd:int"/>
+ <xsd:element name="relationshipRole" type="xsd:int"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="ManageWLConnectionResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="ManageWLConnectionResult" type="msnab:ContactType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CreateContact" type="msnab:CreateContactType"/>
+ <xsd:complexType name="CreateContactType">
+ <xsd:sequence>
+ <xsd:element name="abHandle" type="msnab:abHandleType"/>
+ <xsd:element name="contactHandle" type="msnab:contactHandleType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="CreateContactResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="CreateContactResult" type="msnab:ContactType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="CreateCircle" type="msnab:CreateCircleRequestType"/>
+ <xsd:complexType name="CreateCircleRequestType">
+ <xsd:sequence>
+ <xsd:element name="properties" type="msnab:ContentInfoType"/>
+ <xsd:element name="callerInfo" type="msnab:callerInfoType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="CreateCircleResponseType">
+ <xsd:sequence>
+ <xsd:element name="Id" type="msnab:Guid"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="CreateCircleResponse">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="CreateCircleResult" type="msnab:CreateCircleResponseType"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+</xsd:schema>
diff --git a/plugins/Msn/extlib/phpmsnclass/soap/msnab_sharingservice.wsdl b/plugins/Msn/extlib/phpmsnclass/soap/msnab_sharingservice.wsdl new file mode 100644 index 000000000..7ec87f90c --- /dev/null +++ b/plugins/Msn/extlib/phpmsnclass/soap/msnab_sharingservice.wsdl @@ -0,0 +1,532 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- edited with XMLSpy v2008 sp1 (http://www.altova.com) by wp (freezingsoft) -->
+<definitions xmlns:msnab="http://www.msn.com/webservices/AddressBook" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://schemas.xmlsoap.org/soap/encoding/" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://www.msn.com/webservices/AddressBook">
+ <types>
+ <ns:schema xmlns="http://www.w3.org/2001/XMLSchema">
+ <ns:import schemaLocation="msnab_servicetypes.xsd" namespace="http://www.msn.com/webservices/AddressBook"/>
+ </ns:schema>
+ </types>
+ <message name="ABHeader">
+ <part name="ApplicationHeader" element="msnab:ABApplicationHeader"/>
+ <part name="AuthHeader" element="msnab:ABAuthHeader"/>
+ </message>
+ <message name="FindMembershipMessage">
+ <part name="FindMembershipRequest" element="msnab:FindMembership"/>
+ </message>
+ <message name="ABFindAllMessage">
+ <part name="ABFindAllRequest" element="msnab:ABFindAll"/>
+ </message>
+ <message name="ABContactAddMessage">
+ <part name="ABContactAddRequest" element="msnab:ABContactAdd"/>
+ </message>
+ <message name="ABContactDeleteMessage">
+ <part name="ABContactDeleteRequest" element="msnab:ABContactDelete"/>
+ </message>
+ <message name="ABGroupContactAddMessage">
+ <part name="ABGroupContactAddRequest" element="msnab:ABGroupContactAdd"/>
+ </message>
+ <message name="ABGroupAddMessage">
+ <part name="ABGroupAddRequest" element="msnab:ABGroupAdd"/>
+ </message>
+ <message name="ABGroupUpdateMessage">
+ <part name="ABGroupUpdateRequest" element="msnab:ABGroupUpdate"/>
+ </message>
+ <message name="ABGroupDeleteMessage">
+ <part name="ABGroupDeleteRequest" element="msnab:ABGroupDelete"/>
+ </message>
+ <message name="ABGroupContactDeleteMessage">
+ <part name="ABGroupContactDeleteRequest" element="msnab:ABGroupContactDelete"/>
+ </message>
+ <message name="ABContactUpdateMessage">
+ <part name="ABContactUpdateRequest" element="msnab:ABContactUpdate"/>
+ </message>
+ <message name="AddMemberMessage">
+ <part name="AddMemberRequest" element="msnab:AddMember"/>
+ </message>
+ <message name="DeleteMemberMessage">
+ <part name="DeleteMemberRequest" element="msnab:DeleteMember"/>
+ </message>
+ <message name="ServiceHeader">
+ <part name="ServiceHeader" element="msnab:ServiceHeader"/>
+ </message>
+ <message name="FindMembershipResponseMessage">
+ <part name="FindMembershipResponse" element="msnab:FindMembershipResponse"/>
+ </message>
+ <message name="ABFindAllResponseMessage">
+ <part name="ABFindAllResponse" element="msnab:ABFindAllResponse"/>
+ </message>
+ <message name="ABContactAddResponseMessage">
+ <part name="ABContactAddResponse" element="msnab:ABContactAddResponse"/>
+ </message>
+ <message name="ABContactDeleteResponseMessage">
+ <part name="ABContactDeleteResponse" element="msnab:ABContactDeleteResponse"/>
+ </message>
+ <message name="ABGroupContactAddResponseMessage">
+ <part name="ABGroupContactAddResponse" element="msnab:ABGroupContactAddResponse"/>
+ </message>
+ <message name="ABGroupAddResponseMessage">
+ <part name="ABGroupAddResponse" element="msnab:ABGroupAddResponse"/>
+ </message>
+ <message name="ABGroupUpdateResponseMessage">
+ <part name="ABGroupUpdateResponse" element="msnab:ABGroupUpdateResponse"/>
+ </message>
+ <message name="ABGroupDeleteResponseMessage">
+ <part name="ABGroupDeleteResponse" element="msnab:ABGroupDeleteResponse"/>
+ </message>
+ <message name="ABGroupContactDeleteResponseMessage">
+ <part name="ABGroupContactDeleteResponse" element="msnab:ABGroupContactDeleteResponse"/>
+ </message>
+ <message name="ABContactUpdateResponseMessage">
+ <part name="ABContactUpdateResponse" element="msnab:ABContactUpdateResponse"/>
+ </message>
+ <message name="AddMemberResponseMessage">
+ <part name="AddMemberResponse" element="msnab:AddMemberResponse"/>
+ </message>
+ <message name="DeleteMemberResponseMessage">
+ <part name="DeleteMemberResponse" element="msnab:DeleteMemberResponse"/>
+ </message>
+ <message name="InvalidPassportUserMessage">
+ <part name="fault" element="msnab:InvalidPassportUser"/>
+ </message>
+ <message name="ABAddMessage">
+ <part name="ABAddRequest" element="msnab:ABAdd"/>
+ </message>
+ <message name="ABAddResponseMessage">
+ <part name="ABAddResponse" element="msnab:ABAddResponse"/>
+ </message>
+ <message name="UpdateDynamicItemMessage">
+ <part name="UpdateDynamicItem" element="msnab:UpdateDynamicItem"/>
+ </message>
+ <message name="UpdateDynamicItemResponseMessage">
+ <part name="UpdateDynamicItemResponse" element="msnab:UpdateDynamicItemResponse"/>
+ </message>
+ <message name="ABFindContactsPagedMessage">
+ <part name="ABFindContactsPagedRequest" element="msnab:ABFindContactsPaged"/>
+ </message>
+ <message name="ABFindContactsPagedResponseMessage">
+ <part name="ABFindContactsPagedResponse" element="msnab:ABFindContactsPagedResponse"/>
+ </message>
+ <message name="GetContactsRecentActivityMessage">
+ <part name="GetContactsRecentActivityRequest" element="msnab:GetContactsRecentActivity"/>
+ </message>
+ <message name="GetContactsRecentActivityResponseMessage">
+ <part name="GetContactsRecentActivityResponse" element="msnab:GetContactsRecentActivityResponse"/>
+ </message>
+ <message name="WNHeader">
+ <part name="WNApplicationHeader" element="msnab:WNApplicationHeader"/>
+ <part name="WNAuthHeader" element="msnab:WNAuthHeader"/>
+ <part name="WNServiceHeader" element="msnab:WNServiceHeader"/>
+ </message>
+ <message name="CreateCircleMessage">
+ <part name="CreateCircleRequest" element="msnab:CreateCircle"/>
+ </message>
+ <message name="CreateCircleResponseMessage">
+ <part name="CreateCircleResponse" element="msnab:CreateCircleResponse"/>
+ </message>
+ <message name="CreateContactMessage">
+ <part name="CreateContactRequest" element="msnab:CreateContact"/>
+ </message>
+ <message name="CreateContactResponseMessage">
+ <part name="CreateContactResponse" element="msnab:CreateContactResponse"/>
+ </message>
+ <message name="ManageWLConnectionMessage">
+ <part name="ManageWLConnection" element="msnab:ManageWLConnection"/>
+ </message>
+ <message name="ManageWLConnectionResponseMessage">
+ <part name="ManageWLConnectionResponse" element="msnab:ManageWLConnectionResponse"/>
+ </message>
+ <portType name="SharingServicePortType">
+ <operation name="FindMembership">
+ <input message="msnab:FindMembershipMessage"/>
+ <output message="msnab:FindMembershipResponseMessage"/>
+ </operation>
+ <operation name="AddMember">
+ <input message="msnab:AddMemberMessage"/>
+ <output message="msnab:AddMemberResponseMessage"/>
+ </operation>
+ <operation name="DeleteMember">
+ <input message="msnab:DeleteMemberMessage"/>
+ <output message="msnab:DeleteMemberResponseMessage"/>
+ </operation>
+ <operation name="CreateCircle">
+ <input message="msnab:CreateCircleMessage"/>
+ <output message="msnab:CreateCircleResponseMessage"/>
+ </operation>
+ </portType>
+ <portType name="ABServicePortType">
+ <operation name="ABFindAll">
+ <input message="msnab:ABFindAllMessage"/>
+ <output message="msnab:ABFindAllResponseMessage"/>
+ </operation>
+ <operation name="ABContactAdd">
+ <input message="msnab:ABContactAddMessage"/>
+ <output message="msnab:ABContactAddResponseMessage"/>
+ <fault name="InvalidPassportUserException" message="msnab:InvalidPassportUserMessage"/>
+ </operation>
+ <operation name="ABContactDelete">
+ <input message="msnab:ABContactDeleteMessage"/>
+ <output message="msnab:ABContactDeleteResponseMessage"/>
+ <fault name="InvalidPassportUserException" message="msnab:InvalidPassportUserMessage"/>
+ </operation>
+ <operation name="ABGroupContactAdd">
+ <input message="msnab:ABGroupContactAddMessage"/>
+ <output message="msnab:ABGroupContactAddResponseMessage"/>
+ <fault name="InvalidPassportUserException" message="msnab:InvalidPassportUserMessage"/>
+ </operation>
+ <operation name="ABGroupAdd">
+ <input message="msnab:ABGroupAddMessage"/>
+ <output message="msnab:ABGroupAddResponseMessage"/>
+ </operation>
+ <operation name="ABGroupUpdate">
+ <input message="msnab:ABGroupUpdateMessage"/>
+ <output message="msnab:ABGroupUpdateResponseMessage"/>
+ </operation>
+ <operation name="ABGroupDelete">
+ <input message="msnab:ABGroupDeleteMessage"/>
+ <output message="msnab:ABGroupDeleteResponseMessage"/>
+ </operation>
+ <operation name="ABGroupContactDelete">
+ <input message="msnab:ABGroupContactDeleteMessage"/>
+ <output message="msnab:ABGroupContactDeleteResponseMessage"/>
+ </operation>
+ <operation name="ABContactUpdate">
+ <input message="msnab:ABContactUpdateMessage"/>
+ <output message="msnab:ABContactUpdateResponseMessage"/>
+ </operation>
+ <operation name="ABAdd">
+ <input message="msnab:ABAddMessage"/>
+ <output message="msnab:ABAddResponseMessage"/>
+ </operation>
+ <operation name="UpdateDynamicItem">
+ <input message="msnab:UpdateDynamicItemMessage"/>
+ <output message="msnab:UpdateDynamicItemResponseMessage"/>
+ </operation>
+ <operation name="ABFindContactsPaged">
+ <input message="msnab:ABFindContactsPagedMessage"/>
+ <output message="msnab:ABFindContactsPagedResponseMessage"/>
+ </operation>
+ <operation name="CreateContact">
+ <input message="msnab:CreateContactMessage"/>
+ <output message="msnab:CreateContactResponseMessage"/>
+ </operation>
+ <operation name="ManageWLConnection">
+ <input message="msnab:ManageWLConnectionMessage"/>
+ <output message="msnab:ManageWLConnectionResponseMessage"/>
+ </operation>
+ </portType>
+ <portType name="WhatsUpServicePortType">
+ <operation name="GetContactsRecentActivity">
+ <input message="msnab:GetContactsRecentActivityMessage"/>
+ <output message="msnab:GetContactsRecentActivityResponseMessage"/>
+ </operation>
+ </portType>
+ <binding name="SharingServiceBinding" type="msnab:SharingServicePortType">
+ <soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
+ <operation name="FindMembership">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/FindMembership"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="AddMember">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/AddMember"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="DeleteMember">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/DeleteMember"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="CreateCircle">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/CreateCircle"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ </binding>
+ <binding name="ABServiceBinding" type="msnab:ABServicePortType">
+ <soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
+ <operation name="ABFindAll">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABFindAll"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABContactAdd">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABContactAdd"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ <fault name="InvalidPassportUserException">
+ <soap:fault name="InvalidPassportUserException" use="literal"/>
+ </fault>
+ </operation>
+ <operation name="ABContactDelete">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABContactDelete"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ <fault name="InvalidPassportUserException">
+ <soap:fault name="InvalidPassportUserException" use="literal"/>
+ </fault>
+ </operation>
+ <operation name="ABGroupContactAdd">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABGroupContactAdd"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ <fault name="InvalidPassportUserException">
+ <soap:fault name="InvalidPassportUserException" use="literal"/>
+ </fault>
+ </operation>
+ <operation name="ABGroupAdd">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABGroupAdd"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABGroupUpdate">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABGroupUpdate"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABGroupDelete">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABGroupDelete"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABGroupContactDelete">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABGroupContactDelete"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABContactUpdate">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABContactUpdate"/>
+ <input>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </input>
+ <output>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ <soap:body use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABAdd">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABAdd"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ <operation name="UpdateDynamicItem">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/UpdateDynamicItem"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ <operation name="ABFindContactsPaged">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ABFindContactsPaged"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ <operation name="CreateContact">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/CreateContact"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ <operation name="ManageWLConnection">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/ManageWLConnection"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ABHeader" part="ApplicationHeader" use="literal"/>
+ <soap:header message="msnab:ABHeader" part="AuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:ServiceHeader" part="ServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ </binding>
+ <binding name="WhatsUpServiceBinding" type="msnab:WhatsUpServicePortType">
+ <soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
+ <operation name="GetContactsRecentActivity">
+ <soap:operation soapAction="http://www.msn.com/webservices/AddressBook/GetContactsRecentActivity"/>
+ <input>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:WNHeader" part="WNApplicationHeader" use="literal"/>
+ <soap:header message="msnab:WNHeader" part="WNAuthHeader" use="literal"/>
+ </input>
+ <output>
+ <soap:body use="literal"/>
+ <soap:header message="msnab:WNHeader" part="WNServiceHeader" use="literal"/>
+ </output>
+ </operation>
+ </binding>
+ <service name="SharingService">
+ <port name="FindMembershipPort" binding="msnab:SharingServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/SharingService.asmx"/>
+ </port>
+ <port name="AddMemberPort" binding="msnab:SharingServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/SharingService.asmx"/>
+ </port>
+ <port name="DeleteMemberPort" binding="msnab:SharingServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/SharingService.asmx"/>
+ </port>
+ <port name="CreateCirclePort" binding="msnab:SharingServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/SharingService.asmx"/>
+ </port>
+ </service>
+ <service name="ABService">
+ <port name="ABFindAllPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABContactAddPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABContactDeletePort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABGroupContactAddPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABGroupAddPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABGroupUpdatePort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABGroupDeletePort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABGroupContactDeletePort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABContactUpdatePort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABAddPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="UpdateDynamicItemPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ABFindContactsPagedPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="CreateContactPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ <port name="ManageWLConnectionPort" binding="msnab:ABServiceBinding">
+ <soap:address location="https://contacts.msn.com/abservice/abservice.asmx"/>
+ </port>
+ </service>
+ <service name="WhatsUpService">
+ <port name="GetContactsRecentActivityPort" binding="msnab:WhatsUpServiceBinding">
+ <soap:address location="http://sup.live.com/whatsnew/whatsnewservice.asmx"/>
+ </port>
+ </service>
+</definitions>
diff --git a/plugins/Msn/msn_waiting_message.php b/plugins/Msn/msn_waiting_message.php new file mode 100644 index 000000000..0af7c4f3e --- /dev/null +++ b/plugins/Msn/msn_waiting_message.php @@ -0,0 +1,132 @@ +<?php
+/**
+ * Table Definition for msn_waiting_message
+ */
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+class Msn_waiting_message extends Memcached_DataObject {
+
+ public $__table = 'msn_waiting_message'; // table name
+ public $id; // int primary_key not_null auto_increment
+ public $screenname; // varchar(255) not_null
+ public $message; // text not_null
+ public $created; // datetime() not_null
+ public $claimed; // datetime()
+
+ /* Static get */
+ public function staticGet($k, $v = null) {
+ return Memcached_DataObject::staticGet('Msn_waiting_message', $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+ public function table() {
+ return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'screenname' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'message' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'created' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'claimed' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR);
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has, since it
+ * won't appear in StatusNet's own keys list. In most cases, this will
+ * simply reference your keyTypes() function.
+ *
+ * @return array list of key field names
+ */
+ public function keys() {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them. This key information is used to store and clear
+ * cached data, so be sure to list any key that will be used for static
+ * lookups.
+ *
+ * @return array associative array of key definitions, field name to type:
+ * 'K' for primary key: for compound keys, add an entry for each component;
+ * 'U' for unique keys: compound keys are not well supported here.
+ */
+ public function keyTypes() {
+ return array('id' => 'K');
+ }
+
+ /**
+ * Magic formula for non-autoincrementing integer primary keys
+ *
+ * If a table has a single integer column as its primary key, DB_DataObject
+ * assumes that the column is auto-incrementing and makes a sequence table
+ * to do this incrementation. Since we don't need this for our class, we
+ * overload this method and return the magic formula that DB_DataObject needs.
+ *
+ * @return array magic three-false array that stops auto-incrementing.
+ */
+ function sequenceKey() {
+ return array(false, false, false);
+ }
+
+ /**
+ * @param string $screenname screenname or array of screennames to pull from
+ * If not specified, checks all queues in the system.
+ */
+ public static function top($screenname = null) {
+ $wm = new Msn_waiting_message();
+ if ($screenname) {
+ if (is_array($screenname)) {
+ // @fixme use safer escaping
+ $list = implode("','", array_map('addslashes', $screenname));
+ $wm->whereAdd("screenname in ('$list')");
+ } else {
+ $wm->screenname = $screenname;
+ }
+ }
+ $wm->orderBy('created');
+ $wm->whereAdd('claimed is null');
+
+ $wm->limit(1);
+
+ $cnt = $wm->find(true);
+
+ if ($cnt) {
+ # XXX: potential race condition
+ # can we force it to only update if claimed is still null
+ # (or old)?
+ common_log(LOG_INFO, 'claiming msn waiting message id = ' . $wm->id);
+ $orig = clone($wm);
+ $wm->claimed = common_sql_now();
+ $result = $wm->update($orig);
+ if ($result) {
+ common_log(LOG_INFO, 'claim succeeded.');
+ return $wm;
+ } else {
+ common_log(LOG_INFO, 'claim failed.');
+ }
+ }
+ $wm = null;
+ return null;
+ }
+
+ /**
+ * Release a claimed item.
+ */
+ public function releaseClaim() {
+ // DB_DataObject doesn't let us save nulls right now
+ $sql = sprintf("UPDATE msn_waiting_message SET claimed=NULL WHERE id=%d", $this->id);
+ $this->query($sql);
+
+ $this->claimed = null;
+ $this->encache();
+ }
+}
diff --git a/plugins/Msn/msnmanager.php b/plugins/Msn/msnmanager.php new file mode 100644 index 000000000..a8996ecaf --- /dev/null +++ b/plugins/Msn/msnmanager.php @@ -0,0 +1,275 @@ +<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+/**
+ * MSN background connection manager for MSN-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 handled.
+ *
+ * In a multi-site queuedaemon.php run, one connection will be instantiated
+ * for each site being handled by the current process that has MSN enabled.
+ */
+
+class MsnManager extends ImManager {
+ public $conn = null;
+ protected $lastPing = null;
+ protected $pingInterval;
+
+ /**
+ * Initialise connection to server.
+ *
+ * @return boolean true on success
+ */
+ public function start($master) {
+ if (parent::start($master)) {
+ $this->requeue_waiting_messages();
+ $this->connect();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Return any open sockets that the run loop should listen
+ * for input on.
+ *
+ * @return array Array of socket resources
+ */
+ public function getSockets() {
+ $this->connect();
+ if ($this->conn) {
+ return $this->conn->getSockets();
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Idle processing for io manager's execution loop.
+ * Send keepalive pings to server.
+ *
+ * @return void
+ */
+ public function idle($timeout = 0) {
+ if (empty($this->lastPing) || time() - $this->lastPing > $this->pingInterval) {
+ $this->send_ping();
+ }
+ }
+
+ /**
+ * Message pump is triggered on socket input, so we only need an idle()
+ * call often enough to trigger our outgoing pings.
+ */
+ public function timeout() {
+ return $this->pingInterval;
+ }
+
+ /**
+ * Process MSN events that have come in over the wire.
+ *
+ * @param resource $socket Socket ready
+ * @return void
+ */
+ public function handleInput($socket) {
+ common_log(LOG_DEBUG, 'Servicing the MSN queue.');
+ $this->stats('msn_process');
+ $this->conn->receive();
+ }
+
+ /**
+ * Initiate connection
+ *
+ * @return void
+ */
+ public function connect() {
+ if (!$this->conn) {
+ $this->conn = new MSN(
+ array(
+ 'user' => $this->plugin->user,
+ 'password' => $this->plugin->password,
+ 'alias' => $this->plugin->nickname,
+ 'psm' => 'Send me a message to post a notice',
+ 'debug' => false
+ )
+ );
+ $this->conn->registerHandler('IMin', array($this, 'handle_msn_message'));
+ $this->conn->registerHandler('SessionReady', array($this, 'handle_session_ready'));
+ $this->conn->registerHandler('Pong', array($this, 'update_ping_time'));
+ $this->conn->registerHandler('ConnectFailed', array($this, 'handle_connect_failed'));
+ $this->conn->registerHandler('Reconnect', array($this, 'handle_reconnect'));
+ $this->conn->signon();
+ $this->lastPing = time();
+ }
+ return $this->conn;
+ }
+
+ /**
+ * Called by the idle process to send a ping
+ * when necessary
+ *
+ * @return void
+ */
+ protected function send_ping() {
+ $this->connect();
+ if (!$this->conn) {
+ return false;
+ }
+
+ $this->conn->sendPing();
+ $this->lastPing = time();
+ $this->pingInterval = 50;
+ return true;
+ }
+
+ /**
+ * Update the time till the next ping
+ *
+ * @param $data Time till next ping
+ * @return void
+ */
+ public function update_ping_time($data) {
+ $this->pingInterval = $data;
+ }
+
+ /**
+ * Called via a callback when a message is received
+ *
+ * Passes it back to the queuing system
+ *
+ * @param array $data Data
+ * @return boolean
+ */
+ public function handle_msn_message($data) {
+ $this->plugin->enqueueIncomingRaw($data);
+ return true;
+ }
+
+ /**
+ * Called via a callback when a session becomes ready
+ *
+ * @param array $data Data
+ */
+ public function handle_session_ready($data) {
+ $sessionFailed = false;
+ $wm = Msn_waiting_message::top($data['to']);
+ while ($wm != NULL) {
+ if ($sessionFailed) {
+ $this->plugin->sendMessage($wm->screenname, $wm->message);
+ $sessionFailed = true;
+ } elseif (!$this->conn->sendMessage($wm->screenname, $wm->message, $ignore)) {
+ $this->plugin->sendMessage($wm->screenname, $wm->message);
+ }
+
+ $wm->delete();
+ $wm = Msn_waiting_message::top($data['to']);
+ }
+ }
+
+ /**
+ * Requeue messages from the waiting table so we try
+ * to send them again
+ *
+ * @return void
+ */
+ protected function requeue_waiting_messages() {
+ $wm = Msn_waiting_message::top();
+ while ($wm != NULL) {
+ $this->plugin->sendMessage($wm->screenname, $wm->message);
+ $wm->delete();
+ $wm = Msn_waiting_message::top();
+ }
+ }
+
+ /**
+ * Called by callback to log failure during connect
+ *
+ * @param string $message error message reported
+ * @return void
+ */
+ public function handle_connect_failed($message) {
+ common_log(LOG_NOTICE, 'MSN connect failed, retrying: ' . $message);
+ }
+
+ /**
+ * Called by callback to log reconnection
+ *
+ * @param void $data Not used (there to keep callback happy)
+ * @return void
+ */
+ public function handle_reconnect($data) {
+ common_log(LOG_NOTICE, 'MSN reconnecting');
+ // Requeue messages waiting in the DB
+ $this->requeue_waiting_messages();
+ }
+
+ /**
+ * Enters a message into the database for sending via a callback
+ * when the session is established
+ *
+ * @param string $to Intended recipient
+ * @param string $message Message
+ */
+ protected function enqueue_waiting_message($to, $message) {
+ $wm = new Msn_waiting_message();
+
+ $wm->screenname = $to;
+ $wm->message = $message;
+ $wm->created = common_sql_now();
+ $result = $wm->insert();
+
+ if (!$result) {
+ common_log_db_error($wm, 'INSERT', __FILE__);
+ throw new ServerException('DB error inserting queue item');
+ }
+
+ return true;
+ }
+
+ /**
+ * Send a message using the daemon
+ *
+ * @param $data Message data
+ * @return boolean true on success
+ */
+ public function send_raw_message($data) {
+ $this->connect();
+ if (!$this->conn) {
+ return false;
+ }
+
+ $waitForSession = false;
+ if (!$this->conn->sendMessage($data['to'], $data['message'], $waitForSession)) {
+ if ($waitForSession) {
+ $this->enqueue_waiting_message($data['to'], $data['message']);
+ } else {
+ return false;
+ }
+ }
+
+ // Sending a command updates the time till next ping
+ $this->lastPing = time();
+ $this->pingInterval = 50;
+ return true;
+ }
+}
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index d0b64e876..f846a2fc8 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1010,7 +1010,7 @@ class OStatusPlugin extends Plugin function onEndXrdActionLinks(&$xrd, $user) { - $xrd->links[] = array('rel' => Discovery::UPDATESFROM, + $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', array('id' => $user->id, 'format' => 'atom')), @@ -1045,6 +1045,6 @@ class OStatusPlugin extends Plugin $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => $url ); - return true; + return true; } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index b43a2b5f1..3dd00de29 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -25,7 +25,8 @@ if (!defined('STATUSNET')) { * @package OStatusPlugin * @maintainer Brion Vibber <brion@status.net> */ -class Ostatus_profile extends Memcached_DataObject + +class Ostatus_profile extends Managed_DataObject { public $__table = 'ostatus_profile'; @@ -47,74 +48,35 @@ class Ostatus_profile extends Memcached_DataObject } /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. + * Return table definition for Schema setup and DB_DataObject usage. * * @return array array of column definitions */ - function table() - { - return array('uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'profile_id' => DB_DATAOBJECT_INT, - 'group_id' => DB_DATAOBJECT_INT, - 'feeduri' => DB_DATAOBJECT_STR, - 'salmonuri' => DB_DATAOBJECT_STR, - 'avatar' => DB_DATAOBJECT_STR, - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, - 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); - } static function schemaDef() { - return array(new ColumnDef('uri', 'varchar', - 255, false, 'PRI'), - new ColumnDef('profile_id', 'integer', - null, true, 'UNI'), - new ColumnDef('group_id', 'integer', - null, true, 'UNI'), - new ColumnDef('feeduri', 'varchar', - 255, true, 'UNI'), - new ColumnDef('salmonuri', 'text', - null, true), - new ColumnDef('avatar', 'text', - null, true), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('modified', 'datetime', - null, false)); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has; this function - * defines them. - * - * @return array key definitions - */ - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. - * - * @return array key definitions - */ - function keyTypes() - { - return array('uri' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); - } - - function sequenceKey() - { - return array(false, false, false); + return array( + 'fields' => array( + 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), + 'profile_id' => array('type' => 'integer'), + 'group_id' => array('type' => 'integer'), + 'feeduri' => array('type' => 'varchar', 'length' => 255), + 'salmonuri' => array('type' => 'varchar', 'length' => 255), + 'avatar' => array('type' => 'text'), + 'created' => array('type' => 'datetime', 'not null' => true), + 'modified' => array('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array('uri'), + 'unique keys' => array( + 'ostatus_profile_profile_id_idx' => array('profile_id'), + 'ostatus_profile_group_id_idx' => array('group_id'), + 'ostatus_profile_feeduri_idx' => array('feeduri'), + ), + 'foreign keys' => array( + 'ostatus_profile_profile_id_fkey' => array('profile', array('profile_id' => 'id')), + 'ostatus_profile_group_id_fkey' => array('user_group', array('group_id' => 'id')), + ), + ); } /** diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index d5a8c1bf0..d1a6786fa 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -614,6 +614,28 @@ class OpenIDPlugin extends Plugin } /** + * Add OpenID information to the Account Management Control Document + * Event supplied by the Account Manager plugin + * + * @param array &$amcd Array that expresses the AMCD + * + * @return boolean hook value + */ + + function onEndAccountManagementControlDocument(&$amcd) + { + $amcd['auth-methods']['openid'] = array( + 'connect' => array( + 'method' => 'POST', + 'path' => common_local_url('openidlogin'), + 'params' => array( + 'identity' => 'openid_url' + ) + ) + ); + } + + /** * Add our version information to output * * @param array &$versions Array of version-data arrays diff --git a/plugins/OpenID/openidlogin.php b/plugins/OpenID/openidlogin.php index 4046068cf..8d25a2e9a 100644 --- a/plugins/OpenID/openidlogin.php +++ b/plugins/OpenID/openidlogin.php @@ -44,14 +44,6 @@ class OpenidloginAction extends Action oid_assert_allowed($openid_url); - # CSRF protection - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - // TRANS: Message given when there is a problem with the user's session token. - $this->showForm(_m('There was a problem with your session token. Try again, please.'), $openid_url); - return; - } - $rememberme = $this->boolean('rememberme'); common_ensure_session(); @@ -138,7 +130,6 @@ class OpenidloginAction extends Action $this->elementStart('fieldset'); // TRANS: OpenID plugin logon form legend. $this->element('legend', null, _m('OpenID login')); - $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); diff --git a/plugins/PtitUrl/PtitUrlPlugin.php b/plugins/PtitUrl/PtitUrlPlugin.php index 2963e8997..25a463c0b 100644 --- a/plugins/PtitUrl/PtitUrlPlugin.php +++ b/plugins/PtitUrl/PtitUrlPlugin.php @@ -30,7 +30,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php'; class PtitUrlPlugin extends UrlShortenerPlugin { diff --git a/plugins/RSSCloud/RSSCloudPlugin.php b/plugins/RSSCloud/RSSCloudPlugin.php index bba0be515..7d9c39a67 100644 --- a/plugins/RSSCloud/RSSCloudPlugin.php +++ b/plugins/RSSCloud/RSSCloudPlugin.php @@ -209,19 +209,16 @@ class RSSCloudPlugin extends Plugin { $schema = Schema::get(); $schema->ensureTable('rsscloud_subscription', - array(new ColumnDef('subscribed', 'integer', - null, false, 'PRI'), - new ColumnDef('url', 'varchar', - '255', false, 'PRI'), - new ColumnDef('failures', 'integer', - null, false, null, 0), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('modified', 'timestamp', - null, false, null, - 'CURRENT_TIMESTAMP', - 'on update CURRENT_TIMESTAMP') - )); + array( + 'fields' => array( + 'subscribed' => array('type' => 'int', 'not null' => true), + 'url' => array('type' => 'varchar', 'length' => '255', 'not null' => true), + 'failures' => array('type' => 'int', 'not null' => true, 'default' => 0), + 'created' => array('type' => 'datetime', 'not null' => true), + 'modified' => array('type' => 'timestamp', 'not null' => true), + ), + 'primary key' => array('subscribed', 'url'), + )); return true; } diff --git a/plugins/Recaptcha/RecaptchaPlugin.php b/plugins/Recaptcha/RecaptchaPlugin.php index 0c46a33e0..5a33e7132 100644 --- a/plugins/Recaptcha/RecaptchaPlugin.php +++ b/plugins/Recaptcha/RecaptchaPlugin.php @@ -41,7 +41,8 @@ class RecaptchaPlugin extends Plugin var $failed; var $ssl; - function onInitializePlugin(){ + function onInitializePlugin() + { if(!isset($this->private_key)) { common_log(LOG_ERR, 'Recaptcha: Must specify private_key in config.php'); } @@ -50,14 +51,6 @@ class RecaptchaPlugin extends Plugin } } - function checkssl(){ - if(common_config('site', 'ssl') === 'sometimes' || common_config('site', 'ssl') === 'always') { - return true; - } - return false; - } - - function onEndRegistrationFormData($action) { $action->elementStart('li'); @@ -77,7 +70,7 @@ class RecaptchaPlugin extends Plugin { if (isset($action->recaptchaPluginNeedsOutput) && $action->recaptchaPluginNeedsOutput) { // Load the AJAX API - if ($this->checkssl()) { + if (StatusNet::isHTTPS()) { $url = "https://api-secure.recaptcha.net/js/recaptcha_ajax.js"; } else { $url = "http://api.recaptcha.net/js/recaptcha_ajax.js"; diff --git a/plugins/Sample/SamplePlugin.php b/plugins/Sample/SamplePlugin.php index ef69121a9..a0d1140f3 100644 --- a/plugins/Sample/SamplePlugin.php +++ b/plugins/Sample/SamplePlugin.php @@ -172,9 +172,20 @@ class SamplePlugin extends Plugin // For storing user-submitted flags on profiles $schema->ensureTable('user_greeting_count', - array(new ColumnDef('user_id', 'integer', null, - true, 'PRI'), - new ColumnDef('greeting_count', 'integer'))); + array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true), + 'greeting_count' => array('type' => 'int'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + // Not all databases will support foreign keys, but even + // when not enforced it's helpful to include these definitions + // as documentation. + 'user_greeting_count_user_id_fkey' => array('user', array('user_id' => 'id')), + ), + ) + ); return true; } diff --git a/plugins/SimpleUrl/SimpleUrlPlugin.php b/plugins/SimpleUrl/SimpleUrlPlugin.php index 5e2e85878..24250f4d0 100644 --- a/plugins/SimpleUrl/SimpleUrlPlugin.php +++ b/plugins/SimpleUrl/SimpleUrlPlugin.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php'; - class SimpleUrlPlugin extends UrlShortenerPlugin { public $serviceUrl; diff --git a/plugins/StrictTransportSecurity/README b/plugins/StrictTransportSecurity/README new file mode 100644 index 000000000..66f03e95e --- /dev/null +++ b/plugins/StrictTransportSecurity/README @@ -0,0 +1,21 @@ +The Strict Transport Security plugin implements the Strict Transport Security header, improving the security of HTTPS only sites. +See http://lists.w3.org/Archives/Public/www-archive/2009Sep/att-0051/draft-hodges-strict-transport-sec-05.plain.html for the specification. + +Installation +============ +add "addPlugin('strictTransportSecurity');" +to the bottom of your config.php + +The plugin will not do anything unless: +$config['site']['ssl'] is set to 'always' +$config['site']['path'] is either not set, empty, or '/' + +Settings +======== +max_age (15552000): sets how long to remember the forced HTTPS (seconds) (15552000 seconds is 180 days) +includeSubDomains (false): if set, then STS will apply to all the sub-domains too. + +Example +======= +addPlugin('strictTransportSecurity'); + diff --git a/plugins/StrictTransportSecurity/StrictTransportSecurityPlugin.php b/plugins/StrictTransportSecurity/StrictTransportSecurityPlugin.php new file mode 100644 index 000000000..004a62792 --- /dev/null +++ b/plugins/StrictTransportSecurity/StrictTransportSecurityPlugin.php @@ -0,0 +1,62 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Plugin to enable Single Sign On via CAS (Central Authentication Service) + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews <candrews@integralblue.com> + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +class StrictTransportSecurityPlugin extends Plugin +{ + public $max_age = 15552000; + public $includeSubDomains = false; + + function __construct() + { + parent::__construct(); + } + + function onArgsInitialize($args) + { + $path = common_config('site', 'path'); + if(common_config('site', 'ssl') == 'always' && ($path == '/' || ! $path )) { + header('Strict-Transport-Security: max-age=' . $this->max_age . + ($this->includeSubDomains?'; includeSubDomains':'')); + } + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'StrictTransportSecurity', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews', + 'homepage' => 'http://status.net/wiki/Plugin:StrictTransportSecurity', + 'rawdescription' => + _m('The Strict Transport Security plugin implements the Strict Transport Security header, improving the security of HTTPS only sites.')); + return true; + } +} diff --git a/plugins/TightUrl/TightUrlPlugin.php b/plugins/TightUrl/TightUrlPlugin.php index b8e5addb1..8fd645945 100644 --- a/plugins/TightUrl/TightUrlPlugin.php +++ b/plugins/TightUrl/TightUrlPlugin.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/UrlShortener/UrlShortenerPlugin.php'; - class TightUrlPlugin extends UrlShortenerPlugin { public $serviceUrl; diff --git a/plugins/UrlShortener/UrlShortenerPlugin.php b/plugins/UrlShortener/UrlShortenerPlugin.php deleted file mode 100644 index 41f64bb26..000000000 --- a/plugins/UrlShortener/UrlShortenerPlugin.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Superclass for plugins that do URL shortening - * - * PHP version 5 - * - * LICENCE: This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * @category Plugin - * @package StatusNet - * @author Craig Andrews <candrews@integralblue.com> - * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -/** - * Superclass for plugins that do URL shortening - * - * @category Plugin - * @package StatusNet - * @author Craig Andrews <candrews@integralblue.com> - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -abstract class UrlShortenerPlugin extends Plugin -{ - public $shortenerName; - public $freeService=false; - //------------Url Shortener plugin should implement some (or all) of these methods------------\\ - - /** - * Short a URL - * @param url - * @return string shortened version of the url, or null if URL shortening failed - */ - protected abstract function shorten($url); - - //------------These methods may help you implement your plugin------------\\ - protected function http_get($url) - { - $request = HTTPClient::start(); - $response = $request->get($url); - return $response->getBody(); - } - - protected function http_post($url,$data) - { - $request = HTTPClient::start(); - $response = $request->post($url, null, $data); - return $response->getBody(); - } - - //------------Below are the methods that connect StatusNet to the implementing Url Shortener plugin------------\\ - - function onInitializePlugin(){ - if(!isset($this->shortenerName)){ - throw new Exception("must specify a shortenerName"); - } - } - - function onGetUrlShorteners(&$shorteners) - { - $shorteners[$this->shortenerName]=array('freeService'=>$this->freeService); - } - - function onStartShortenUrl($url,$shortenerName,&$shortenedUrl) - { - if($shortenerName == $this->shortenerName && strlen($url) >= common_config('site', 'shorturllength')){ - $result = $this->shorten($url); - if(isset($result) && $result != null && $result !== false){ - $shortenedUrl=$result; - common_log(LOG_INFO, __CLASS__ . ": $this->shortenerName shortened $url to $shortenedUrl"); - return false; - } - } - } -} diff --git a/lib/queued_xmpp.php b/plugins/Xmpp/Queued_XMPP.php index f6bccfd5b..24f542805 100644 --- a/lib/queued_xmpp.php +++ b/plugins/Xmpp/Queued_XMPP.php @@ -31,13 +31,17 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR . '/lib/jabber.php'; - class Queued_XMPP extends XMPPHP_XMPP { + /** + * Reference to the XmppPlugin object we're hooked up to. + */ + public $plugin; + /** * Constructor * + * @param XmppPlugin $plugin * @param string $host * @param integer $port * @param string $user @@ -47,8 +51,10 @@ class Queued_XMPP extends XMPPHP_XMPP * @param boolean $printlog * @param string $loglevel */ - public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) + public function __construct($plugin, $host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) { + $this->plugin = $plugin; + parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel); // We use $host to connect, but $server to build JIDs if specified. @@ -73,55 +79,43 @@ class Queued_XMPP extends XMPPHP_XMPP */ public function send($msg, $timeout=NULL) { - $qm = QueueManager::get('xmppout'); - $qm->enqueue(strval($msg), 'xmppout'); - } - - /** - * Since we'll be getting input through a queue system's run loop, - * we'll process one standalone message at a time rather than our - * own XMPP message pump. - * - * @param string $message - */ - public function processMessage($message) { - $frame = array_shift($this->frames); - xml_parse($this->parser, $frame->body, false); + $this->plugin->enqueueOutgoingRaw($msg); } //@{ /** - * Stream i/o functions disabled; push input through processMessage() + * Stream i/o functions disabled; only do output */ public function connect($timeout = 30, $persistent = false, $sendinit = true) { - throw new Exception("Can't connect to server from XMPP queue proxy."); + throw new Exception("Can't connect to server from fake XMPP."); } public function disconnect() { - throw new Exception("Can't connect to server from XMPP queue proxy."); + throw new Exception("Can't connect to server from fake XMPP."); } public function process() { - throw new Exception("Can't read stream from XMPP queue proxy."); + throw new Exception("Can't read stream from fake XMPP."); } public function processUntil($event, $timeout=-1) { - throw new Exception("Can't read stream from XMPP queue proxy."); + throw new Exception("Can't read stream from fake XMPP."); } public function read() { - throw new Exception("Can't read stream from XMPP queue proxy."); + throw new Exception("Can't read stream from fake XMPP."); } public function readyToProcess() { - throw new Exception("Can't read stream from XMPP queue proxy."); + throw new Exception("Can't read stream from fake XMPP."); } //@} + } diff --git a/plugins/Xmpp/README b/plugins/Xmpp/README new file mode 100644 index 000000000..9bd71e980 --- /dev/null +++ b/plugins/Xmpp/README @@ -0,0 +1,35 @@ +The XMPP plugin allows users to send and receive notices over the XMPP/Jabber/GTalk network. + +Installation +============ +add "addPlugin('xmpp', + array('setting'=>'value', 'setting2'=>'value2', ...);" +to the bottom of your config.php + +The daemon included with this plugin must be running. It will be started by +the plugin along with their other daemons when you run scripts/startdaemons.sh. +See the StatusNet README for more about queuing and daemons. + +Settings +======== +user*: user part of the jid +server*: server part of the jid +resource: resource part of the jid +port (5222): port on which to connect to the server +encryption (true): use encryption on the connection +host (same as server): host to connect to. Usually, you won't set this. +debug (false): log extra debug info +public: list of jid's that should get the public feed (firehose) + +* required +default values are in (parenthesis) + +Example +======= +addPlugin('xmpp', array( + 'user=>'update', + 'server=>'identi.ca', + 'password'=>'...', + 'public'=>array('bob@aol.com', 'sue@google.com') +)); + diff --git a/plugins/Xmpp/Sharing_XMPP.php b/plugins/Xmpp/Sharing_XMPP.php new file mode 100644 index 000000000..4b69125da --- /dev/null +++ b/plugins/Xmpp/Sharing_XMPP.php @@ -0,0 +1,43 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Send and receive notices using the Jabber network + * + * PHP version 5 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Jabber + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +class Sharing_XMPP extends XMPPHP_XMPP +{ + function getSocket() + { + return $this->socket; + } +} diff --git a/plugins/Xmpp/XmppPlugin.php b/plugins/Xmpp/XmppPlugin.php new file mode 100644 index 000000000..200254178 --- /dev/null +++ b/plugins/Xmpp/XmppPlugin.php @@ -0,0 +1,433 @@ +<?php +/** + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * Send and receive notices using the XMPP network + * + * PHP version 5 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category IM + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Plugin for XMPP + * + * @category Plugin + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class XmppPlugin extends ImPlugin +{ + public $server = null; + public $port = 5222; + public $user = 'update'; + public $resource = null; + public $encryption = true; + public $password = null; + public $host = null; // only set if != server + public $debug = false; // print extra debug info + + public $transport = 'xmpp'; + + function getDisplayName(){ + return _m('XMPP/Jabber/GTalk'); + } + + /** + * Splits a Jabber ID (JID) into node, domain, and resource portions. + * + * Based on validation routine submitted by: + * @copyright 2009 Patrick Georgi <patrick@georgi-clan.de> + * @license Licensed under ISC-L, which is compatible with everything else that keeps the copyright notice intact. + * + * @param string $jid string to check + * + * @return array with "node", "domain", and "resource" indices + * @throws Exception if input is not valid + */ + + protected function splitJid($jid) + { + $chars = ''; + /* the following definitions come from stringprep, Appendix C, + which is used in its entirety by nodeprop, Chapter 5, "Prohibited Output" */ + /* C1.1 ASCII space characters */ + $chars .= "\x{20}"; + /* C1.2 Non-ASCII space characters */ + $chars .= "\x{a0}\x{1680}\x{2000}-\x{200b}\x{202f}\x{205f}\x{3000a}"; + /* C2.1 ASCII control characters */ + $chars .= "\x{00}-\x{1f}\x{7f}"; + /* C2.2 Non-ASCII control characters */ + $chars .= "\x{80}-\x{9f}\x{6dd}\x{70f}\x{180e}\x{200c}\x{200d}\x{2028}\x{2029}\x{2060}-\x{2063}\x{206a}-\x{206f}\x{feff}\x{fff9}-\x{fffc}\x{1d173}-\x{1d17a}"; + /* C3 - Private Use */ + $chars .= "\x{e000}-\x{f8ff}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}"; + /* C4 - Non-character code points */ + $chars .= "\x{fdd0}-\x{fdef}\x{fffe}\x{ffff}\x{1fffe}\x{1ffff}\x{2fffe}\x{2ffff}\x{3fffe}\x{3ffff}\x{4fffe}\x{4ffff}\x{5fffe}\x{5ffff}\x{6fffe}\x{6ffff}\x{7fffe}\x{7ffff}\x{8fffe}\x{8ffff}\x{9fffe}\x{9ffff}\x{afffe}\x{affff}\x{bfffe}\x{bffff}\x{cfffe}\x{cffff}\x{dfffe}\x{dffff}\x{efffe}\x{effff}\x{ffffe}\x{fffff}\x{10fffe}\x{10ffff}"; + /* C5 - Surrogate codes */ + $chars .= "\x{d800}-\x{dfff}"; + /* C6 - Inappropriate for plain text */ + $chars .= "\x{fff9}-\x{fffd}"; + /* C7 - Inappropriate for canonical representation */ + $chars .= "\x{2ff0}-\x{2ffb}"; + /* C8 - Change display properties or are deprecated */ + $chars .= "\x{340}\x{341}\x{200e}\x{200f}\x{202a}-\x{202e}\x{206a}-\x{206f}"; + /* C9 - Tagging characters */ + $chars .= "\x{e0001}\x{e0020}-\x{e007f}"; + + /* Nodeprep forbids some more characters */ + $nodeprepchars = $chars; + $nodeprepchars .= "\x{22}\x{26}\x{27}\x{2f}\x{3a}\x{3c}\x{3e}\x{40}"; + + $parts = explode("/", $jid, 2); + if (count($parts) > 1) { + $resource = $parts[1]; + if ($resource == '') { + // Warning: empty resource isn't legit. + // But if we're normalizing, we may as well take it... + } + } else { + $resource = null; + } + + $node = explode("@", $parts[0]); + if ((count($node) > 2) || (count($node) == 0)) { + throw new Exception("Invalid JID: too many @s"); + } else if (count($node) == 1) { + $domain = $node[0]; + $node = null; + } else { + $domain = $node[1]; + $node = $node[0]; + if ($node == '') { + throw new Exception("Invalid JID: @ but no node"); + } + } + + // Length limits per http://xmpp.org/rfcs/rfc3920.html#addressing + if ($node !== null) { + if (strlen($node) > 1023) { + throw new Exception("Invalid JID: node too long."); + } + if (preg_match("/[".$nodeprepchars."]/u", $node)) { + throw new Exception("Invalid JID node '$node'"); + } + } + + if (strlen($domain) > 1023) { + throw new Exception("Invalid JID: domain too long."); + } + if (!common_valid_domain($domain)) { + throw new Exception("Invalid JID domain name '$domain'"); + } + + if ($resource !== null) { + if (strlen($resource) > 1023) { + throw new Exception("Invalid JID: resource too long."); + } + if (preg_match("/[".$chars."]/u", $resource)) { + throw new Exception("Invalid JID resource '$resource'"); + } + } + + return array('node' => is_null($node) ? null : mb_strtolower($node), + 'domain' => is_null($domain) ? null : mb_strtolower($domain), + 'resource' => $resource); + } + + /** + * Checks whether a string is a syntactically valid Jabber ID (JID), + * either with or without a resource. + * + * Note that a bare domain can be a valid JID. + * + * @param string $jid string to check + * @param bool $check_domain whether we should validate that domain... + * + * @return boolean whether the string is a valid JID + */ + protected function validateFullJid($jid, $check_domain=false) + { + try { + $parts = $this->splitJid($jid); + if ($check_domain) { + if (!$this->checkDomain($parts['domain'])) { + return false; + } + } + return $parts['resource'] !== ''; // missing or present; empty ain't kosher + } catch (Exception $e) { + return false; + } + } + + /** + * Checks whether a string is a syntactically valid base Jabber ID (JID). + * A base JID won't include a resource specifier on the end; since we + * take it off when reading input we can't really use them reliably + * to direct outgoing messages yet (sorry guys!) + * + * Note that a bare domain can be a valid JID. + * + * @param string $jid string to check + * @param bool $check_domain whether we should validate that domain... + * + * @return boolean whether the string is a valid JID + */ + protected function validateBaseJid($jid, $check_domain=false) + { + try { + $parts = $this->splitJid($jid); + if ($check_domain) { + if (!$this->checkDomain($parts['domain'])) { + return false; + } + } + return ($parts['resource'] === null); // missing; empty ain't kosher + } catch (Exception $e) { + return false; + } + } + + /** + * Normalizes a Jabber ID for comparison, dropping the resource component if any. + * + * @param string $jid JID to check + * @param bool $check_domain if true, reject if the domain isn't findable + * + * @return string an equivalent JID in normalized (lowercase) form + */ + + function normalize($jid) + { + try { + $parts = $this->splitJid($jid); + if ($parts['node'] !== null) { + return $parts['node'] . '@' . $parts['domain']; + } else { + return $parts['domain']; + } + } catch (Exception $e) { + return null; + } + } + + /** + * Check if this domain's got some legit DNS record + */ + protected function checkDomain($domain) + { + if (checkdnsrr("_xmpp-server._tcp." . $domain, "SRV")) { + return true; + } + if (checkdnsrr($domain, "ANY")) { + return true; + } + return false; + } + + function daemonScreenname() + { + $ret = $this->user . '@' . $this->server; + if($this->resource) + { + return $ret . '/' . $this->resource; + }else{ + return $ret; + } + } + + function validate($screenname) + { + return $this->validateBaseJid($screenname, common_config('email', 'check_domain')); + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'XMPPHP_XMPP': + require_once $dir . '/extlib/XMPPHP/XMPP.php'; + return false; + case 'Sharing_XMPP': + case 'Queued_XMPP': + require_once $dir . '/'.$cls.'.php'; + return false; + case 'XmppManager': + require_once $dir . '/'.strtolower($cls).'.php'; + return false; + default: + return true; + } + } + + function onStartImDaemonIoManagers(&$classes) + { + parent::onStartImDaemonIoManagers(&$classes); + $classes[] = new XmppManager($this); // handles pings/reconnects + return true; + } + + function microiduri($screenname) + { + return 'xmpp:' . $screenname; + } + + function sendMessage($screenname, $body) + { + $this->queuedConnection()->message($screenname, $body, 'chat'); + } + + function sendNotice($screenname, $notice) + { + $msg = $this->formatNotice($notice); + $entry = $this->format_entry($notice); + + $this->queuedConnection()->message($screenname, $msg, 'chat', null, $entry); + return true; + } + + /** + * extra information for XMPP messages, as defined by Twitter + * + * @param Profile $profile Profile of the sending user + * @param Notice $notice Notice being sent + * + * @return string Extra information (Atom, HTML, addresses) in string format + */ + + function format_entry($notice) + { + $profile = $notice->getProfile(); + + $entry = $notice->asAtomEntry(true, true); + + $xs = new XMLStringer(); + $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im')); + $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml')); + $xs->element('a', array('href' => $profile->profileurl), + $profile->nickname); + $xs->text(": "); + if (!empty($notice->rendered)) { + $xs->raw($notice->rendered); + } else { + $xs->raw(common_render_content($notice->content, $notice)); + } + $xs->text(" "); + $xs->element('a', array( + 'href'=>common_local_url('conversation', + array('id' => $notice->conversation)).'#notice-'.$notice->id + ),sprintf(_('[%s]'),$notice->id)); + $xs->elementEnd('body'); + $xs->elementEnd('html'); + + $html = $xs->getString(); + + return $html . ' ' . $entry; + } + + function receiveRawMessage($pl) + { + $from = $this->normalize($pl['from']); + + if ($pl['type'] != 'chat') { + $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from: " . $pl['xml']->toString()); + return; + } + + if (mb_strlen($pl['body']) == 0) { + $this->log(LOG_WARNING, "Ignoring message with empty body from $from: " . $pl['xml']->toString()); + return; + } + + $this->handleIncoming($from, $pl['body']); + + return true; + } + + /** + * Build a queue-proxied XMPP interface object. Any outgoing messages + * will be run back through us for enqueing rather than sent directly. + * + * @return Queued_XMPP + * @throws Exception if server settings are invalid. + */ + function queuedConnection(){ + if(!isset($this->server)){ + throw new Exception("must specify a server"); + } + if(!isset($this->port)){ + throw new Exception("must specify a port"); + } + if(!isset($this->user)){ + throw new Exception("must specify a user"); + } + if(!isset($this->password)){ + throw new Exception("must specify a password"); + } + + return new Queued_XMPP($this, $this->host ? + $this->host : + $this->server, + $this->port, + $this->user, + $this->password, + $this->resource, + $this->server, + $this->debug ? + true : false, + $this->debug ? + XMPPHP_Log::LEVEL_VERBOSE : null + ); + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'XMPP', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:XMPP', + 'rawdescription' => + _m('The XMPP plugin allows users to send and receive notices over the XMPP/Jabber network.')); + return true; + } +} + diff --git a/extlib/XMPPHP/BOSH.php b/plugins/Xmpp/extlib/XMPPHP/BOSH.php index befaf60a7..befaf60a7 100644 --- a/extlib/XMPPHP/BOSH.php +++ b/plugins/Xmpp/extlib/XMPPHP/BOSH.php diff --git a/extlib/XMPPHP/Exception.php b/plugins/Xmpp/extlib/XMPPHP/Exception.php index da59bc791..da59bc791 100644 --- a/extlib/XMPPHP/Exception.php +++ b/plugins/Xmpp/extlib/XMPPHP/Exception.php diff --git a/extlib/XMPPHP/Log.php b/plugins/Xmpp/extlib/XMPPHP/Log.php index a9bce3d84..a9bce3d84 100644 --- a/extlib/XMPPHP/Log.php +++ b/plugins/Xmpp/extlib/XMPPHP/Log.php diff --git a/extlib/XMPPHP/Roster.php b/plugins/Xmpp/extlib/XMPPHP/Roster.php index 2e459e2a2..2e459e2a2 100644 --- a/extlib/XMPPHP/Roster.php +++ b/plugins/Xmpp/extlib/XMPPHP/Roster.php diff --git a/extlib/XMPPHP/XMLObj.php b/plugins/Xmpp/extlib/XMPPHP/XMLObj.php index 0d3e21991..0d3e21991 100644 --- a/extlib/XMPPHP/XMLObj.php +++ b/plugins/Xmpp/extlib/XMPPHP/XMLObj.php diff --git a/extlib/XMPPHP/XMLStream.php b/plugins/Xmpp/extlib/XMPPHP/XMLStream.php index d33411ec5..d33411ec5 100644 --- a/extlib/XMPPHP/XMLStream.php +++ b/plugins/Xmpp/extlib/XMPPHP/XMLStream.php diff --git a/extlib/XMPPHP/XMPP.php b/plugins/Xmpp/extlib/XMPPHP/XMPP.php index c0f896339..c0f896339 100644 --- a/extlib/XMPPHP/XMPP.php +++ b/plugins/Xmpp/extlib/XMPPHP/XMPP.php diff --git a/extlib/XMPPHP/XMPP_Old.php b/plugins/Xmpp/extlib/XMPPHP/XMPP_Old.php index 43f56b154..43f56b154 100644 --- a/extlib/XMPPHP/XMPP_Old.php +++ b/plugins/Xmpp/extlib/XMPPHP/XMPP_Old.php diff --git a/plugins/Xmpp/xmppmanager.php b/plugins/Xmpp/xmppmanager.php new file mode 100644 index 000000000..1a4e9546d --- /dev/null +++ b/plugins/Xmpp/xmppmanager.php @@ -0,0 +1,279 @@ +<?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 handled. + * + * 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 ImManager +{ + protected $lastping = null; + protected $pingid = null; + + public $conn = null; + + const PING_INTERVAL = 120; + + + /** + * Initialize connection to server. + * @return boolean true on success + */ + public function start($master) + { + if(parent::start($master)) + { + $this->connect(); + return true; + }else{ + return false; + } + } + + function send_raw_message($data) + { + $this->connect(); + if (!$this->conn || $this->conn->isDisconnected()) { + return false; + } + $this->conn->send($data); + return true; + } + + /** + * 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; + } + + /** + * Process XMPP events that have come in over the wire. + * @fixme may kill process on XMPP error + * @param resource $socket + */ + public function handleInput($socket) + { + # Process the queue for as long as needed + try { + 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()); + } + } + + /** + * Lists the IM 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() + { + $this->connect(); + if($this->conn){ + return array($this->conn->getSocket()); + }else{ + return array(); + } + } + + /** + * 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) + { + $now = time(); + if (empty($this->lastping) || $now - $this->lastping > self::PING_INTERVAL) { + try { + $this->send_ping(); + } catch (XMPPHP_Exception $e) { + common_log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); + die($e->getMessage()); + } + } + } + + function connect() + { + if (!$this->conn || $this->conn->isDisconnected()) { + $resource = 'queue' . posix_getpid(); + $this->conn = new Sharing_XMPP($this->plugin->host ? + $this->plugin->host : + $this->plugin->server, + $this->plugin->port, + $this->plugin->user, + $this->plugin->password, + $this->plugin->resource, + $this->plugin->server, + $this->plugin->debug ? + true : false, + $this->plugin->debug ? + XMPPHP_Log::LEVEL_VERBOSE : null + ); + + if (!$this->conn) { + return false; + } + $this->conn->addEventHandler('message', 'handle_xmpp_message', $this); + $this->conn->addEventHandler('reconnect', 'handle_xmpp_reconnect', $this); + $this->conn->setReconnectTimeout(600); + + $this->conn->autoSubscribe(); + $this->conn->useEncryption($this->plugin->encryption); + + try { + $this->conn->connect(true); // true = persistent connection + } catch (XMPPHP_Exception $e) { + common_log(LOG_ERR, $e->getMessage()); + return false; + } + + $this->conn->processUntil('session_start'); + $this->send_presence(_m('Send me a message to post a notice'), 'available', null, 'available', 100); + } + return $this->conn; + } + + function send_ping() + { + $this->connect(); + if (!$this->conn || $this->conn->isDisconnected()) { + return false; + } + $now = time(); + if (!isset($this->pingid)) { + $this->pingid = 0; + } else { + $this->pingid++; + } + + common_log(LOG_DEBUG, "Sending ping #{$this->pingid}"); + $this->conn->send("<iq from='{" . $this->plugin->daemonScreenname() . "}' to='{$this->plugin->server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>"); + $this->lastping = $now; + return true; + } + + function handle_xmpp_message(&$pl) + { + $this->plugin->enqueueIncomingRaw($pl); + return true; + } + + /** + * Callback for Jabber reconnect event + * @param $pl + */ + function handle_xmpp_reconnect(&$pl) + { + common_log(LOG_NOTICE, 'XMPP reconnected'); + + $this->conn->processUntil('session_start'); + $this->send_presence(_m('Send me a message to post a notice'), 'available', null, 'available', 100); + } + + /** + * sends a presence stanza on the XMPP network + * + * @param string $status current status, free-form string + * @param string $show structured status value + * @param string $to recipient of presence, null for general + * @param string $type type of status message, related to $show + * @param int $priority priority of the presence + * + * @return boolean success value + */ + + function send_presence($status, $show='available', $to=null, + $type = 'available', $priority=null) + { + $this->connect(); + if (!$this->conn || $this->conn->isDisconnected()) { + return false; + } + $this->conn->presence($status, $show, $to, $type, $priority); + return true; + } + + /** + * sends a "special" presence stanza on the XMPP network + * + * @param string $type Type of presence + * @param string $to JID to send presence to + * @param string $show show value for presence + * @param string $status status value for presence + * + * @return boolean success flag + * + * @see send_presence() + */ + + function special_presence($type, $to=null, $show=null, $status=null) + { + // FIXME: why use this instead of send_presence()? + $this->connect(); + if (!$this->conn || $this->conn->isDisconnected()) { + return false; + } + + $to = htmlspecialchars($to); + $status = htmlspecialchars($status); + + $out = "<presence"; + if ($to) { + $out .= " to='$to'"; + } + if ($type) { + $out .= " type='$type'"; + } + if ($show == 'available' and !$status) { + $out .= "/>"; + } else { + $out .= ">"; + if ($show && ($show != 'available')) { + $out .= "<show>$show</show>"; + } + if ($status) { + $out .= "<status>$status</status>"; + } + $out .= "</presence>"; + } + $this->conn->send($out); + return true; + } +} diff --git a/scripts/checkschema.php b/scripts/checkschema.php index bf52abe15..73fd74302 100755 --- a/scripts/checkschema.php +++ b/scripts/checkschema.php @@ -27,4 +27,18 @@ END_OF_CHECKSCHEMA_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; +function tableDefs() +{ + $schema = array(); + require INSTALLDIR.'/db/core.php'; + return $schema; +} + +$schema = Schema::get(); +$schemaUpdater = new SchemaUpdater($schema); +foreach (tableDefs() as $table => $def) { + $schemaUpdater->register($table, $def); +} +$schemaUpdater->checkSchema(); + Event::handle('CheckSchema'); diff --git a/scripts/clearcache.php b/scripts/clearcache.php index 0fb75daa0..c67d6275d 100644 --- a/scripts/clearcache.php +++ b/scripts/clearcache.php @@ -38,7 +38,7 @@ require_once INSTALLDIR.'/scripts/commandline.inc'; $karg = get_option_value('k', 'key'); if (!empty($karg)) { - $k = common_cache_key($karg); + $k = Cache::key($karg); } else { $table = get_option_value('t', 'table'); if (empty($table)) { @@ -55,7 +55,7 @@ if (!empty($karg)) { print "Clearing key '$k'..."; -$c = common_memcache(); +$c = Cache::instance(); if (empty($c)) { die("Can't initialize cache object!\n"); diff --git a/scripts/dumpschema.php b/scripts/dumpschema.php new file mode 100644 index 000000000..9c5e3cf8b --- /dev/null +++ b/scripts/dumpschema.php @@ -0,0 +1,253 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2008, 2009, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$helptext = <<<END_OF_CHECKSCHEMA_HELP +Attempt to pull a schema definition for a given table. + + --all run over all defined core tables + --diff show differences between the expected and live table defs + --raw skip compatibility filtering for diffs + --create dump SQL that would be run to update or create this table + --build dump SQL that would be run to create this table fresh + --checksum just output checksums from the source schema defs + + +END_OF_CHECKSCHEMA_HELP; + +$longoptions = array('diff', 'all', 'create', 'update', 'raw', 'checksum'); +require_once INSTALLDIR.'/scripts/commandline.inc'; + +function indentOptions($indent) +{ + $cutoff = 3; + if ($indent < $cutoff) { + $space = $indent ? str_repeat(' ', $indent * 4) : ''; + $sep = ","; + $lf = "\n"; + $endspace = "$lf" . ($indent ? str_repeat(' ', ($indent - 1) * 4) : ''); + } else { + $space = ''; + $sep = ", "; + $lf = ''; + $endspace = ''; + } + if ($indent - 1 < $cutoff) { + } + return array($space, $sep, $lf, $endspace); +} + +function prettyDumpArray($arr, $key=null, $indent=0) +{ + // hack + if ($key == 'primary key') { + $subIndent = $indent + 2; + } else { + $subIndent = $indent + 1; + } + + list($space, $sep, $lf, $endspace) = indentOptions($indent); + list($inspace, $insep, $inlf, $inendspace) = indentOptions($subIndent); + + print "{$space}"; + if (!is_numeric($key)) { + print "'$key' => "; + } + if (is_array($arr)) { + print "array({$inlf}"; + $n = 0; + foreach ($arr as $key => $row) { + $n++; + prettyDumpArray($row, $key, $subIndent); + if ($n < count($arr)) { + print "$insep$inlf"; + } + } + // hack! + print "{$inendspace})"; + } else { + print var_export($arr, true); + } +} + +function getCoreSchema($tableName) +{ + $schema = array(); + include INSTALLDIR . '/db/core.php'; + return $schema[$tableName]; +} + +function getCoreTables() +{ + $schema = array(); + include INSTALLDIR . '/db/core.php'; + return array_keys($schema); +} + +function dumpTable($tableName, $live) +{ + if ($live) { + $schema = Schema::get(); + $def = $schema->getTableDef($tableName); + } else { + // hack + $def = getCoreSchema($tableName); + } + prettyDumpArray($def, $tableName); + print "\n"; +} + +function dumpBuildTable($tableName) +{ + echo "-- \n"; + echo "-- $tableName\n"; + echo "-- \n"; + + $schema = Schema::get(); + $def = getCoreSchema($tableName); + $sql = $schema->buildCreateTable($tableName, $def); + $sql[] = ''; + + echo implode(";\n", $sql); + echo "\n"; +} + +function dumpEnsureTable($tableName) +{ + $schema = Schema::get(); + $def = getCoreSchema($tableName); + $sql = $schema->buildEnsureTable($tableName, $def); + + if ($sql) { + echo "-- \n"; + echo "-- $tableName\n"; + echo "-- \n"; + + $sql[] = ''; + echo implode(";\n", $sql); + echo "\n"; + } +} + +function dumpDiff($tableName, $filter) +{ + $schema = Schema::get(); + $def = getCoreSchema($tableName); + try { + $old = $schema->getTableDef($tableName); + } catch (Exception $e) { + // @fixme this is a terrible check :D + if (preg_match('/no such table/i', $e->getMessage())) { + return dumpTable($tableName, false); + } else { + throw $e; + } + } + + if ($filter) { + //$old = $schema->filterDef($old); + $def = $schema->filterDef($def); + } + + // @hack + $old = tweakPrimaryKey($old); + $def = tweakPrimaryKey($def); + + $sections = array_unique(array_merge(array_keys($old), array_keys($def))); + $final = array(); + foreach ($sections as $section) { + if ($section == 'fields') { + // this shouldn't be needed maybe... wait what? + } + $diff = $schema->diffArrays($old, $def, $section); + $chunks = array('del', 'mod', 'add'); + foreach ($chunks as $chunk) { + if ($diff[$chunk]) { + foreach ($diff[$chunk] as $key) { + if ($chunk == 'del') { + $final[$section]["DEL $key"] = $old[$section][$key]; + } else if ($chunk == 'add') { + $final[$section]["ADD $key"] = $def[$section][$key]; + } else if ($chunk == 'mod') { + $final[$section]["OLD $key"] = $old[$section][$key]; + $final[$section]["NEW $key"] = $def[$section][$key]; + } + } + } + } + } + + prettyDumpArray($final, $tableName); + print "\n"; +} + +function tweakPrimaryKey($def) +{ + if (isset($def['primary key'])) { + $def['primary keys'] = array('primary key' => $def['primary key']); + unset($def['primary key']); + } + if (isset($def['description'])) { + $def['descriptions'] = array('description' => $def['description']); + unset($def['description']); + } + return $def; +} + +function dumpChecksum($tableName) +{ + $schema = Schema::get(); + $def = getCoreSchema($tableName); + + $updater = new SchemaUpdater($schema); + $checksum = $updater->checksum($def); + $old = @$updater->checksums[$tableName]; + + if ($old == $checksum) { + echo "OK $checksum $tableName\n"; + } else if (!$old) { + echo "NEW $checksum $tableName\n"; + } else { + echo "MOD $checksum $tableName (was $old)\n"; + } +} + +if (have_option('all')) { + $args = getCoreTables(); +} + +if (count($args)) { + foreach ($args as $tableName) { + if (have_option('diff')) { + dumpDiff($tableName, !have_option('raw')); + } else if (have_option('create')) { + dumpBuildTable($tableName); + } else if (have_option('update')) { + dumpEnsureTable($tableName); + } else if (have_option('checksum')) { + dumpChecksum($tableName); + } else { + dumpTable($tableName, true); + } + } +} else { + show_help($helptext); +}
\ No newline at end of file diff --git a/scripts/fixup_inboxes.php b/scripts/fixup_inboxes.php index d55a53885..c6e4fd071 100755 --- a/scripts/fixup_inboxes.php +++ b/scripts/fixup_inboxes.php @@ -46,7 +46,7 @@ if ($start_at) { } $cnt = $user->find(); -$cache = common_memcache(); +$cache = Cache::instance(); while ($user->fetch()) { common_log(LOG_INFO, 'Updating inbox for user ' . $user->id); @@ -76,6 +76,6 @@ while ($user->fetch()) { $inbox->free(); unset($inbox); if ($cache) { - $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); + $cache->delete(Cache::key('user:notices_with_friends:' . $user->id)); } } diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php index a332e06b5..80c21bce5 100755 --- a/scripts/getvaliddaemons.php +++ b/scripts/getvaliddaemons.php @@ -39,9 +39,7 @@ $daemons = array(); $daemons[] = INSTALLDIR.'/scripts/queuedaemon.php'; -if(common_config('xmpp','enabled')) { - $daemons[] = INSTALLDIR.'/scripts/xmppdaemon.php'; -} +$daemons[] = INSTALLDIR.'/scripts/imdaemon.php'; if (Event::handle('GetValidDaemons', array(&$daemons))) { foreach ($daemons as $daemon) { diff --git a/scripts/xmppdaemon.php b/scripts/imdaemon.php index abd7cc22b..0ce74667c 100755 --- a/scripts/xmppdaemon.php +++ b/scripts/imdaemon.php @@ -23,21 +23,19 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); $shortoptions = 'fi::a'; $longoptions = array('id::', 'foreground', 'all'); -$helptext = <<<END_OF_XMPP_HELP -Daemon script for receiving new notices from Jabber users. +$helptext = <<<END_OF_IM_HELP +Daemon script for receiving new notices from IM users. -i --id Identity (default none) -a --all Handle XMPP for all local sites (requires Stomp queue handler, status_network setup) -f --foreground Stay in the foreground (default background) -END_OF_XMPP_HELP; +END_OF_IM_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; -require_once INSTALLDIR . '/lib/jabber.php'; - -class XMPPDaemon extends SpawningDaemon +class ImDaemon extends SpawningDaemon { protected $allsites = false; @@ -45,7 +43,7 @@ class XMPPDaemon extends SpawningDaemon { if ($threads != 1) { // This should never happen. :) - throw new Exception("XMPPDaemon can must run single-threaded"); + throw new Exception("IMDaemon can must run single-threaded"); } parent::__construct($id, $daemonize, $threads); $this->allsites = $allsites; @@ -53,9 +51,9 @@ class XMPPDaemon extends SpawningDaemon function runThread() { - common_log(LOG_INFO, 'Waiting to listen to XMPP and queues'); + common_log(LOG_INFO, 'Waiting to listen to IM connections and queues'); - $master = new XmppMaster($this->get_id(), $this->processManager()); + $master = new ImMaster($this->get_id(), $this->processManager()); $master->init($this->allsites); $master->service(); @@ -66,7 +64,7 @@ class XMPPDaemon extends SpawningDaemon } -class XmppMaster extends IoMaster +class ImMaster extends IoMaster { protected $processManager; @@ -82,25 +80,20 @@ class XmppMaster extends IoMaster */ function initManagers() { - if (common_config('xmpp', 'enabled')) { + $classes = array(); + if (Event::handle('StartImDaemonIoManagers', array(&$classes))) { $qm = QueueManager::get(); - $qm->setActiveGroup('xmpp'); - $this->instantiate($qm); - $this->instantiate(XmppManager::get()); - $this->instantiate($this->processManager); + $qm->setActiveGroup('im'); + $classes[] = $qm; + $classes[] = $this->processManager; + } + Event::handle('EndImDaemonIoManagers', array(&$classes)); + foreach ($classes as $class) { + $this->instantiate($class); } } } -// Abort immediately if xmpp is not enabled, otherwise the daemon chews up -// lots of CPU trying to connect to unconfigured servers -// @fixme do this check after we've run through the site list so we -// don't have to find an XMPP site to start up when using --all mode. -if (common_config('xmpp','enabled')==false) { - print "Aborting daemon - xmpp is disabled\n"; - exit(1); -} - if (version_compare(PHP_VERSION, '5.2.6', '<')) { $arch = php_uname('m'); if ($arch == 'x86_64' || $arch == 'amd64') { @@ -120,6 +113,6 @@ if (have_option('i', 'id')) { $foreground = have_option('f', 'foreground'); $all = have_option('a') || have_option('--all'); -$daemon = new XMPPDaemon($id, !$foreground, 1, $all); +$daemon = new ImDaemon($id, !$foreground, 1, $all); $daemon->runOnce(); diff --git a/scripts/showcache.php b/scripts/showcache.php index 93b57a484..8ef08467d 100644 --- a/scripts/showcache.php +++ b/scripts/showcache.php @@ -38,7 +38,7 @@ require_once INSTALLDIR.'/scripts/commandline.inc'; $karg = get_option_value('k'); if (!empty($karg)) { - $k = common_cache_key($karg); + $k = Cache::key($karg); } else { $table = get_option_value('t'); if (empty($table)) { @@ -55,7 +55,7 @@ if (!empty($karg)) { print "Checking key '$k'...\n"; -$c = common_memcache(); +$c = Cache::instance(); if (empty($c)) { die("Can't initialize cache object!\n"); diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index c790f1f34..bc1230e64 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -23,8 +23,8 @@ SDIR=`dirname $0` DIR=`php $SDIR/getpiddir.php` -for f in jabberhandler ombhandler publichandler smshandler pinghandler \ - xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ +for f in ombhandler smshandler pinghandler \ + twitterhandler facebookhandler \ twitterstatusfetcher synctwitterfriends pluginhandler rsscloudhandler; do FILES="$DIR/$f.*.pid" diff --git a/scripts/uncache_users.php b/scripts/uncache_users.php index 5a1d33030..eb7e39802 100644 --- a/scripts/uncache_users.php +++ b/scripts/uncache_users.php @@ -34,7 +34,7 @@ common_log(LOG_INFO, 'Updating user inboxes.'); $ids = file($id_file); -$memc = common_memcache(); +$memc = Cache::instance(); foreach ($ids as $id) { @@ -47,6 +47,6 @@ foreach ($ids as $id) { $user->decache(); - $memc->delete(common_cache_key('user:notices_with_friends:'. $user->id)); - $memc->delete(common_cache_key('user:notices_with_friends:'. $user->id . ';last')); + $memc->delete(Cache::key('user:notices_with_friends:'. $user->id)); + $memc->delete(Cache::key('user:notices_with_friends:'. $user->id . ';last')); } diff --git a/theme/default/css/display.css b/theme/default/css/display.css index a9d9910d4..a2624ff4c 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -7,8 +7,6 @@ * @link http://status.net/ */ -@import url(../../base/css/display.css); - @media screen, projection, tv { body, a:active { diff --git a/theme/default/theme.ini b/theme/default/theme.ini new file mode 100644 index 000000000..33d0b6797 --- /dev/null +++ b/theme/default/theme.ini @@ -0,0 +1 @@ +include=base diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 755775cf2..67068104b 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -7,8 +7,6 @@ * @link http://status.net/ */ -@import url(../../base/css/display.css); - @media screen, projection, tv { body, a:active { diff --git a/theme/identica/theme.ini b/theme/identica/theme.ini new file mode 100644 index 000000000..33d0b6797 --- /dev/null +++ b/theme/identica/theme.ini @@ -0,0 +1 @@ +include=base |