diff --git a/README b/README index 0dcbeea239..ef032cad84 100644 --- a/README +++ b/README @@ -1472,6 +1472,8 @@ Configuration options specific to notices. contentlimit: max length of the plain-text content of a notice. Default is null, meaning to use the site-wide text limit. 0 means no limit. +defaultscope: default scope for notices. Defaults to 0; set to + 1 to keep notices private to this site by default. message ------- diff --git a/actions/apistatusesretweet.php b/actions/apistatusesretweet.php index ecc4a3f033..4832da1823 100644 --- a/actions/apistatusesretweet.php +++ b/actions/apistatusesretweet.php @@ -78,22 +78,6 @@ class ApiStatusesRetweetAction extends ApiAuthAction $this->user = $this->auth_user; - if ($this->user->id == $this->original->profile_id) { - // TRANS: Client error displayed trying to repeat an own notice through the API. - $this->clientError(_('Cannot repeat your own notice.'), - 400, $this->format); - return false; - } - - $profile = $this->user->getProfile(); - - if ($profile->hasRepeated($id)) { - // TRANS: Client error displayed trying to re-repeat a notice through the API. - $this->clientError(_('Already repeated that notice.'), - 400, $this->format); - return false; - } - return true; } diff --git a/actions/approvegroup.php b/actions/approvegroup.php index c52e0e3c46..d46a4451c4 100644 --- a/actions/approvegroup.php +++ b/actions/approvegroup.php @@ -98,6 +98,7 @@ class ApprovegroupAction extends Action $cur = common_current_user(); if (empty($cur)) { + // TRANS: Client error displayed trying to approve group membership while not logged in. $this->clientError(_('Must be logged in.'), 403); return false; } @@ -105,10 +106,12 @@ class ApprovegroupAction extends Action if ($cur->isAdmin($this->group)) { $this->profile = Profile::staticGet('id', $this->arg('profile_id')); } else { + // TRANS: Client error displayed trying to approve group membership while not a group administrator. $this->clientError(_('Only group admin can approve or cancel join requests.'), 403); return false; } } else { + // TRANS: Client error displayed trying to approve group membership without specifying a profile to approve. $this->clientError(_('Must specify a profile.')); return false; } @@ -117,8 +120,21 @@ class ApprovegroupAction extends Action 'group_id' => $this->group->id)); if (empty($this->request)) { + // TRANS: Client error displayed trying to approve group membership for a non-existing request. + // TRANS: %s is a nickname. $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); } + + $this->approve = (bool)$this->arg('approve'); + $this->cancel = (bool)$this->arg('cancel'); + if (!$this->approve && !$this->cancel) { + // TRANS: Client error displayed trying to approve/deny group membership. + $this->clientError(_('Internal error: received neither cancel nor abort.')); + } + if ($this->approve && $this->cancel) { + // TRANS: Client error displayed trying to approve/deny group membership. + $this->clientError(_('Internal error: received both cancel and abort.')); + } return true; } @@ -136,9 +152,13 @@ class ApprovegroupAction extends Action parent::handle($args); try { - $this->profile->completeJoinGroup($this->group); + if ($this->approve) { + $this->request->complete(); + } elseif ($this->cancel) { + $this->request->abort(); + } } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), @@ -149,14 +169,20 @@ class ApprovegroupAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); - // TRANS: Title for leave group page after leaving. - $this->element('title', null, sprintf(_m('TITLE','%1$s left group %2$s'), + // TRANS: Title for leave group page after group join request is approved/disapproved. + // TRANS: %1$s is the user nickname, %2$s is the group nickname. + $this->element('title', null, sprintf(_m('TITLE','%1$s\'s request for %2$s'), $this->profile->nickname, $this->group->nickname)); $this->elementEnd('head'); $this->elementStart('body'); - $jf = new JoinForm($this, $this->group); - $jf->show(); + if ($this->approve) { + // TRANS: Message on page for group admin after approving a join request. + $this->element('p', 'success', _('Join request approved.')); + } elseif ($this->cancel) { + // TRANS: Message on page for group admin after rejecting a join request. + $this->element('p', 'success', _('Join request canceled.')); + } $this->elementEnd('body'); $this->elementEnd('html'); } else { diff --git a/actions/approvesub.php b/actions/approvesub.php new file mode 100644 index 0000000000..be07b6e877 --- /dev/null +++ b/actions/approvesub.php @@ -0,0 +1,145 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class ApprovesubAction extends Action +{ + var $profile = null; + + /** + * Prepare to run + */ + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (empty($cur)) { + // TRANS: Client error displayed trying to approve group membership while not logged in. + $this->clientError(_('Must be logged in.'), 403); + return false; + } + if ($this->arg('profile_id')) { + $this->profile = Profile::staticGet('id', $this->arg('profile_id')); + } else { + // TRANS: Client error displayed trying to approve subscriptionswithout specifying a profile to approve. + $this->clientError(_('Must specify a profile.')); + return false; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $this->profile->id, + 'subscribed' => $cur->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed trying to approve subscription for a non-existing request. + $this->clientError(sprintf(_('%s is not in the moderation queue for your subscriptions.'), $this->profile->nickname), 403); + } + + $this->approve = (bool)$this->arg('approve'); + $this->cancel = (bool)$this->arg('cancel'); + if (!$this->approve && !$this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription. + $this->clientError(_('Internal error: received neither cancel nor abort.')); + } + if ($this->approve && $this->cancel) { + // TRANS: Client error displayed trying to approve/deny subscription + $this->clientError(_('Internal error: received both cancel and abort.')); + } + return true; + } + + /** + * Handle the request + * + * On POST, add the current user to the group + * + * @param array $args unused + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $cur = common_current_user(); + + try { + if ($this->approve) { + $this->request->complete(); + } elseif ($this->cancel) { + $this->request->abort(); + } + } catch (Exception $e) { + common_log(LOG_ERR, "Exception canceling sub: " . $e->getMessage()); + // TRANS: Server error displayed when cancelling a queued subscription request fails. + // TRANS: %1$s is the leaving user's nickname, $2$s is the nickname for which the leave failed. + $this->serverError(sprintf(_('Could not cancel or approve request for user %1$s to join group %2$s.'), + $this->profile->nickname, $cur->nickname)); + return; + } + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title for subscription approval ajax return + // TRANS: %1$s is the approved user's nickname + $this->element('title', null, sprintf(_m('TITLE','%1$s\'s request'), + $this->profile->nickname)); + $this->elementEnd('head'); + $this->elementStart('body'); + if ($this->approve) { + // TRANS: Message on page for user after approving a subscription request. + $this->element('p', 'success', _('Subscription approved.')); + } elseif ($this->cancel) { + // TRANS: Message on page for user after rejecting a subscription request. + $this->element('p', 'success', _('Subscription canceled.')); + } + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subqueue', array('nickname' => + $cur->nickname)), + 303); + } + } +} diff --git a/actions/atompubmembershipfeed.php b/actions/atompubmembershipfeed.php index f7882d97d3..37e4a386a2 100644 --- a/actions/atompubmembershipfeed.php +++ b/actions/atompubmembershipfeed.php @@ -141,7 +141,7 @@ class AtompubmembershipfeedAction extends ApiAuthAction // TRANS: Title for group membership feed. // TRANS: %s is a username. - $feed->setTitle(sprintf(_("%s group memberships"), + $feed->setTitle(sprintf(_('Group memberships of %s'), $this->_profile->getBestName())); // TRANS: Subtitle for group membership feed. @@ -237,8 +237,7 @@ class AtompubmembershipfeedAction extends ApiAuthAction if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { if ($activity->verb != ActivityVerb::JOIN) { - // TRANS: Client error displayed when not using the POST verb. - // TRANS: Do not translate POST. + // TRANS: Client error displayed when not using the join verb. throw new ClientException(_('Can only handle join activities.')); return; } diff --git a/actions/cancelgroup.php b/actions/cancelgroup.php index 68d7f39139..3074e3ffa3 100644 --- a/actions/cancelgroup.php +++ b/actions/cancelgroup.php @@ -68,7 +68,6 @@ class CancelgroupAction extends Action $nickname = common_canonical_nickname($nickname_arg); // Permanent redirect on non-canonical nickname - if ($nickname_arg != $nickname) { $args = array('nickname' => $nickname); common_redirect(common_local_url('leavegroup', $args), 301); @@ -98,6 +97,7 @@ class CancelgroupAction extends Action $cur = common_current_user(); if (empty($cur)) { + // TRANS: Client error displayed when trying to leave a group while not logged in. $this->clientError(_('Must be logged in.'), 403); return false; } @@ -105,6 +105,8 @@ class CancelgroupAction extends Action if ($cur->isAdmin($this->group)) { $this->profile = Profile::staticGet('id', $this->arg('profile_id')); } else { + // TRANS: Client error displayed when trying to approve or cancel a group join request without + // TRANS: being a group administrator. $this->clientError(_('Only group admin can approve or cancel join requests.'), 403); return false; } @@ -116,6 +118,8 @@ class CancelgroupAction extends Action 'group_id' => $this->group->id)); if (empty($this->request)) { + // TRANS: Client error displayed when trying to approve a non-existing group join request. + // TRANS: %s is a user nickname. $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); } return true; @@ -135,9 +139,9 @@ class CancelgroupAction extends Action parent::handle($args); try { - $this->profile->cancelJoinGroup($this->group); + $this->request->abort(); } catch (Exception $e) { - common_log(LOG_ERROR, "Exception canceling group sub: " . $e->getMessage()); + common_log(LOG_ERR, "Exception canceling group sub: " . $e->getMessage()); // TRANS: Server error displayed when cancelling a queued group join request fails. // TRANS: %1$s is the leaving user's nickname, $2$s is the group nickname for which the leave failed. $this->serverError(sprintf(_('Could not cancel request for user %1$s to join group %2$s.'), @@ -149,6 +153,7 @@ class CancelgroupAction extends Action $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); // TRANS: Title for leave group page after leaving. + // TRANS: %s$s is the leaving user's name, %2$s is the group name. $this->element('title', null, sprintf(_m('TITLE','%1$s left group %2$s'), $this->profile->nickname, $this->group->nickname)); diff --git a/actions/cancelsubscription.php b/actions/cancelsubscription.php new file mode 100644 index 0000000000..0cd922ff70 --- /dev/null +++ b/actions/cancelsubscription.php @@ -0,0 +1,123 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Leave a group + * + * This is the action for leaving a group. It works more or less like the subscribe action + * for users. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class CancelsubscriptionAction extends Action +{ + + function handle($args) + { + parent::handle($args); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); + } + if (!common_logged_in()) { + // TRANS: Client error displayed when trying to leave a group while not logged in. + $this->clientError(_('Not logged in.')); + return; + } + + $user = common_current_user(); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname))); + return; + } + + /* Use a session token for 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; + } + + $other_id = $this->arg('unsubscribeto'); + + if (!$other_id) { + // TRANS: Client error displayed when trying to leave a group without specifying an ID. + $this->clientError(_('No profile ID in request.')); + return; + } + + $other = Profile::staticGet('id', $other_id); + + if (!$other) { + // TRANS: Client error displayed when trying to leave a non-existing group. + $this->clientError(_('No profile with that ID.')); + return; + } + + $this->request = Subscription_queue::pkeyGet(array('subscriber' => $user->id, + 'subscribed' => $other->id)); + + if (empty($this->request)) { + // TRANS: Client error displayed when trying to approve a non-existing group join request. + // TRANS: %s is a user nickname. + $this->clientError(sprintf(_('%s is not in the moderation queue for this group.'), $this->profile->nickname), 403); + } + + $this->request->abort(); + + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + // TRANS: Title after unsubscribing from a group. + $this->element('title', null, _m('TITLE','Unsubscribed')); + $this->elementEnd('head'); + $this->elementStart('body'); + $subscribe = new SubscribeForm($this, $other); + $subscribe->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect(common_local_url('subscriptions', + array('nickname' => $user->nickname)), + 303); + } + } +} diff --git a/actions/groupqueue.php b/actions/groupqueue.php index 687fa0b973..7cc32a9c69 100644 --- a/actions/groupqueue.php +++ b/actions/groupqueue.php @@ -52,7 +52,7 @@ class GroupqueueAction extends GroupDesignAction return true; } - // fixme most of this belongs in a base class, sounds common to most group actions? + // @todo FIXME: most of this belongs in a base class, sounds common to most group actions? function prepare($args) { parent::prepare($args); @@ -96,6 +96,7 @@ class GroupqueueAction extends GroupDesignAction $cur = common_current_user(); if (!$cur || !$cur->isAdmin($this->group)) { + // TRANS: Client error displayed when trying to approve group applicants without being a group administrator. $this->clientError(_('Only the group admin may approve users.')); return false; } @@ -105,12 +106,12 @@ class GroupqueueAction extends GroupDesignAction function title() { if ($this->page == 1) { - // TRANS: Title of the page showing pending group members still awaiting approval to join the group. + // TRANS: Title of the first page showing pending group members still awaiting approval to join the group. // TRANS: %s is the name of the group. return sprintf(_('%s group members awaiting approval'), $this->group->nickname); } else { - // TRANS: Title of the page showing pending group members still awaiting approval to join the group. + // TRANS: Title of all but the first page showing pending group members still awaiting approval to join the group. // TRANS: %1$s is the name of the group, %2$d is the page number of the members list. return sprintf(_('%1$s group members awaiting approval, page %2$d'), $this->group->nickname, @@ -155,11 +156,12 @@ class GroupqueueAction extends GroupDesignAction $members->free(); $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, - $this->page, 'groupmembers', + $this->page, 'groupqueue', array('nickname' => $this->group->nickname)); } } +// @todo FIXME: documentation missing. class GroupQueueList extends GroupMemberList { function newListItem($profile) @@ -168,32 +170,24 @@ class GroupQueueList extends GroupMemberList } } +// @todo FIXME: documentation missing. class GroupQueueListItem extends GroupMemberListItem { function showActions() { $this->startActions(); if (Event::handle('StartProfileListItemActionElements', array($this))) { - $this->showApproveButton(); - $this->showCancelButton(); + $this->showApproveButtons(); Event::handle('EndProfileListItemActionElements', array($this)); } $this->endActions(); } - function showApproveButton() + function showApproveButtons() { - $this->out->elementStart('li', 'entity_join'); + $this->out->elementStart('li', 'entity_approval'); $form = new ApproveGroupForm($this->out, $this->group, $this->profile); $form->show(); $this->out->elementEnd('li'); } - - function showCancelButton() - { - $this->out->elementStart('li', 'entity_leave'); - $bf = new CancelGroupForm($this->out, $this->group, $this->profile); - $bf->show(); - $this->out->elementEnd('li'); - } } diff --git a/actions/invite.php b/actions/invite.php index c64ff8adda..bbb6b26c11 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -54,7 +54,7 @@ class InviteAction extends CurrentUserDesignAction function sendInvitations() { - # CSRF protection + // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { $this->showForm(_('There was a problem with your session token. Try again, please.')); diff --git a/actions/joingroup.php b/actions/joingroup.php index f302b39e79..bb7b835915 100644 --- a/actions/joingroup.php +++ b/actions/joingroup.php @@ -154,7 +154,8 @@ class JoingroupAction extends Action $form = new CancelGroupForm($this, $this->group); } else { // wtf? - throw new Exception(_m("Unknown error joining group.")); + // TRANS: Exception thrown when there is an unknown error joining a group. + throw new Exception(_("Unknown error joining group.")); } $form->show(); $this->elementEnd('body'); diff --git a/actions/newnotice.php b/actions/newnotice.php index 3e601ae362..f48134c3b2 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -209,6 +209,10 @@ class NewnoticeAction extends Action $author_id = $user->id; $text = $content_shortened; + // Does the heavy-lifting for getting "To:" information + + ToSelector::fillOptions($this, $options); + if (Event::handle('StartNoticeSaveWeb', array($this, &$author_id, &$text, &$options))) { $notice = Notice::saveNew($user->id, $content_shortened, 'web', $options); @@ -344,7 +348,9 @@ class NewnoticeAction extends Action $inreplyto = null; } - $notice_form = new NoticeForm($this, '', $content, null, $inreplyto); + $notice_form = new NoticeForm($this, array('content' => $content, + 'inreplyto' => $inreplyto)); + $notice_form->show(); } diff --git a/actions/passwordsettings.php b/actions/passwordsettings.php index 465cb558a4..d9a6d32ae0 100644 --- a/actions/passwordsettings.php +++ b/actions/passwordsettings.php @@ -156,7 +156,7 @@ class PasswordsettingsAction extends SettingsAction $newpassword = $this->arg('newpassword'); $confirm = $this->arg('confirm'); - # Some validation + // Some validation if (strlen($newpassword) < 6) { // TRANS: Form validation error on page where to change password. diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 046bedf8ac..1961f94e9f 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -33,8 +33,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - /** * Change profile settings * @@ -46,7 +44,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class ProfilesettingsAction extends SettingsAction { /** @@ -127,15 +124,15 @@ class ProfilesettingsAction extends SettingsAction // TRANS: Tooltip for field label in form for profile settings. Plural // TRANS: is decided by the number of characters available for the // TRANS: biography (%d). - $bioInstr = sprintf(_m('Describe yourself and your interests in %d character', - 'Describe yourself and your interests in %d characters', + $bioInstr = sprintf(_m('Describe yourself and your interests in %d character.', + 'Describe yourself and your interests in %d characters.', $maxBio), $maxBio); } else { // TRANS: Tooltip for field label in form for profile settings. - $bioInstr = _('Describe yourself and your interests'); + $bioInstr = _('Describe yourself and your interests.'); } - // TRANS: Text area label in form for profile settings where users can provide. + // TRANS: Text area label in form for profile settings where users can provide // TRANS: their biography. $this->textarea('bio', _('Bio'), ($this->arg('bio')) ? $this->arg('bio') : $profile->bio, @@ -146,7 +143,7 @@ class ProfilesettingsAction extends SettingsAction $this->input('location', _('Location'), ($this->arg('location')) ? $this->arg('location') : $profile->location, // TRANS: Tooltip for field label in form for profile settings. - _('Where you are, like "City, State (or Region), Country"')); + _('Where you are, like "City, State (or Region), Country".')); $this->elementEnd('li'); if (common_config('location', 'share') == 'user') { $this->elementStart('li'); @@ -192,6 +189,19 @@ class ProfilesettingsAction extends SettingsAction ($this->arg('autosubscribe')) ? $this->boolean('autosubscribe') : $user->autosubscribe); $this->elementEnd('li'); + $this->elementStart('li'); + $this->dropdown('subscribe_policy', + // TRANS: Dropdown field label on profile settings, for what policies to apply when someone else tries to subscribe to your updates. + _('Subscription policy'), + // TRANS: Dropdown field option for following policy. + array(User::SUBSCRIBE_POLICY_OPEN => _('Let anyone follow me'), + // TRANS: Dropdown field option for following policy. + User::SUBSCRIBE_POLICY_MODERATE => _('Ask me first')), + // TRANS: Dropdown field title on group edit form. + _('Whether other users need your permission to follow your updates.'), + false, + (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy); + $this->elementEnd('li'); } $this->elementEnd('ul'); // TRANS: Button to save input in profile settings. @@ -234,6 +244,7 @@ class ProfilesettingsAction extends SettingsAction $bio = $this->trimmed('bio'); $location = $this->trimmed('location'); $autosubscribe = $this->boolean('autosubscribe'); + $subscribe_policy = $this->trimmed('subscribe_policy'); $language = $this->trimmed('language'); $timezone = $this->trimmed('timezone'); $tagstring = $this->trimmed('tags'); @@ -339,11 +350,12 @@ class ProfilesettingsAction extends SettingsAction } // XXX: XOR - if ($user->autosubscribe ^ $autosubscribe) { + if (($user->autosubscribe ^ $autosubscribe) || $user->subscribe_policy != $subscribe_policy) { $original = clone($user); $user->autosubscribe = $autosubscribe; + $user->subscribe_policy = $subscribe_policy; $result = $user->update($original); @@ -351,7 +363,7 @@ class ProfilesettingsAction extends SettingsAction common_log_db_error($user, 'UPDATE', __FILE__); // TRANS: Server error thrown when user profile settings could not be updated to // TRANS: automatically subscribe to any subscriber. - $this->serverError(_('Could not update user for autosubscribe.')); + $this->serverError(_('Could not update user for autosubscribe or subscribe_policy.')); return; } } diff --git a/actions/public.php b/actions/public.php index d2b0373dc6..871859cd33 100644 --- a/actions/public.php +++ b/actions/public.php @@ -85,8 +85,11 @@ class PublicAction extends Action common_set_returnto($this->selfUrl()); - $this->notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE, - NOTICES_PER_PAGE + 1); + $stream = new PublicNoticeStream(PublicNoticeStream::THREADED); + $this->notice = $stream->getNotices(($this->page-1)*NOTICES_PER_PAGE, + NOTICES_PER_PAGE + 1, + 0, + 0); if (!$this->notice) { // TRANS: Server error displayed when a public timeline cannot be retrieved. diff --git a/actions/publictagcloud.php b/actions/publictagcloud.php index 1432ca66a8..db8185bb13 100644 --- a/actions/publictagcloud.php +++ b/actions/publictagcloud.php @@ -100,7 +100,7 @@ class PublictagcloudAction extends Action function showContent() { - # This should probably be cached rather than recalculated + // This should probably be cached rather than recalculated $tags = new Notice_tag(); #Need to clear the selection and then only re-add the field diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index e30bf7af2e..71f673bd3b 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -19,7 +19,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -# You have 24 hours to claim your password +// You have 24 hours to claim your password define('MAX_RECOVERY_TIME', 24 * 60 * 60); @@ -81,7 +81,7 @@ class RecoverpasswordAction extends Action $touched = strtotime($confirm->modified); $email = $confirm->address; - # Burn this code + // Burn this code $result = $confirm->delete(); @@ -92,8 +92,8 @@ class RecoverpasswordAction extends Action return; } - # These should be reaped, but for now we just check mod time - # Note: it's still deleted; let's avoid a second attempt! + // These should be reaped, but for now we just check mod time + // Note: it's still deleted; let's avoid a second attempt! if ((time() - $touched) > MAX_RECOVERY_TIME) { common_log(LOG_WARNING, @@ -105,8 +105,8 @@ class RecoverpasswordAction extends Action return; } - # If we used an outstanding confirmation to send the email, - # it's been confirmed at this point. + // If we used an outstanding confirmation to send the email, + // it's been confirmed at this point. if (!$user->email) { $orig = clone($user); @@ -120,7 +120,7 @@ class RecoverpasswordAction extends Action } } - # Success! + // Success! $this->setTempUser($user); $this->showPasswordForm(); @@ -289,7 +289,7 @@ class RecoverpasswordAction extends Action } } - # See if it's an unconfirmed email address + // See if it's an unconfirmed email address if (!$user) { // Warning: it may actually be legit to have multiple folks @@ -314,7 +314,7 @@ class RecoverpasswordAction extends Action return; } - # Try to get an unconfirmed email address if they used a user name + // Try to get an unconfirmed email address if they used a user name if (!$user->email && !$confirm_email) { $confirm_email = new Confirm_address(); @@ -332,7 +332,7 @@ class RecoverpasswordAction extends Action return; } - # Success! We have a valid user and a confirmed or unconfirmed email address + // Success! We have a valid user and a confirmed or unconfirmed email address $confirm = new Confirm_address(); $confirm->code = common_confirmation_code(128); @@ -380,7 +380,7 @@ class RecoverpasswordAction extends Action function resetPassword() { - # CSRF protection + // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { // TRANS: Form validation error message. @@ -410,7 +410,7 @@ class RecoverpasswordAction extends Action return; } - # OK, we're ready to go + // OK, we're ready to go $original = clone($user); diff --git a/actions/register.php b/actions/register.php index 7b3c075156..084e3e6f1d 100644 --- a/actions/register.php +++ b/actions/register.php @@ -531,7 +531,7 @@ class RegisterAction extends Action $this->elementEnd('li'); } $this->elementEnd('ul'); - // TRANS: Field label on account registration page. + // TRANS: Button text to register a user on account registration page. $this->submit('submit', _m('BUTTON','Register')); $this->elementEnd('fieldset'); $this->elementEnd('form'); diff --git a/actions/repeat.php b/actions/repeat.php index 869c2ddd4e..44c57456fa 100644 --- a/actions/repeat.php +++ b/actions/repeat.php @@ -73,12 +73,6 @@ class RepeatAction extends Action return false; } - if ($this->user->id == $this->notice->profile_id) { - // TRANS: Client error displayed when trying to repeat an own notice. - $this->clientError(_('You cannot repeat your own notice.')); - return false; - } - $token = $this->trimmed('token-'.$id); if (empty($token) || $token != common_session_token()) { @@ -86,14 +80,6 @@ class RepeatAction extends Action return false; } - $profile = $this->user->getProfile(); - - if ($profile->hasRepeated($id)) { - // TRANS: Client error displayed when trying to repeat an already repeated notice. - $this->clientError(_('You already repeated that notice.')); - return false; - } - return true; } diff --git a/actions/sandbox.php b/actions/sandbox.php index d1ef4c86b2..ddbca40b2d 100644 --- a/actions/sandbox.php +++ b/actions/sandbox.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class SandboxAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class SandboxAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class SandboxAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SANDBOXUSER)) { + // TRANS: Client error displayed trying to sandbox users on a site where the feature is not enabled. $this->clientError(_('You cannot sandbox users on this site.')); return false; } @@ -69,6 +68,7 @@ class SandboxAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if ($this->profile->isSandboxed()) { + // TRANS: Client error displayed trying to sandbox an already sandboxed user. $this->clientError(_('User is already sandboxed.')); return false; } @@ -81,7 +81,6 @@ class SandboxAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->sandbox(); diff --git a/actions/sessionsadminpanel.php b/actions/sessionsadminpanel.php index e9bd1719f2..0d1ead1021 100644 --- a/actions/sessionsadminpanel.php +++ b/actions/sessionsadminpanel.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class SessionsadminpanelAction extends AdminPanelAction { /** @@ -48,10 +47,10 @@ class SessionsadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { - return _('Sessions'); + // TRANS: Title for the sessions administration panel. + return _m('TITLE','Sessions'); } /** @@ -59,9 +58,9 @@ class SessionsadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for the sessions administration panel. return _('Session settings for this StatusNet site'); } @@ -70,7 +69,6 @@ class SessionsadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new SessionsAdminPanelForm($this); @@ -83,7 +81,6 @@ class SessionsadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $booleans = array('sessions' => array('handle', 'debug')); @@ -123,6 +120,7 @@ class SessionsadminpanelAction extends AdminPanelAction } } +// @todo FIXME: Class documentation missing. class SessionsAdminPanelForm extends AdminForm { /** @@ -130,7 +128,6 @@ class SessionsAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'sessionsadminpanel'; @@ -141,7 +138,6 @@ class SessionsAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -152,7 +148,6 @@ class SessionsAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('sessionsadminpanel'); @@ -163,24 +158,31 @@ class SessionsAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->out->elementStart('fieldset', array('id' => 'settings_user_sessions')); - $this->out->element('legend', null, _('Sessions')); + // TRANS: Fieldset legend on the sessions administration panel. + $this->out->element('legend', null, _m('LEGEND','Sessions')); $this->out->elementStart('ul', 'form_data'); $this->li(); + // TRANS: Checkbox title on the sessions administration panel. + // TRANS: Indicates if StatusNet should handle session administration. $this->out->checkbox('handle', _('Handle sessions'), (bool) $this->value('handle', 'sessions'), - _('Whether to handle sessions ourselves.')); + // TRANS: Checkbox title on the sessions administration panel. + // TRANS: Indicates if StatusNet should handle session administration. + _('Handle sessions ourselves.')); $this->unli(); $this->li(); + // TRANS: Checkbox label on the sessions administration panel. + // TRANS: Indicates if StatusNet should write session debugging output. $this->out->checkbox('debug', _('Session debugging'), (bool) $this->value('debug', 'sessions'), - _('Turn on debugging output for sessions.')); + // TRANS: Checkbox title on the sessions administration panel. + _('Enable debugging output for sessions.')); $this->unli(); $this->out->elementEnd('ul'); @@ -193,9 +195,14 @@ class SessionsAdminPanelForm extends AdminForm * * @return void */ - function formActions() { - $this->out->submit('submit', _('Save'), 'submit', null, _('Save site settings')); + $this->out->submit('submit', + // TRANS: Submit button text on the sessions administration panel. + _m('BUTTON','Save'), + 'submit', + null, + // TRANS: Title for submit button on the sessions administration panel. + _('Save session settings')); } } diff --git a/actions/showapplication.php b/actions/showapplication.php index 02a4dc1517..38e6f1953e 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -75,11 +75,13 @@ class ShowApplicationAction extends OwnerDesignAction $this->owner = User::staticGet($this->application->owner); if (!common_logged_in()) { + // TRANS: Client error displayed trying to display an OAuth application while not logged in. $this->clientError(_('You must be logged in to view an application.')); return false; } if (empty($this->application)) { + // TRANS: Client error displayed trying to display a non-existing OAuth application. $this->clientError(_('No such application.'), 404); return false; } @@ -87,6 +89,7 @@ class ShowApplicationAction extends OwnerDesignAction $cur = common_current_user(); if ($cur->id != $this->owner->id) { + // TRANS: Client error displayed trying to display an OAuth application for which the logged in user is not the owner. $this->clientError(_('You are not the owner of this application.'), 401); return false; } @@ -148,6 +151,7 @@ class ShowApplicationAction extends OwnerDesignAction $consumer = $this->application->getConsumer(); $this->elementStart('div', 'entity_profile vcard'); + // TRANS: Header on the OAuth application page. $this->element('h2', null, _('Application profile')); if (!empty($this->application->icon)) { $this->element('img', array('src' => $this->application->icon, @@ -176,7 +180,12 @@ class ShowApplicationAction extends OwnerDesignAction $userCnt = $appUsers->count(); $this->raw(sprintf( - _('Created by %1$s - %2$s access by default - %3$d users'), + // TRANS: Information output on an OAuth application page. + // TRANS: %1$s is the application creator, %2$s is "read-only" or "read-write", + // TRANS: %3$d is the number of users using the OAuth application. + _m('Created by %1$s - %2$s access by default - %3$d user', + 'Created by %1$s - %2$s access by default - %3$d users', + $userCnt), $profile->getBestName(), $defaultAccess, $userCnt @@ -186,13 +195,15 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('div'); $this->elementStart('div', 'entity_actions'); + // TRANS: Header on the OAuth application page. $this->element('h2', null, _('Application actions')); $this->elementStart('ul'); $this->elementStart('li', 'entity_edit'); $this->element('a', array('href' => common_local_url('editapplication', array('id' => $this->application->id))), - 'Edit'); + // TRANS: Link text to edit application on the OAuth application page. + _m('EDITAPP','Edit')); $this->elementEnd('li'); $this->elementStart('li', 'entity_reset_keysecret'); @@ -209,6 +220,8 @@ class ShowApplicationAction extends OwnerDesignAction 'id' => 'reset', 'name' => 'reset', 'class' => 'submit', + // TRANS: Button text on the OAuth application page. + // TRANS: Resets the OAuth consumer key and secret. 'value' => _('Reset key & secret'), 'onClick' => 'return confirmReset()')); $this->elementEnd('fieldset'); @@ -225,7 +238,8 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); - $this->submit('delete', _('Delete')); + // TRANS: Submit button text the OAuth application page to delete an application. + $this->submit('delete', _m('BUTTON','Delete')); $this->elementEnd('fieldset'); $this->elementEnd('form'); $this->elementEnd('li'); @@ -234,6 +248,7 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('div'); $this->elementStart('div', 'entity_data'); + // TRANS: Header on the OAuth application page. $this->element('h2', null, _('Application info')); $this->element('div', 'entity_consumer_key', @@ -252,7 +267,8 @@ class ShowApplicationAction extends OwnerDesignAction $this->element('div', 'entity_authorize_url', common_local_url('ApiOauthAuthorize')); $this->element('p', 'note', - _('Note: We support HMAC-SHA1 signatures. We do not support the plaintext signature method.')); + // TRANS: Note on the OAuth application page about signature support. + _('Note: HMAC-SHA1 signatures are supported. The plaintext signature method is not supported.')); $this->elementEnd('div'); $this->elementStart('p', array('id' => 'application_action')); @@ -272,6 +288,7 @@ class ShowApplicationAction extends OwnerDesignAction { parent::showScripts(); + // TRANS: Text in confirmation dialog to reset consumer key and secret for an OAuth application. $msg = _('Are you sure you want to reset your consumer key and secret?'); $js = 'function confirmReset() { '; diff --git a/actions/showgroup.php b/actions/showgroup.php index aa1f620880..512ca6a0ee 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -324,6 +324,8 @@ class ShowgroupAction extends GroupDesignAction $this->element('h2', null, _('Statistics')); $this->elementStart('dl'); + + // TRANS: Label for group creation date. $this->element('dt', null, _m('LABEL','Created')); $this->element('dd', 'entity_created', date('j M Y', strtotime($this->group->created))); @@ -363,6 +365,18 @@ class ShowgroupAction extends GroupDesignAction $this->raw(common_markup_to_html($m)); $this->elementEnd('div'); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (!empty($cur) && $cur->isMember($this->group)) { + $options['to_group'] = $this->group; + } + + return $options; + } } class GroupAdminSection extends ProfileSection @@ -382,8 +396,8 @@ class GroupAdminSection extends ProfileSection function title() { - // TRANS: Header for list of group administrators on a group page (h2). - return _('Admins'); + // TRANS: Title for list of group administrators on a group page. + return _m('TITLE','Admins'); } function divId() diff --git a/actions/shownotice.php b/actions/shownotice.php index b8927372bb..b6d0625e13 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -44,25 +44,21 @@ require_once INSTALLDIR.'/lib/feedlist.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 ShownoticeAction extends OwnerDesignAction { /** * Notice object to show */ - var $notice = null; /** * Profile of the notice object */ - var $profile = null; /** * Avatar of the profile of the notice object */ - var $avatar = null; /** @@ -74,7 +70,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return success flag */ - function prepare($args) { parent::prepare($args); @@ -82,24 +77,25 @@ class ShownoticeAction extends OwnerDesignAction StatusNet::setApi(true); } - $id = $this->arg('notice'); + $this->notice = $this->getNotice(); - $this->notice = Notice::staticGet($id); + $cur = common_current_user(); - if (empty($this->notice)) { - // Did we used to have it, and it got deleted? - $deleted = Deleted_notice::staticGet($id); - if (!empty($deleted)) { - $this->clientError(_('Notice deleted.'), 410); - } else { - $this->clientError(_('No such notice.'), 404); - } - return false; + if (!empty($cur)) { + $curProfile = $cur->getProfile(); + } else { + $curProfile = null; + } + + if (!$this->notice->inScope($curProfile)) { + // TRANS: Client exception thrown when trying a view a notice the user has no access to. + throw new ClientException(_('Not available.'), 403); } $this->profile = $this->notice->getProfile(); if (empty($this->profile)) { + // TRANS: Server error displayed trying to show a notice without a connected profile. $this->serverError(_('Notice has no profile.'), 500); return false; } @@ -111,12 +107,38 @@ class ShownoticeAction extends OwnerDesignAction return true; } + /** + * Fetch the notice to show. This may be overridden by child classes to + * customize what we fetch without duplicating all of the prepare() method. + * + * @return Notice + */ + function getNotice() + { + $id = $this->arg('notice'); + + $notice = Notice::staticGet('id', $id); + + if (empty($notice)) { + // Did we used to have it, and it got deleted? + $deleted = Deleted_notice::staticGet($id); + if (!empty($deleted)) { + // TRANS: Client error displayed trying to show a deleted notice. + $this->clientError(_('Notice deleted.'), 410); + } else { + // TRANS: Client error displayed trying to show a non-existing notice. + $this->clientError(_('No such notice.'), 404); + } + return false; + } + return $notice; + } + /** * Is this action read-only? * * @return boolean true */ - function isReadOnly($args) { return true; @@ -130,7 +152,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return int last-modified date as unix timestamp */ - function lastModified() { return max(strtotime($this->notice->modified), @@ -147,7 +168,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return string etag */ - function etag() { $avtime = ($this->avatar) ? @@ -167,11 +187,12 @@ class ShownoticeAction extends OwnerDesignAction * * @return string title of the page */ - function title() { $base = $this->profile->getFancyName(); + // TRANS: Title of the page that shows a notice. + // TRANS: %1$s is a user name, %2$s is the notice creation date/time. return sprintf(_('%1$s\'s status on %2$s'), $base, common_exact_date($this->notice->created)); @@ -186,7 +207,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function handle($args) { parent::handle($args); @@ -218,7 +238,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function showLocalNavBlock() { } @@ -230,7 +249,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function showContent() { $this->elementStart('ol', array('class' => 'notices xoxo')); @@ -245,7 +263,8 @@ class ShownoticeAction extends OwnerDesignAction $this->xw->startDocument('1.0', 'UTF-8'); $this->elementStart('html'); $this->elementStart('head'); - $this->element('title', null, _('Notice')); + // TRANS: Title for page that shows a notice. + $this->element('title', null, _m('TITLE','Notice')); $this->elementEnd('head'); $this->elementStart('body'); $nli = new NoticeListItem($this->notice, $this); @@ -259,7 +278,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function showPageNoticeBlock() { } @@ -269,7 +287,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function showAside() { } @@ -280,7 +297,6 @@ class ShownoticeAction extends OwnerDesignAction * * @return void */ - function extraHead() { $user = User::staticGet($this->profile->id); @@ -323,16 +339,16 @@ class ShownoticeAction extends OwnerDesignAction } } +// @todo FIXME: Class documentation missing. class SingleNoticeItem extends DoFollowListItem { /** - * recipe function for displaying a single notice. + * Recipe function for displaying a single notice. * * We overload to show attachments. * * @return void */ - function show() { $this->showStart(); @@ -363,7 +379,6 @@ class SingleNoticeItem extends DoFollowListItem * * @return void */ - function showAvatar() { $avatar_size = AVATAR_PROFILE_SIZE; diff --git a/actions/showstream.php b/actions/showstream.php index 10085d94d7..fed9685b35 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -65,7 +65,8 @@ class ShowstreamAction extends ProfileAction $base = $this->profile->getFancyName(); if (!empty($this->tag)) { if ($this->page == 1) { - // TRANS: Page title showing tagged notices in one user's stream. %1$s is the username, %2$s is the hash tag. + // TRANS: Page title showing tagged notices in one user's stream. + // TRANS: %1$s is the username, %2$s is the hash tag. return sprintf(_('%1$s tagged %2$s'), $base, $this->tag); } else { // TRANS: Page title showing tagged notices in one user's stream. @@ -153,6 +154,8 @@ class ShowstreamAction extends ProfileAction array( 'id' => $this->user->id, 'format' => 'atom')), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. sprintf(_('Notice feed for %s (Atom)'), $this->user->nickname)), new Feed(Feed::FOAF, @@ -275,6 +278,18 @@ class ShowstreamAction extends ProfileAction $cloud = new PersonalTagCloudSection($this, $this->user); $cloud->show(); } + + function noticeFormOptions() + { + $options = parent::noticeFormOptions(); + $cur = common_current_user(); + + if (empty($cur) || $cur->id != $this->profile->id) { + $options['to_profile'] = $this->profile; + } + + return $options; + } } // We don't show the author for a profile, since we already know who it is! diff --git a/actions/silence.php b/actions/silence.php index 09cc480d9e..c44aa3a6d7 100644 --- a/actions/silence.php +++ b/actions/silence.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class SilenceAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class SilenceAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class SilenceAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SILENCEUSER)) { + // TRANS: Client error displayed trying to silence a user on a site where the feature is not enabled. $this->clientError(_('You cannot silence users on this site.')); return false; } @@ -69,6 +68,7 @@ class SilenceAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if ($this->profile->isSilenced()) { + // TRANS: Client error displayed trying to silence an already silenced user. $this->clientError(_('User is already silenced.')); return false; } @@ -81,7 +81,6 @@ class SilenceAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->silence(); diff --git a/actions/siteadminpanel.php b/actions/siteadminpanel.php index 4238b3e85a..29813ca3b9 100644 --- a/actions/siteadminpanel.php +++ b/actions/siteadminpanel.php @@ -44,7 +44,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class SiteadminpanelAction extends AdminPanelAction { /** @@ -52,10 +51,10 @@ class SiteadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { - return _('Site'); + // TRANS: Title for site administration panel. + return _m('TITLE','Site'); } /** @@ -63,9 +62,9 @@ class SiteadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for site administration panel. return _('Basic settings for this StatusNet site'); } @@ -74,7 +73,6 @@ class SiteadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new SiteAdminPanelForm($this); @@ -87,7 +85,6 @@ class SiteadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $settings = array( @@ -130,6 +127,7 @@ class SiteadminpanelAction extends AdminPanelAction // Validate site name if (empty($values['site']['name'])) { + // TRANS: Client error displayed trying to save an empty site name. $this->clientError(_('Site name must have non-zero length.')); } @@ -138,9 +136,11 @@ class SiteadminpanelAction extends AdminPanelAction $values['site']['email'] = common_canonical_email($values['site']['email']); if (empty($values['site']['email'])) { + // TRANS: Client error displayed trying to save site settings without a contact address. $this->clientError(_('You must have a valid contact email address.')); } if (!Validate::email($values['site']['email'], common_config('email', 'check_domain'))) { + // TRANS: Client error displayed trying to save site settings without a valid contact address. $this->clientError(_('Not a valid email address.')); } @@ -148,6 +148,7 @@ class SiteadminpanelAction extends AdminPanelAction if (is_null($values['site']['timezone']) || !in_array($values['site']['timezone'], DateTimeZone::listIdentifiers())) { + // TRANS: Client error displayed trying to save site settings without a timezone. $this->clientError(_('Timezone not selected.')); return; } @@ -156,24 +157,28 @@ class SiteadminpanelAction extends AdminPanelAction if (!is_null($values['site']['language']) && !in_array($values['site']['language'], array_keys(get_nice_language_list()))) { + // TRANS: Client error displayed trying to save site settings with an invalid language code. + // TRANS: %s is the invalid language code. $this->clientError(sprintf(_('Unknown language "%s".'), $values['site']['language'])); } // Validate text limit if (!Validate::number($values['site']['textlimit'], array('min' => 0))) { - $this->clientError(_("Minimum text limit is 0 (unlimited).")); + // TRANS: Client error displayed trying to save site settings with a text limit below 0. + $this->clientError(_('Minimum text limit is 0 (unlimited).')); } // Validate dupe limit if (!Validate::number($values['site']['dupelimit'], array('min' => 1))) { + // TRANS: Client error displayed trying to save site settings with a text limit below 1. $this->clientError(_("Dupe limit must be one or more seconds.")); } - } } +// @todo FIXME: Class documentation missing. class SiteAdminPanelForm extends AdminForm { /** @@ -181,7 +186,6 @@ class SiteAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'form_site_admin_panel'; @@ -192,7 +196,6 @@ class SiteAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -203,7 +206,6 @@ class SiteAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('siteadminpanel'); @@ -214,35 +216,44 @@ class SiteAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->out->elementStart('fieldset', array('id' => 'settings_admin_general')); - $this->out->element('legend', null, _('General')); + // TRANS: Fieldset legend on site settings panel. + $this->out->element('legend', null, _m('LEGEND','General')); $this->out->elementStart('ul', 'form_data'); $this->li(); - $this->input('name', _('Site name'), - _('The name of your site, like "Yourcompany Microblog"')); + // TRANS: Field label on site settings panel. + $this->input('name', _m('LABEL','Site name'), + // TRANS: Field title on site settings panel. + _('The name of your site, like "Yourcompany Microblog".')); $this->unli(); $this->li(); + // TRANS: Field label on site settings panel. $this->input('broughtby', _('Brought by'), - _('Text used for credits link in footer of each page')); + // TRANS: Field title on site settings panel. + _('Text used for credits link in footer of each page.')); $this->unli(); $this->li(); + // TRANS: Field label on site settings panel. $this->input('broughtbyurl', _('Brought by URL'), - _('URL used for credits link in footer of each page')); + // TRANS: Field title on site settings panel. + _('URL used for credits link in footer of each page.')); $this->unli(); $this->li(); + // TRANS: Field label on site settings panel. $this->input('email', _('Email'), - _('Contact email address for your site')); + // TRANS: Field title on site settings panel. + _('Contact email address for your site.')); $this->unli(); $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset'); $this->out->elementStart('fieldset', array('id' => 'settings_admin_local')); - $this->out->element('legend', null, _('Local')); + // TRANS: Fieldset legend on site settings panel. + $this->out->element('legend', null, _m('LEGEND','Local')); $this->out->elementStart('ul', 'form_data'); $timezones = array(); @@ -253,14 +264,20 @@ class SiteAdminPanelForm extends AdminForm asort($timezones); $this->li(); + // TRANS: Dropdown label on site settings panel. $this->out->dropdown('timezone', _('Default timezone'), + // TRANS: Dropdown title on site settings panel. $timezones, _('Default timezone for the site; usually UTC.'), true, $this->value('timezone')); $this->unli(); $this->li(); - $this->out->dropdown('language', _('Default language'), - get_nice_language_list(), _('Site language when autodetection from browser settings is not available'), + $this->out->dropdown('language', + // TRANS: Dropdown label on site settings panel. + _('Default language'), + get_nice_language_list(), + // TRANS: Dropdown title on site settings panel. + _('Site language when autodetection from browser settings is not available'), false, $this->value('language')); $this->unli(); @@ -268,14 +285,23 @@ class SiteAdminPanelForm extends AdminForm $this->out->elementEnd('fieldset'); $this->out->elementStart('fieldset', array('id' => 'settings_admin_limits')); - $this->out->element('legend', null, _('Limits')); + // TRANS: Fieldset legend on site settings panel. + $this->out->element('legend', null, _m('LEGEND','Limits')); $this->out->elementStart('ul', 'form_data'); $this->li(); - $this->input('textlimit', _('Text limit'), _('Maximum number of characters for notices.')); + $this->input('textlimit', + // TRANS: Field label on site settings panel. + _('Text limit'), + // TRANS: Field title on site settings panel. + _('Maximum number of characters for notices.')); $this->unli(); $this->li(); - $this->input('dupelimit', _('Dupe limit'), _('How long users must wait (in seconds) to post the same thing again.')); + $this->input('dupelimit', + // TRANS: Field label on site settings panel. + _('Dupe limit'), + // TRANS: Field title on site settings panel. + _('How long users must wait (in seconds) to post the same thing again.')); $this->unli(); $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset'); @@ -286,9 +312,14 @@ class SiteAdminPanelForm extends AdminForm * * @return void */ - function formActions() { - $this->out->submit('submit', _('Save'), 'submit', null, _('Save site settings')); + $this->out->submit('submit', + // TRANS: Button text for saving site settings. + _m('BUTTON','Save'), + 'submit', + null, + // TRANS: Button title for saving site settings. + _('Save site settings')); } } diff --git a/actions/snapshotadminpanel.php b/actions/snapshotadminpanel.php index be0a793e51..9790947071 100644 --- a/actions/snapshotadminpanel.php +++ b/actions/snapshotadminpanel.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class SnapshotadminpanelAction extends AdminPanelAction { /** @@ -48,10 +47,10 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return string page title */ - function title() { - return _('Snapshots'); + // TRANS: Title for admin panel to configure snapshots. + return _m('TITLE','Snapshots'); } /** @@ -59,9 +58,9 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return string instructions */ - function getInstructions() { + // TRANS: Instructions for admin panel to configure snapshots. return _('Manage snapshot configuration'); } @@ -70,7 +69,6 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return void */ - function showForm() { $form = new SnapshotAdminPanelForm($this); @@ -83,7 +81,6 @@ class SnapshotadminpanelAction extends AdminPanelAction * * @return void */ - function saveSettings() { static $settings = array( @@ -124,12 +121,14 @@ class SnapshotadminpanelAction extends AdminPanelAction // Validate snapshot run value if (!in_array($values['snapshot']['run'], array('web', 'cron', 'never'))) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid run value. $this->clientError(_('Invalid snapshot run value.')); } // Validate snapshot frequency value if (!Validate::number($values['snapshot']['frequency'])) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid value for frequency. $this->clientError(_('Snapshot frequency must be a number.')); } @@ -141,11 +140,13 @@ class SnapshotadminpanelAction extends AdminPanelAction array('allowed_schemes' => array('http', 'https') ) )) { + // TRANS: Client error displayed on admin panel for snapshots when providing an invalid report URL. $this->clientError(_('Invalid snapshot report URL.')); } } } +// @todo FIXME: add documentation class SnapshotAdminPanelForm extends AdminForm { /** @@ -153,7 +154,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return int ID of the form */ - function id() { return 'form_snapshot_admin_panel'; @@ -164,7 +164,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return string class of the form */ - function formClass() { return 'form_settings'; @@ -175,7 +174,6 @@ class SnapshotAdminPanelForm extends AdminForm * * @return string URL of the action */ - function action() { return common_local_url('snapshotadminpanel'); @@ -186,26 +184,31 @@ class SnapshotAdminPanelForm extends AdminForm * * @return void */ - function formData() { $this->out->elementStart( 'fieldset', array('id' => 'settings_admin_snapshots') ); - $this->out->element('legend', null, _('Snapshots')); + // TRANS: Fieldset legend on admin panel for snapshots. + $this->out->element('legend', null, _m('LEGEND','Snapshots')); $this->out->elementStart('ul', 'form_data'); $this->li(); $snapshot = array( + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'web' => _('Randomly during web hit'), + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'cron' => _('In a scheduled job'), + // TRANS: Option in dropdown for snapshot method in admin panel for snapshots. 'never' => _('Never') ); $this->out->dropdown( 'run', + // TRANS: Dropdown label for snapshot method in admin panel for snapshots. _('Data snapshots'), $snapshot, - _('When to send statistical data to status.net servers'), + // TRANS: Dropdown title for snapshot method in admin panel for snapshots. + _('When to send statistical data to status.net servers.'), false, $this->value('run', 'snapshot') ); @@ -214,8 +217,10 @@ class SnapshotAdminPanelForm extends AdminForm $this->li(); $this->input( 'frequency', + // TRANS: Input field label for snapshot frequency in admin panel for snapshots. _('Frequency'), - _('Snapshots will be sent once every N web hits'), + // TRANS: Input field title for snapshot frequency in admin panel for snapshots. + _('Snapshots will be sent once every N web hits.'), 'snapshot' ); $this->unli(); @@ -223,8 +228,10 @@ class SnapshotAdminPanelForm extends AdminForm $this->li(); $this->input( 'reporturl', + // TRANS: Input field label for snapshot report URL in admin panel for snapshots. _('Report URL'), - _('Snapshots will be sent to this URL'), + // TRANS: Input field title for snapshot report URL in admin panel for snapshots. + _('Snapshots will be sent to this URL.'), 'snapshot' ); $this->unli(); @@ -237,15 +244,16 @@ class SnapshotAdminPanelForm extends AdminForm * * @return void */ - function formActions() { $this->out->submit( 'submit', - _('Save'), + // TRANS: Button text to save snapshot settings. + _m('BUTTON','Save'), 'submit', null, - _('Save snapshot settings') + // TRANS: Title for button to save snapshot settings. + _('Save snapshot settings.') ); } } diff --git a/actions/subqueue.php b/actions/subqueue.php new file mode 100644 index 0000000000..38bc16e562 --- /dev/null +++ b/actions/subqueue.php @@ -0,0 +1,142 @@ +. + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once(INSTALLDIR.'/lib/profilelist.php'); + +/** + * List of group members + * + * @category Group + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ +class SubqueueAction extends GalleryAction +{ + var $page = null; + + function isReadOnly($args) + { + return true; + } + + // @todo FIXME: most of this belongs in a base class, sounds common to most group actions? + function prepare($args) + { + parent::prepare($args); + + $cur = common_current_user(); + if (!$cur || $cur->id != $this->profile->id) { + // TRANS: Client error displayed when trying to approve group applicants without being a group administrator. + $this->clientError(_('You may only approve your own pending subscriptions.')); + return false; + } + return true; + } + + function title() + { + if ($this->page == 1) { + // TRANS: Title of the first page showing pending subscribers still awaiting approval. + // TRANS: %s is the name of the user. + return sprintf(_('%s subscribers awaiting approval'), + $this->profile->nickname); + } else { + // TRANS: Title of all but the first page showing pending subscribersmembers still awaiting approval. + // TRANS: %1$s is the name of the user, %2$d is the page number of the members list. + return sprintf(_('%1$s subscribers awaiting approval, page %2$d'), + $this->profile->nickname, + $this->page); + } + } + + function showPageNotice() + { + $this->element('p', 'instructions', + // TRANS: Page notice for group members page. + _('A list of users awaiting approval to subscribe to you.')); + } + + + function showContent() + { + $offset = ($this->page-1) * PROFILES_PER_PAGE; + $limit = PROFILES_PER_PAGE + 1; + + $cnt = 0; + + $members = $this->profile->getRequests($offset, $limit); + + if ($members) { + // @fixme change! + $member_list = new SubQueueList($members, $this); + $cnt = $member_list->show(); + } + + $members->free(); + + $this->pagination($this->page > 1, $cnt > PROFILES_PER_PAGE, + $this->page, 'subqueue', + array('nickname' => $this->profile->nickname)); // urgh + } +} + +class SubQueueList extends ProfileList +{ + function newListItem($profile) + { + return new SubQueueListItem($profile, $this->action); + } +} + +class SubQueueListItem extends ProfileListItem +{ + function showActions() + { + $this->startActions(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showApproveButtons(); + Event::handle('EndProfileListItemActionElements', array($this)); + } + $this->endActions(); + } + + function showApproveButtons() + { + $this->out->elementStart('li', 'entity_approval'); + $form = new ApproveSubForm($this->out, $this->profile); + $form->show(); + $this->out->elementEnd('li'); + } +} diff --git a/actions/subscribe.php b/actions/subscribe.php index 3837915d53..fad153fc6e 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -139,8 +139,8 @@ class SubscribeAction extends Action { // Throws exception on error - Subscription::start($this->user->getProfile(), - $this->other); + $sub = Subscription::start($this->user->getProfile(), + $this->other); if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); @@ -149,8 +149,12 @@ class SubscribeAction extends Action $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $this->other); - $unsubscribe->show(); + if ($sub instanceof Subscription) { + $form = new UnsubscribeForm($this, $this->other); + } else { + $form = new CancelSubscriptionForm($this, $this->other); + } + $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); } else { diff --git a/actions/sup.php b/actions/sup.php index c4da9d3db6..911f0d9e55 100644 --- a/actions/sup.php +++ b/actions/sup.php @@ -61,8 +61,8 @@ class SupAction extends Action { $notice = new Notice(); - # XXX: cache this. Depends on how big this protocol becomes; - # Re-doing this query every 15 seconds isn't the end of the world. + // XXX: cache this. Depends on how big this protocol becomes; + // Re-doing this query every 15 seconds isn't the end of the world. $divider = common_sql_date(time() - $seconds); diff --git a/actions/tag.php b/actions/tag.php index 944cda1f4a..045fac97b5 100644 --- a/actions/tag.php +++ b/actions/tag.php @@ -19,9 +19,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +// @todo FIXME: documentation missing. class TagAction extends Action { - var $notice; function prepare($args) @@ -48,7 +48,7 @@ class TagAction extends Action $this->notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1); if($this->page > 1 && $this->notice->N == 0){ - // TRANS: Server error when page not found (404) + // TRANS: Server error when page not found (404). $this->serverError(_('No such page.'),$code=404); } @@ -88,18 +88,24 @@ class TagAction extends Action return array(new Feed(Feed::RSS1, common_local_url('tagrss', array('tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (RSS 1.0)'), $this->tag)), new Feed(Feed::RSS2, common_local_url('ApiTimelineTag', array('format' => 'rss', 'tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (RSS 2.0)'), $this->tag)), new Feed(Feed::ATOM, common_local_url('ApiTimelineTag', array('format' => 'atom', 'tag' => $this->tag)), + // TRANS: Link label for feed on "notices with tag" page. + // TRANS: %s is the tag the feed is for. sprintf(_('Notice feed for tag %s (Atom)'), $this->tag))); } @@ -107,7 +113,7 @@ class TagAction extends Action function showContent() { if(Event::handle('StartTagShowContent', array($this))) { - + $nl = new NoticeList($this->notice, $this); $cnt = $nl->show(); diff --git a/actions/tagrss.php b/actions/tagrss.php index 467a64abed..5f4831d221 100644 --- a/actions/tagrss.php +++ b/actions/tagrss.php @@ -22,7 +22,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } require_once(INSTALLDIR.'/lib/rssaction.php'); // Formatting of RSS handled by Rss10Action - class TagrssAction extends Rss10Action { var $tag; @@ -32,6 +31,7 @@ class TagrssAction extends Rss10Action $tag = common_canonical_tag($this->trimmed('tag')); $this->tag = Notice_tag::staticGet('tag', $tag); if (!$this->tag) { + // TRANS: Client error when requesting a tag feed for a non-existing tag. $this->clientError(_('No such tag.')); return false; } else { @@ -62,6 +62,8 @@ class TagrssAction extends Rss10Action $c = array('url' => common_local_url('tagrss', array('tag' => $tagname)), 'title' => $tagname, 'link' => common_local_url('tagrss', array('tag' => $tagname)), + // TRANS: Tag feed description. + // TRANS: %1$s is the tag name, %2$s is the StatusNet sitename. 'description' => sprintf(_('Updates tagged with %1$s on %2$s!'), $tagname, common_config('site', 'name'))); return $c; diff --git a/actions/unsandbox.php b/actions/unsandbox.php index d50b5072ee..98bd2d4f54 100644 --- a/actions/unsandbox.php +++ b/actions/unsandbox.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class UnsandboxAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class UnsandboxAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class UnsandboxAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SANDBOXUSER)) { + // TRANS: Client error on page to unsandbox a user when the feature is not enabled. $this->clientError(_('You cannot sandbox users on this site.')); return false; } @@ -69,6 +68,7 @@ class UnsandboxAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if (!$this->profile->isSandboxed()) { + // TRANS: Client error on page to unsilence a user when the to be unsandboxed user has not been sandboxed. $this->clientError(_('User is not sandboxed.')); return false; } @@ -81,7 +81,6 @@ class UnsandboxAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->unsandbox(); diff --git a/actions/unsilence.php b/actions/unsilence.php index 7d282c3661..723bf1bed5 100644 --- a/actions/unsilence.php +++ b/actions/unsilence.php @@ -40,7 +40,6 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ - class UnsilenceAction extends ProfileFormAction { /** @@ -50,7 +49,6 @@ class UnsilenceAction extends ProfileFormAction * * @return boolean success flag */ - function prepare($args) { if (!parent::prepare($args)) { @@ -62,6 +60,7 @@ class UnsilenceAction extends ProfileFormAction assert(!empty($cur)); // checked by parent if (!$cur->hasRight(Right::SILENCEUSER)) { + // TRANS: Client error on page to unsilence a user when the feature is not enabled. $this->clientError(_('You cannot silence users on this site.')); return false; } @@ -69,6 +68,7 @@ class UnsilenceAction extends ProfileFormAction assert(!empty($this->profile)); // checked by parent if (!$this->profile->isSilenced()) { + // TRANS: Client error on page to unsilence a user when the to be unsilenced user has not been silenced. $this->clientError(_('User is not silenced.')); return false; } @@ -81,7 +81,6 @@ class UnsilenceAction extends ProfileFormAction * * @return void */ - function handlePost() { $this->profile->unsilence(); diff --git a/actions/unsubscribe.php b/actions/unsubscribe.php index 57ca15d687..8f90619f64 100644 --- a/actions/unsubscribe.php +++ b/actions/unsubscribe.php @@ -44,11 +44,11 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { */ class UnsubscribeAction extends Action { - function handle($args) { parent::handle($args); if (!common_logged_in()) { + // TRANS: Client error displayed when trying to unsubscribe while not logged in. $this->clientError(_('Not logged in.')); return; } @@ -74,6 +74,7 @@ class UnsubscribeAction extends Action $other_id = $this->arg('unsubscribeto'); if (!$other_id) { + // TRANS: Client error displayed when trying to unsubscribe without providing a profile ID. $this->clientError(_('No profile ID in request.')); return; } @@ -81,6 +82,7 @@ class UnsubscribeAction extends Action $other = Profile::staticGet('id', $other_id); if (!$other) { + // TRANS: Client error displayed when trying to unsubscribe while providing a non-existing profile ID. $this->clientError(_('No profile with that ID.')); return; } @@ -95,6 +97,7 @@ class UnsubscribeAction extends Action if ($this->boolean('ajax')) { $this->startHTML('text/xml;charset=utf-8'); $this->elementStart('head'); + // TRANS: Page title for page to unsubscribe. $this->element('title', null, _('Unsubscribed')); $this->elementEnd('head'); $this->elementStart('body'); diff --git a/actions/updateprofile.php b/actions/updateprofile.php index bae6108cce..e5c0803495 100644 --- a/actions/updateprofile.php +++ b/actions/updateprofile.php @@ -45,7 +45,6 @@ require_once INSTALLDIR.'/extlib/libomb/service_provider.php'; */ class UpdateprofileAction extends Action { - /** * For initializing members of the class. * @@ -61,8 +60,10 @@ class UpdateprofileAction extends Action $license = $_POST['omb_listenee_license']; $site_license = common_config('license', 'url'); if (!common_compatible_license($license, $site_license)) { - $this->clientError(sprintf(_('Listenee stream license ‘%1$s’ is not '. - 'compatible with site license ‘%2$s’.'), + // TRANS: Client error displayed when trying to update profile with an incompatible license. + // TRANS: %1$s is the license incompatible with site license %2$s. + $this->clientError(sprintf(_('Listenee stream license "%1$s" is not '. + 'compatible with site license "%2$s".'), $license, $site_license)); return false; } diff --git a/actions/urlsettings.php b/actions/urlsettings.php index 7661086aae..aaaa1b78dd 100644 --- a/actions/urlsettings.php +++ b/actions/urlsettings.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - - /** * Miscellaneous settings actions * @@ -46,7 +44,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ - class UrlsettingsAction extends SettingsAction { /** @@ -54,9 +51,9 @@ class UrlsettingsAction extends SettingsAction * * @return string Title of the page */ - function title() { + // TRANS: Title of URL settings tab in profile settings. return _('URL settings'); } @@ -65,7 +62,6 @@ class UrlsettingsAction extends SettingsAction * * @return instructions for use */ - function getInstructions() { // TRANS: Instructions for tab "Other" in user profile settings. @@ -85,7 +81,6 @@ class UrlsettingsAction extends SettingsAction * * @return void */ - function showContent() { $user = common_current_user(); @@ -118,11 +113,12 @@ class UrlsettingsAction extends SettingsAction // Include default values + // TRANS: Default value for URL shortening settings. $services['none'] = _('[none]'); + // TRANS: Default value for URL shortening settings. $services['internal'] = _('[internal]'); if ($services) { - asort($services); $this->elementStart('li'); @@ -135,16 +131,20 @@ class UrlsettingsAction extends SettingsAction } $this->elementStart('li'); $this->input('maxurllength', + // TRANS: Field label in URL settings in profile. _('URL longer than'), (!is_null($this->arg('maxurllength'))) ? $this->arg('maxurllength') : User_urlshortener_prefs::maxUrlLength($user), + // TRANS: Field title in URL settings in profile. _('URLs longer than this will be shortened, 0 means always shorten.')); $this->elementEnd('li'); $this->elementStart('li'); $this->input('maxnoticelength', + // TRANS: Field label in URL settings in profile. _('Text longer than'), (!is_null($this->arg('maxnoticelength'))) ? $this->arg('maxnoticelength') : User_urlshortener_prefs::maxNoticeLength($user), + // TRANS: Field title in URL settings in profile. _('URLs in notices longer than this will be shortened, 0 means always shorten.')); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -162,7 +162,6 @@ class UrlsettingsAction extends SettingsAction * * @return void */ - function handlePost() { // CSRF protection @@ -184,13 +183,15 @@ class UrlsettingsAction extends SettingsAction $maxurllength = $this->trimmed('maxurllength'); if (!Validate::number($maxurllength, array('min' => 0))) { - throw new ClientException(_('Invalid number for max url length.')); + // TRANS: Client exception thrown when the maximum URL settings value is invalid in profile URL settings. + throw new ClientException(_('Invalid number for maximum URL length.')); } $maxnoticelength = $this->trimmed('maxnoticelength'); if (!Validate::number($maxnoticelength, array('min' => 0))) { - throw new ClientException(_('Invalid number for max notice length.')); + // TRANS: Client exception thrown when the maximum notice length settings value is invalid in profile URL settings. + throw new ClientException(_('Invalid number for maximum notice length.')); } $user = common_current_user(); @@ -235,6 +236,7 @@ class UrlsettingsAction extends SettingsAction } if (!$result) { + // TRANS: Server exception thrown in profile URL settings when preferences could not be saved. throw new ServerException(_('Error saving user URL shortening preferences.')); } diff --git a/actions/userauthorization.php b/actions/userauthorization.php index d9cdc660fd..e58a096bac 100644 --- a/actions/userauthorization.php +++ b/actions/userauthorization.php @@ -111,7 +111,7 @@ class UserauthorizationAction extends Action function showPageNotice() { - // TRANS: Page notice on "Auhtorize subscription" page. + // TRANS: Page notice on "Authorize subscription" page. $this->element('p', null, _('Please check these details to make sure '. 'that you want to subscribe to this ' . 'user’s notices. If you didn’t just ask ' . @@ -243,10 +243,10 @@ class UserauthorizationAction extends Action { // TRANS: Accept message header from Authorise subscription page. common_show_header(_('Subscription authorized')); - // TRANS: Accept message text from Authorise subscription page. $this->element('p', null, + // TRANS: Accept message text from Authorise subscription page. _('The subscription has been authorized, but no '. - 'callback URL was passed. Check with the site’s ' . + 'callback URL was passed. Check with the site\'s ' . 'instructions for details on how to authorize the ' . 'subscription. Your subscription token is:')); $this->element('blockquote', 'token', $tok); @@ -257,10 +257,10 @@ class UserauthorizationAction extends Action { // TRANS: Reject message header from Authorise subscription page. common_show_header(_('Subscription rejected')); - // TRANS: Reject message from Authorise subscription page. $this->element('p', null, + // TRANS: Reject message from Authorise subscription page. _('The subscription has been rejected, but no '. - 'callback URL was passed. Check with the site’s ' . + 'callback URL was passed. Check with the site\'s ' . 'instructions for details on how to fully reject ' . 'the subscription.')); common_show_footer(); diff --git a/actions/userrss.php b/actions/userrss.php index b7078fcaf8..ba9f64f8ac 100644 --- a/actions/userrss.php +++ b/actions/userrss.php @@ -112,7 +112,7 @@ class UserrssAction extends Rss10Action return ($avatar) ? $avatar->url : null; } - # override parent to add X-SUP-ID URL + // override parent to add X-SUP-ID URL function initRss($limit=0) { diff --git a/classes/Avatar.php b/classes/Avatar.php index 6edc817685..34ec4a3caf 100644 --- a/classes/Avatar.php +++ b/classes/Avatar.php @@ -27,7 +27,7 @@ class Avatar extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - # We clean up the file, too + // We clean up the file, too function delete() { diff --git a/classes/Fave.php b/classes/Fave.php index efbceee6a8..e8fdbffc71 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -79,70 +79,16 @@ class Fave extends Memcached_DataObject function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Fave', '_streamDirect'), - array($user_id, $own), - ($own) ? 'fave:ids_by_user_own:'.$user_id : - 'fave:ids_by_user:'.$user_id, - $offset, $limit, $since_id, $max_id); - return $ids; + $stream = new FaveNoticeStream($user_id, $own); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - /** - * Note that the sorting for this is by order of *fave* not order of *notice*. - * - * @fixme add since_id, max_id support? - * - * @param $user_id - * @param $own - * @param $offset - * @param $limit - * @param $since_id - * @param $max_id - * @return - */ - function _streamDirect($user_id, $own, $offset, $limit, $since_id, $max_id) + function idStream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0) { - $fav = new Fave(); - $qry = null; + $stream = new FaveNoticeStream($user_id, $own); - if ($own) { - $qry = 'SELECT fave.* FROM fave '; - $qry .= 'WHERE fave.user_id = ' . $user_id . ' '; - } else { - $qry = 'SELECT fave.* FROM fave '; - $qry .= 'INNER JOIN notice ON fave.notice_id = notice.id '; - $qry .= 'WHERE fave.user_id = ' . $user_id . ' '; - $qry .= 'AND notice.is_local != ' . Notice::GATEWAY . ' '; - } - - if ($since_id != 0) { - $qry .= 'AND notice_id > ' . $since_id . ' '; - } - - if ($max_id != 0) { - $qry .= 'AND notice_id <= ' . $max_id . ' '; - } - - // NOTE: we sort by fave time, not by notice time! - - $qry .= 'ORDER BY modified DESC '; - - if (!is_null($offset)) { - $qry .= "LIMIT $limit OFFSET $offset"; - } - - $fav->query($qry); - - $ids = array(); - - while ($fav->fetch()) { - $ids[] = $fav->notice_id; - } - - $fav->free(); - unset($fav); - - return $ids; + return $stream->getNoticeIds($offset, $limit, $since_id, $max_id); } function asActivity() diff --git a/classes/File.php b/classes/File.php index e9a0131c4e..36ffff585c 100644 --- a/classes/File.php +++ b/classes/File.php @@ -449,52 +449,8 @@ class File extends Memcached_DataObject function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'file:notice-ids:'.$this->url, - $offset, $limit, $since_id, $max_id); - - return Notice::getStreamByIds($ids); - } - - /** - * Stream of notices linking to this URL - * - * @param integer $offset Offset to show; default is 0 - * @param integer $limit Limit of notices to show - * @param integer $since_id Since this notice - * @param integer $max_id Before this notice - * - * @return array ids of notices that link to this file - */ - - function _streamDirect($offset, $limit, $since_id, $max_id) - { - $f2p = new File_to_post(); - - $f2p->selectAdd(); - $f2p->selectAdd('post_id'); - - $f2p->file_id = $this->id; - - Notice::addWhereSinceId($f2p, $since_id, 'post_id', 'modified'); - Notice::addWhereMaxId($f2p, $max_id, 'post_id', 'modified'); - - $f2p->orderBy('modified DESC, post_id DESC'); - - if (!is_null($offset)) { - $f2p->limit($offset, $limit); - } - - $ids = array(); - - if ($f2p->find()) { - while ($f2p->fetch()) { - $ids[] = $f2p->post_id; - } - } - - return $ids; + $stream = new FileNoticeStream($this); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function noticeCount() diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php index 60db51595e..57a10dcedb 100644 --- a/classes/Foreign_link.php +++ b/classes/Foreign_link.php @@ -91,7 +91,7 @@ class Foreign_link extends Memcached_DataObject $this->profilesync = 0; } - # Convenience methods + // Convenience methods function getForeignUser() { $fuser = new Foreign_user(); diff --git a/classes/Foreign_user.php b/classes/Foreign_user.php index 8e6e0b33e0..82ca749a59 100644 --- a/classes/Foreign_user.php +++ b/classes/Foreign_user.php @@ -65,7 +65,7 @@ class Foreign_user extends Memcached_DataObject } } if (count($parts) == 0) { - # No changes + // No changes return true; } $toupdate = implode(', ', $parts); diff --git a/classes/Group_join_queue.php b/classes/Group_join_queue.php index ee47b4932d..7df71ed042 100644 --- a/classes/Group_join_queue.php +++ b/classes/Group_join_queue.php @@ -55,4 +55,80 @@ class Group_join_queue extends Managed_DataObject $rq->insert(); return $rq; } + + function getMember() + { + $member = Profile::staticGet('id', $this->profile_id); + + if (empty($member)) { + // TRANS: Exception thrown providing an invalid profile ID. + // TRANS: %s is the invalid profile ID. + throw new Exception(sprintf(_('Profile ID %s is invalid.'),$this->profile_id)); + } + + return $member; + } + + function getGroup() + { + $group = User_group::staticGet('id', $this->group_id); + + if (empty($group)) { + // TRANS: Exception thrown providing an invalid group ID. + // TRANS: %s is the invalid group ID. + throw new Exception(sprintf(_('Group ID %s is invalid.'),$this->group_id)); + } + + return $group; + } + + /** + * Abort the pending group join... + * + * @param User_group $group + */ + function abort() + { + $profile = $this->getMember(); + $group = $this->getGroup(); + if ($request) { + if (Event::handle('StartCancelJoinGroup', array($profile, $group))) { + $this->delete(); + Event::handle('EndCancelJoinGroup', array($profile, $group)); + } + } + } + + /** + * Complete a pending group join... + * + * @return Group_member object on success + */ + function complete() + { + $join = null; + $profile = $this->getMember(); + $group = $this->getGroup(); + if (Event::handle('StartJoinGroup', array($profile, $group))) { + $join = Group_member::join($group->id, $profile->id); + $this->delete(); + Event::handle('EndJoinGroup', array($profile, $group)); + } + if (!$join) { + throw new Exception('Internal error: group join failed.'); + } + $join->notify(); + return $join; + } + + /** + * Send notifications via email etc to group administrators about + * this exciting new pending moderation queue item! + */ + public function notify() + { + $joiner = Profile::staticGet('id', $this->profile_id); + $group = User_group::staticGet('id', $this->group_id); + mail_notify_group_join_pending($group, $joiner); + } } diff --git a/classes/Group_member.php b/classes/Group_member.php index 30b79bb931..5385e0f487 100644 --- a/classes/Group_member.php +++ b/classes/Group_member.php @@ -162,4 +162,13 @@ class Group_member extends Memcached_DataObject return $act; } + + /** + * Send notifications via email etc to group administrators about + * this exciting new membership! + */ + public function notify() + { + mail_notify_group_join($this->getGroup(), $this->getMember()); + } } diff --git a/classes/Inbox.php b/classes/Inbox.php index f0f626a24e..feaead249b 100644 --- a/classes/Inbox.php +++ b/classes/Inbox.php @@ -233,7 +233,7 @@ class Inbox extends Memcached_DataObject // Do a bulk lookup for the first $limit items // Fast path when nothing's deleted. $firstChunk = array_slice($ids, 0, $offset + $limit); - $notices = Notice::getStreamByIds($firstChunk); + $notices = NoticeStream::getStreamByIds($firstChunk); assert($notices instanceof ArrayWrapper); $items = $notices->_items; @@ -292,7 +292,7 @@ class Inbox extends Memcached_DataObject // Do a bulk lookup for the first $limit items // Fast path when nothing's deleted. $firstChunk = array_slice($ids, 0, $limit); - $notices = Notice::getStreamByIds($firstChunk); + $notices = NoticeStream::getStreamByIds($firstChunk); $wanted = count($firstChunk); // raw entry count in the inbox up to our $limit if ($notices->N >= $wanted) { diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 97f793f4d8..59809dc8f7 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -34,7 +34,7 @@ class Memcached_DataObject extends Safe_DataObject { if (is_null($v)) { $v = $k; - # XXX: HACK! + // XXX: HACK! $i = new $cls; $keys = $i->keys(); $k = $keys[0]; diff --git a/classes/Notice.php b/classes/Notice.php index bd47b8b8c3..3d1639c26d 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -45,7 +45,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep 200 notices, the max number of notices available per API request, * in the memcached cache. */ -define('NOTICE_CACHE_WINDOW', 200); +define('NOTICE_CACHE_WINDOW', CachingNoticeStream::CACHE_WINDOW); define('MAX_BOXCARS', 128); @@ -73,6 +73,7 @@ class Notice extends Memcached_DataObject public $location_ns; // int(4) public $repeat_of; // int(4) public $object_type; // varchar(255) + public $scope; // int(4) /* Static get */ function staticGet($k,$v=NULL) @@ -89,6 +90,12 @@ class Notice extends Memcached_DataObject const LOCAL_NONPUBLIC = -1; const GATEWAY = -2; + const PUBLIC_SCOPE = 0; // Useful fake constant + const SITE_SCOPE = 1; + const ADDRESSEE_SCOPE = 2; + const GROUP_SCOPE = 4; + const FOLLOWER_SCOPE = 8; + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); @@ -243,6 +250,7 @@ class Notice extends Memcached_DataObject * notice in place of extracting links from content * boolean 'distribute' whether to distribute the notice, default true * string 'object_type' URL of the associated object type (default ActivityObject::NOTE) + * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise * * @fixme tag override * @@ -254,6 +262,7 @@ class Notice extends Memcached_DataObject 'url' => null, 'reply_to' => null, 'repeat_of' => null, + 'scope' => null, 'distribute' => true); if (!empty($options)) { @@ -312,7 +321,7 @@ class Notice extends Memcached_DataObject $autosource = common_config('public', 'autosource'); - # Sandboxed are non-false, but not 1, either + // Sandboxed are non-false, but not 1, either if (!$profile->hasRight(Right::PUBLICNOTICE) || ($source && $autosource && in_array($source, $autosource))) { @@ -336,6 +345,37 @@ class Notice extends Memcached_DataObject // Handle repeat case if (isset($repeat_of)) { + + // Check for a private one + + $repeat = Notice::staticGet('id', $repeat_of); + + if (empty($repeat)) { + throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); + } + + if ($profile->id == $repeat->profile_id) { + // TRANS: Client error displayed when trying to repeat an own notice. + throw new ClientException(_('You cannot repeat your own notice.')); + } + + if ($repeat->scope != Notice::SITE_SCOPE && + $repeat->scope != Notice::PUBLIC_SCOPE) { + // TRANS: Client error displayed when trying to repeat a non-public notice. + throw new ClientException(_('Cannot repeat a private notice.'), 403); + } + + if (!$repeat->inScope($profile)) { + // The generic checks above should cover this, but let's be sure! + // TRANS: Client error displayed when trying to repeat a notice you cannot access. + throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); + } + + if ($profile->hasRepeated($repeat->id)) { + // TRANS: Client error displayed when trying to repeat an already repeated notice. + throw new ClientException(_('You already repeated that notice.')); + } + $notice->repeat_of = $repeat_of; } else { $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final); @@ -343,6 +383,12 @@ class Notice extends Memcached_DataObject if (!empty($notice->reply_to)) { $reply = Notice::staticGet('id', $notice->reply_to); + if (!$reply->inScope($profile)) { + // TRANS: Client error displayed when trying to reply to a notice a the target has no access to. + // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). + throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'), + $profile->nickname, $reply->id), 403); + } $notice->conversation = $reply->conversation; } @@ -368,6 +414,12 @@ class Notice extends Memcached_DataObject $notice->object_type = $object_type; } + if (is_null($scope)) { // 0 is a valid value + $notice->scope = common_config('notice', 'defaultscope'); + } else { + $notice->scope = $scope; + } + if (Event::handle('StartNoticeSave', array(&$notice))) { // XXX: some of these functions write to the DB @@ -410,8 +462,8 @@ class Notice extends Memcached_DataObject } - # Clear the cache for subscribed users, so they'll update at next request - # XXX: someone clever could prepend instead of clearing the cache + // Clear the cache for subscribed users, so they'll update at next request + // XXX: someone clever could prepend instead of clearing the cache $notice->blowOnInsert(); @@ -554,7 +606,7 @@ class Notice extends Memcached_DataObject if (empty($profile)) { return false; } - $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW); + $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW); if (!empty($notice)) { $last = 0; while ($notice->fetch()) { @@ -565,8 +617,8 @@ class Notice extends Memcached_DataObject } } } - # If we get here, oldest item in cache window is not - # old enough for dupe limit; do direct check against DB + // If we get here, oldest item in cache window is not + // old enough for dupe limit; do direct check against DB $notice = new Notice(); $notice->profile_id = $profile_id; $notice->content = $content; @@ -582,16 +634,16 @@ class Notice extends Memcached_DataObject if (empty($profile)) { return false; } - # Get the Nth notice + // Get the Nth notice $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1); if ($notice && $notice->fetch()) { - # If the Nth notice was posted less than timespan seconds ago + // If the Nth notice was posted less than timespan seconds ago if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) { - # Then we throttle + // Then we throttle return false; } } - # Either not N notices in the stream, OR the Nth was not posted within timespan seconds + // Either not N notices in the stream, OR the Nth was not posted within timespan seconds return true; } @@ -635,134 +687,19 @@ class Notice extends Memcached_DataObject return $att; } - function getStreamByIds($ids) - { - $cache = Cache::instance(); - - if (!empty($cache)) { - $notices = array(); - foreach ($ids as $id) { - $n = Notice::staticGet('id', $id); - if (!empty($n)) { - $notices[] = $n; - } - } - return new ArrayWrapper($notices); - } else { - $notice = new Notice(); - if (empty($ids)) { - //if no IDs requested, just return the notice object - return $notice; - } - $notice->whereAdd('id in (' . implode(', ', $ids) . ')'); - - $notice->find(); - - $temp = array(); - - while ($notice->fetch()) { - $temp[$notice->id] = clone($notice); - } - - $wrapped = array(); - - foreach ($ids as $id) { - if (array_key_exists($id, $temp)) { - $wrapped[] = $temp[$id]; - } - } - - return new ArrayWrapper($wrapped); - } - } function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Notice', '_publicStreamDirect'), - array(), - 'public', - $offset, $limit, $since_id, $max_id); - return Notice::getStreamByIds($ids); + $stream = new PublicNoticeStream(); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - if (common_config('public', 'localonly')) { - $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC); - } else { - # -1 == blacklisted, -2 == gateway (i.e. Twitter) - $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC); - $notice->whereAdd('is_local !='. Notice::GATEWAY); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; - } function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Notice', '_conversationStreamDirect'), - array($id), - 'notice:conversation_ids:'.$id, - $offset, $limit, $since_id, $max_id); + $stream = new ConversationNoticeStream($id); - return Notice::getStreamByIds($ids); - } - - function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->conversation = $id; - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; + return $stream->getNotices($offset, $limit, $since_id, $max_id); } /** @@ -1655,61 +1592,6 @@ class Notice extends Memcached_DataObject } } - function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0) - { - $cache = Cache::instance(); - - if (empty($cache) || - $since_id != 0 || $max_id != 0 || - is_null($limit) || - ($offset + $limit) > NOTICE_CACHE_WINDOW) { - return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id, - $max_id))); - } - - $idkey = Cache::key($cachekey); - - $idstr = $cache->get($idkey); - - if ($idstr !== false) { - // Cache hit! Woohoo! - $window = explode(',', $idstr); - $ids = array_slice($window, $offset, $limit); - return $ids; - } - - $laststr = $cache->get($idkey.';last'); - - if ($laststr !== false) { - $window = explode(',', $laststr); - $last_id = $window[0]; - $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - $last_id, 0, null))); - - $new_window = array_merge($new_ids, $window); - - $new_windowstr = implode(',', $new_window); - - $result = $cache->set($idkey, $new_windowstr); - $result = $cache->set($idkey . ';last', $new_windowstr); - - $ids = array_slice($new_window, $offset, $limit); - - return $ids; - } - - $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW, - 0, 0, null))); - - $windowstr = implode(',', $window); - - $result = $cache->set($idkey, $windowstr); - $result = $cache->set($idkey . ';last', $windowstr); - - $ids = array_slice($window, $offset, $limit); - - return $ids; - } /** * Determine which notice, if any, a new notice is in reply to. @@ -1820,6 +1702,15 @@ class Notice extends Memcached_DataObject return $location; } + /** + * Convenience function for posting a repeat of an existing message. + * + * @param int $repeater_id: profile ID of user doing the repeat + * @param string $source: posting source key, eg 'web', 'api', etc + * @return Notice + * + * @throws Exception on failure or permission problems + */ function repeat($repeater_id, $source) { $author = Profile::staticGet('id', $this->profile_id); @@ -1841,8 +1732,13 @@ class Notice extends Memcached_DataObject $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; } - return self::saveNew($repeater_id, $content, $source, - array('repeat_of' => $this->id)); + // Scope is same as this one's + + return self::saveNew($repeater_id, + $content, + $source, + array('repeat_of' => $this->id, + 'scope' => $this->scope)); } // These are supposed to be in chron order! @@ -1867,7 +1763,7 @@ class Notice extends Memcached_DataObject } } - return Notice::getStreamByIds($ids); + return NoticeStream::getStreamByIds($ids); } function _repeatStreamDirect($limit) @@ -2296,4 +2192,94 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + /** + * Check that the given profile is allowed to read, respond to, or otherwise + * act on this notice. + * + * The $scope member is a bitmask of scopes, representing a logical AND of the + * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means + * "only visible to people who are mentioned in the notice AND are users on this site." + * Users on the site who are not mentioned in the notice will not be able to see the + * notice. + * + * @param Profile $profile The profile to check; pass null to check for public/unauthenticated users. + * + * @return boolean whether the profile is in the notice's scope + */ + function inScope($profile) + { + // If there's no scope, anyone (even anon) is in scope. + + if ($this->scope == 0) { + return true; + } + + // If there's scope, anon cannot be in scope + + if (empty($profile)) { + return false; + } + + // Author is always in scope + + if ($this->profile_id == $profile->id) { + return true; + } + + // Only for users on this site + + if ($this->scope & Notice::SITE_SCOPE) { + $user = $profile->getUser(); + if (empty($user)) { + return false; + } + } + + // Only for users mentioned in the notice + + if ($this->scope & Notice::ADDRESSEE_SCOPE) { + + // XXX: just query for the single reply + + $replies = $this->getReplies(); + + if (!in_array($profile->id, $replies)) { + return false; + } + } + + // Only for members of the given group + + if ($this->scope & Notice::GROUP_SCOPE) { + + // XXX: just query for the single membership + + $groups = $this->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + + // Only for followers of the author + + if ($this->scope & Notice::FOLLOWER_SCOPE) { + $author = $this->getProfile(); + if (!Subscription::exists($profile, $author)) { + return false; + } + } + + return true; + } } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index 81d346c5d3..809403a9bd 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -36,43 +36,11 @@ class Notice_tag extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function getStream($tag, $offset=0, $limit=20) { - - $ids = Notice::stream(array('Notice_tag', '_streamDirect'), - array($tag), - 'notice_tag:notice_ids:' . Cache::keyize($tag), - $offset, $limit); - - return Notice::getStreamByIds($ids); - } - - function _streamDirect($tag, $offset, $limit, $since_id, $max_id) + static function getStream($tag, $offset=0, $limit=20, $sinceId=0, $maxId=0) { - $nt = new Notice_tag(); - - $nt->tag = $tag; - - $nt->selectAdd(); - $nt->selectAdd('notice_id'); - - Notice::addWhereSinceId($nt, $since_id, 'notice_id'); - Notice::addWhereMaxId($nt, $max_id, 'notice_id'); - - $nt->orderBy('created DESC, notice_id DESC'); - - if (!is_null($offset)) { - $nt->limit($offset, $limit); - } - - $ids = array(); - - if ($nt->find()) { - while ($nt->fetch()) { - $ids[] = $nt->notice_id; - } - } - - return $ids; + $stream = new TagNoticeStream($tag); + + return $stream->getNotices($offset, $limit, $sinceId, $maxId); } function blowCache($blowLast=false) diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php index e1b4b8c046..834e38d2be 100644 --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -51,7 +51,7 @@ class Oauth_application_user extends Memcached_DataObject } } if (count($parts) == 0) { - # No changes + // No changes return true; } $toupdate = implode(', ', $parts); diff --git a/classes/Profile.php b/classes/Profile.php index f2f8fc123e..7ca0d0d338 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -93,7 +93,7 @@ class Profile extends Memcached_DataObject $avatar->url = Avatar::url($filename); $avatar->created = DB_DataObject_Cast::dateTime(); # current time - # XXX: start a transaction here + // XXX: start a transaction here if (!$this->delete_avatars() || !$avatar->insert()) { @unlink(Avatar::path($filename)); @@ -101,7 +101,7 @@ class Profile extends Memcached_DataObject } foreach (array(AVATAR_PROFILE_SIZE, AVATAR_STREAM_SIZE, AVATAR_MINI_SIZE) as $size) { - # We don't do a scaled one if original is our scaled size + // We don't do a scaled one if original is our scaled size if (!($avatar->width == $size && $avatar->height == $size)) { $scaled_filename = $imagefile->resize($size); @@ -168,7 +168,7 @@ class Profile extends Memcached_DataObject function getFancyName() { if ($this->fullname) { - // TRANS: Full name of a profile or group followed by nickname in parens + // TRANS: Full name of a profile or group (%1$s) followed by nickname (%2$s) in parentheses. return sprintf(_m('FANCYNAME','%1$s (%2$s)'), $this->fullname, $this->nickname); } else { return $this->nickname; @@ -180,7 +180,6 @@ class Profile extends Memcached_DataObject * * @return mixed Notice or null */ - function getCurrentNotice() { $notice = $this->getNotices(0, 1); @@ -198,90 +197,16 @@ class Profile extends Memcached_DataObject function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array($this, '_streamTaggedDirect'), - array($tag), - 'profile:notice_ids_tagged:' . $this->id . ':' . $tag, - $offset, $limit, $since_id, $max_id); - return Notice::getStreamByIds($ids); + $stream = new TaggedProfileNoticeStream($this, $tag); + + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - // XXX: I'm not sure this is going to be any faster. It probably isn't. - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'profile:notice_ids:' . $this->id, - $offset, $limit, $since_id, $max_id); + $stream = new ProfileNoticeStream($this); - return Notice::getStreamByIds($ids); - } - - function _streamTaggedDirect($tag, $offset, $limit, $since_id, $max_id) - { - // XXX It would be nice to do this without a join - // (necessary to do it efficiently on accounts with long history) - - $notice = new Notice(); - - $query = - "select id from notice join notice_tag on id=notice_id where tag='". - $notice->escape($tag) . - "' and profile_id=" . intval($this->id); - - $since = Notice::whereSinceId($since_id, 'id', 'notice.created'); - if ($since) { - $query .= " and ($since)"; - } - - $max = Notice::whereMaxId($max_id, 'id', 'notice.created'); - if ($max) { - $query .= " and ($max)"; - } - - $query .= ' order by notice.created DESC, id DESC'; - - if (!is_null($offset)) { - $query .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset); - } - - $notice->query($query); - - $ids = array(); - - while ($notice->fetch()) { - $ids[] = $notice->id; - } - - return $ids; - } - - function _streamDirect($offset, $limit, $since_id, $max_id) - { - $notice = new Notice(); - - $notice->profile_id = $this->id; - - $notice->selectAdd(); - $notice->selectAdd('id'); - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - $notice->find(); - - $ids = array(); - - while ($notice->fetch()) { - $ids[] = $notice->id; - } - - return $ids; + return $stream->getNotices($offset, $limit, $since_id, $max_id); } function isMember($group) @@ -532,61 +457,26 @@ class Profile extends Memcached_DataObject */ function joinGroup(User_group $group) { - $ok = null; + $join = null; if ($group->join_policy == User_group::JOIN_POLICY_MODERATE) { - $ok = Group_join_queue::saveNew($this, $group); + $join = Group_join_queue::saveNew($this, $group); } else { if (Event::handle('StartJoinGroup', array($group, $this))) { - $ok = Group_member::join($group->id, $this->id); + $join = Group_member::join($group->id, $this->id); Event::handle('EndJoinGroup', array($group, $this)); } } - return $ok; - } - - /** - * Cancel a pending group join... - * - * @param User_group $group - */ - function cancelJoinGroup(User_group $group) - { - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartCancelJoinGroup', array($group, $this))) { - $request->delete(); - Event::handle('EndCancelJoinGroup', array($group, $this)); - } + if ($join) { + // Send any applicable notifications... + $join->notify(); } - } - - /** - * Complete a pending group join on our end... - * - * @param User_group $group - */ - function completeJoinGroup(User_group $group) - { - $ok = null; - $request = Group_join_queue::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - if ($request) { - if (Event::handle('StartJoinGroup', array($group, $this))) { - $ok = Group_member::join($group->id, $this->id); - $request->delete(); - Event::handle('EndJoinGroup', array($group, $this)); - } - } else { - throw new Exception(_m('Invalid group join approval: not pending.')); - } - return $ok; + return $join; } /** * Leave a group that this profile is a member of. * - * @param User_group $group + * @param User_group $group */ function leaveGroup(User_group $group) { @@ -642,7 +532,6 @@ class Profile extends Memcached_DataObject return new ArrayWrapper($profiles); } - function getTaggedSubscribers($tag) { $qry = @@ -668,6 +557,36 @@ class Profile extends Memcached_DataObject return $tagged; } + /** + * Get pending subscribers, who have not yet been approved. + * + * @param int $offset + * @param int $limit + * @return Profile + */ + function getRequests($offset=0, $limit=null) + { + $qry = + 'SELECT profile.* ' . + 'FROM profile JOIN subscription_queue '. + 'ON profile.id = subscription_queue.subscriber ' . + 'WHERE subscription_queue.subscribed = %d ' . + 'ORDER BY subscription_queue.created DESC '; + + if ($limit != null) { + if (common_config('db','type') == 'pgsql') { + $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $qry .= ' LIMIT ' . $offset . ', ' . $limit; + } + } + + $members = new Profile(); + + $members->query(sprintf($qry, $this->id)); + return $members; + } + function subscriptionCount() { $c = Cache::instance(); @@ -726,6 +645,17 @@ class Profile extends Memcached_DataObject return Subscription::exists($this, $other); } + /** + * Check if a pending subscription request is outstanding for this... + * + * @param Profile $other + * @return boolean + */ + function hasPendingSubscription($other) + { + return Subscription_queue::exists($this, $other); + } + /** * Are these two profiles subscribed to each other? * @@ -748,7 +678,7 @@ class Profile extends Memcached_DataObject // This is the stream of favorite notices, in rev chron // order. This forces it into cache. - $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW); + $ids = Fave::idStream($this->id, 0, CachingNoticeStream::CACHE_WINDOW); // If it's in the list, then it's a fave @@ -760,7 +690,7 @@ class Profile extends Memcached_DataObject // then the cache has all available faves, so this one // is not a fave. - if (count($ids) < NOTICE_CACHE_WINDOW) { + if (count($ids) < CachingNoticeStream::CACHE_WINDOW) { return false; } @@ -1359,4 +1289,44 @@ class Profile extends Memcached_DataObject return $profile; } + + function canRead(Notice $notice) + { + if ($notice->scope & Notice::SITE_SCOPE) { + $user = $this->getUser(); + if (empty($user)) { + return false; + } + } + + if ($notice->scope & Notice::ADDRESSEE_SCOPE) { + $replies = $notice->getReplies(); + + if (!in_array($this->id, $replies)) { + $groups = $notice->getGroups(); + + $foundOne = false; + + foreach ($groups as $group) { + if ($this->isMember($group)) { + $foundOne = true; + break; + } + } + + if (!$foundOne) { + return false; + } + } + } + + if ($notice->scope & Notice::FOLLOWER_SCOPE) { + $author = $notice->getProfile(); + if (!Subscription::exists($this, $author)) { + return false; + } + } + + return true; + } } diff --git a/classes/Profile_tag.php b/classes/Profile_tag.php index 183f79145a..eb583c98fe 100644 --- a/classes/Profile_tag.php +++ b/classes/Profile_tag.php @@ -106,11 +106,11 @@ class Profile_tag extends Memcached_DataObject $ptag = new Profile_tag(); - # Delete stuff that's in old and not in new + // Delete stuff that's in old and not in new $to_delete = array_diff($oldtags, $newtags); - # Insert stuff that's in new and not in old + // Insert stuff that's in new and not in old $to_insert = array_diff($newtags, $oldtags); @@ -271,4 +271,19 @@ class Profile_tag extends Memcached_DataObject } return true; } + + // Return profiles with a given tag + static function getTagged($tagger, $tag) { + $profile = new Profile(); + $profile->query('SELECT profile.* ' . + 'FROM profile JOIN profile_tag ' . + 'ON profile.id = profile_tag.tagged ' . + 'WHERE profile_tag.tagger = ' . $tagger . ' ' . + 'AND profile_tag.tag = "' . $tag . '" '); + $tagged = array(); + while ($profile->fetch()) { + $tagged[] = clone($profile); + } + return true; + } } diff --git a/classes/Queue_item.php b/classes/Queue_item.php index 007d4ed232..d17e512b96 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -46,9 +46,9 @@ class Queue_item extends Memcached_DataObject $cnt = $qi->find(true); if ($cnt) { - # XXX: potential race condition - # can we force it to only update if claimed is still null - # (or old)? + // XXX: potential race condition + // can we force it to only update if claimed is still null + // (or old)? common_log(LOG_INFO, 'claiming queue item id = ' . $qi->id . ' for transport ' . $qi->transport); $orig = clone($qi); diff --git a/classes/Reply.php b/classes/Reply.php index 371c16cf48..9ba623ba3f 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -38,35 +38,8 @@ class Reply extends Memcached_DataObject function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Notice::stream(array('Reply', '_streamDirect'), - array($user_id), - 'reply:stream:' . $user_id, - $offset, $limit, $since_id, $max_id); - return $ids; - } + $stream = new ReplyNoticeStream($user_id); - function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) - { - $reply = new Reply(); - $reply->profile_id = $user_id; - - Notice::addWhereSinceId($reply, $since_id, 'notice_id', 'modified'); - Notice::addWhereMaxId($reply, $max_id, 'notice_id', 'modified'); - - $reply->orderBy('modified DESC, notice_id DESC'); - - if (!is_null($offset)) { - $reply->limit($offset, $limit); - } - - $ids = array(); - - if ($reply->find()) { - while ($reply->fetch()) { - $ids[] = $reply->notice_id; - } - } - - return $ids; + return $stream->getNotices($offset, $limit, $since_id, $max_id); } } diff --git a/classes/Subscription.php b/classes/Subscription.php index 1d4f37929b..8af414b3a7 100644 --- a/classes/Subscription.php +++ b/classes/Subscription.php @@ -27,6 +27,7 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; class Subscription extends Memcached_DataObject { const CACHE_WINDOW = 201; + const FORCE = true; ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -58,11 +59,12 @@ class Subscription extends Memcached_DataObject * * @param Profile $subscriber party to receive new notices * @param Profile $other party sending notices; publisher + * @param bool $force pass Subscription::FORCE to override local subscription approval * - * @return Subscription new subscription + * @return mixed Subscription or Subscription_queue: new subscription info */ - static function start($subscriber, $other) + static function start($subscriber, $other, $force=false) { // @fixme should we enforce this as profiles in callers instead? if ($subscriber instanceof User) { @@ -88,35 +90,39 @@ class Subscription extends Memcached_DataObject } if (Event::handle('StartSubscribe', array($subscriber, $other))) { - $sub = self::saveNew($subscriber->id, $other->id); - $sub->notify(); - - self::blow('user:notices_with_friends:%d', $subscriber->id); - - self::blow('subscription:by-subscriber:'.$subscriber->id); - self::blow('subscription:by-subscribed:'.$other->id); - - $subscriber->blowSubscriptionCount(); - $other->blowSubscriberCount(); - $otherUser = User::staticGet('id', $other->id); + if ($otherUser && $otherUser->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE && !$force) { + $sub = Subscription_queue::saveNew($subscriber, $other); + $sub->notify(); + } else { + $sub = self::saveNew($subscriber->id, $other->id); + $sub->notify(); - if (!empty($otherUser) && - $otherUser->autosubscribe && - !self::exists($other, $subscriber) && - !$subscriber->hasBlocked($other)) { + self::blow('user:notices_with_friends:%d', $subscriber->id); - try { - self::start($other, $subscriber); - } catch (Exception $e) { - common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + self::blow('subscription:by-subscriber:'.$subscriber->id); + self::blow('subscription:by-subscribed:'.$other->id); + + $subscriber->blowSubscriptionCount(); + $other->blowSubscriberCount(); + + if (!empty($otherUser) && + $otherUser->autosubscribe && + !self::exists($other, $subscriber) && + !$subscriber->hasBlocked($other)) { + + try { + self::start($other, $subscriber); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}"); + } } } Event::handle('EndSubscribe', array($subscriber, $other)); } - return true; + return $sub; } /** @@ -146,9 +152,9 @@ class Subscription extends Memcached_DataObject function notify() { - # XXX: add other notifications (Jabber, SMS) here - # XXX: queue this and handle it offline - # XXX: Whatever happens, do it in Twitter-like API, too + // XXX: add other notifications (Jabber, SMS) here + // XXX: queue this and handle it offline + // XXX: Whatever happens, do it in Twitter-like API, too $this->notifyEmail(); } @@ -261,8 +267,8 @@ class Subscription extends Memcached_DataObject common_date_iso8601($this->created)); $act->time = strtotime($this->created); - // TRANS: Activity tile when subscribing to another person. - $act->title = _("Follow"); + // TRANS: Activity title when subscribing to another person. + $act->title = _m('TITLE','Follow'); // TRANS: Notification given when one person starts following another. // TRANS: %1$s is the subscriber, %2$s is the subscribed. $act->content = sprintf(_('%1$s is now following %2$s.'), @@ -295,7 +301,6 @@ class Subscription extends Memcached_DataObject * * @return Subscription stream of subscriptions; use fetch() to iterate */ - static function bySubscriber($subscriberId, $offset = 0, $limit = PROFILES_PER_PAGE) @@ -356,7 +361,6 @@ class Subscription extends Memcached_DataObject * * @return Subscription stream of subscriptions; use fetch() to iterate */ - static function bySubscribed($subscribedId, $offset = 0, $limit = PROFILES_PER_PAGE) @@ -414,7 +418,6 @@ class Subscription extends Memcached_DataObject * * @return boolean success flag. */ - function update($orig=null) { $result = parent::update($orig); diff --git a/classes/Subscription_queue.php b/classes/Subscription_queue.php new file mode 100644 index 0000000000..19cd71c6a8 --- /dev/null +++ b/classes/Subscription_queue.php @@ -0,0 +1,105 @@ + 'Holder for subscription requests awaiting moderation.', + 'fields' => array( + 'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile making the request'), + 'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'remote or local profile being subscribed to'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + ), + 'primary key' => array('subscriber', 'subscribed'), + 'indexes' => array( + 'subscription_queue_subscriber_created_idx' => array('subscriber', 'created'), + 'subscription_queue_subscribed_created_idx' => array('subscribed', 'created'), + ), + 'foreign keys' => array( + 'subscription_queue_subscriber_fkey' => array('profile', array('subscriber' => 'id')), + 'subscription_queue_subscribed_fkey' => array('profile', array('subscribed' => 'id')), + ) + ); + } + + public static function saveNew(Profile $subscriber, Profile $subscribed) + { + $rq = new Subscription_queue(); + $rq->subscriber = $subscriber->id; + $rq->subscribed = $subscribed->id; + $rq->created = common_sql_now(); + $rq->insert(); + return $rq; + } + + function exists($subscriber, $other) + { + $sub = Subscription_queue::pkeyGet(array('subscriber' => $subscriber->id, + 'subscribed' => $other->id)); + return (empty($sub)) ? false : true; + } + + /** + * Complete a pending subscription, as we've got approval of some sort. + * + * @return Subscription + */ + public function complete() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + $sub = Subscription::start($subscriber, $subscribed, Subscription::FORCE); + if ($sub) { + $this->delete(); + } + return $sub; + } + + /** + * Cancel an outstanding subscription request to the other profile. + */ + public function abort() + { + $subscriber = Profile::staticGet('id', $this->subscriber); + $subscribed = Profile::staticGet('id', $this->subscribed); + if (Event::handle('StartCancelSubscription', array($subscriber, $subscribed))) { + $this->delete(); + Event::handle('EndCancelSubscription', array($subscriber, $subscribed)); + } + } + + /** + * Send notifications via email etc to group administrators about + * this exciting new pending moderation queue item! + */ + public function notify() + { + $other = Profile::staticGet('id', $this->subscriber); + $listenee = User::staticGet('id', $this->subscribed); + mail_subscribe_pending_notify_profile($listenee, $other); + } +} diff --git a/classes/User.php b/classes/User.php index 328e5be3e1..93afc1e612 100644 --- a/classes/User.php +++ b/classes/User.php @@ -30,6 +30,9 @@ require_once 'Validate.php'; class User extends Memcached_DataObject { + const SUBSCRIBE_POLICY_OPEN = 0; + const SUBSCRIBE_POLICY_MODERATE = 1; + ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ @@ -55,6 +58,7 @@ class User extends Memcached_DataObject public $smsemail; // varchar(255) public $uri; // varchar(255) unique_key public $autosubscribe; // tinyint(1) + public $subscribe_policy; // tinyint(1) public $urlshorteningservice; // varchar(50) default_ur1.ca public $inboxed; // tinyint(1) public $design_id; // int(4) @@ -86,6 +90,12 @@ class User extends Memcached_DataObject return $profile->isSubscribed($other); } + function hasPendingSubscription($other) + { + $profile = $this->getProfile(); + return $profile->hasPendingSubscription($other); + } + // 'update' won't write key columns, so we have to do it ourselves. function updateKeys(&$orig) @@ -448,8 +458,7 @@ class User extends Memcached_DataObject function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { - $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id); - return Notice::getStreamByIds($ids); + return Reply::stream($this->id, $offset, $limit, $since_id, $before_id); } function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) { @@ -465,8 +474,7 @@ class User extends Memcached_DataObject function favoriteNotices($own=false, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $ids = Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id); - return Notice::getStreamByIds($ids); + return Fave::stream($this->id, $offset, $limit, $own, $since_id, $max_id); } function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) @@ -769,95 +777,18 @@ class User extends Memcached_DataObject function repeatedByMe($offset=0, $limit=20, $since_id=null, $max_id=null) { - $ids = Notice::stream(array($this, '_repeatedByMeDirect'), - array(), - 'user:repeated_by_me:'.$this->id, - $offset, $limit, $since_id, $max_id, null); - - return Notice::getStreamByIds($ids); + $stream = new RepeatedByMeNoticeStream($this); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - function _repeatedByMeDirect($offset, $limit, $since_id, $max_id) - { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->profile_id = $this->id; - $notice->whereAdd('repeat_of IS NOT NULL'); - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; - } function repeatsOfMe($offset=0, $limit=20, $since_id=null, $max_id=null) { - $ids = Notice::stream(array($this, '_repeatsOfMeDirect'), - array(), - 'user:repeats_of_me:'.$this->id, - $offset, $limit, $since_id, $max_id); + $stream = new RepeatsOfMeNoticeStream($this); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - function _repeatsOfMeDirect($offset, $limit, $since_id, $max_id) - { - $qry = - 'SELECT DISTINCT original.id AS id ' . - 'FROM notice original JOIN notice rept ON original.id = rept.repeat_of ' . - 'WHERE original.profile_id = ' . $this->id . ' '; - - $since = Notice::whereSinceId($since_id, 'original.id', 'original.created'); - if ($since) { - $qry .= "AND ($since) "; - } - - $max = Notice::whereMaxId($max_id, 'original.id', 'original.created'); - if ($max) { - $qry .= "AND ($max) "; - } - - $qry .= 'ORDER BY original.created, original.id DESC '; - - if (!is_null($offset)) { - $qry .= "LIMIT $limit OFFSET $offset"; - } - - $ids = array(); - - $notice = new Notice(); - - $notice->query($qry); - - while ($notice->fetch()) { - $ids[] = $notice->id; - } - - $notice->free(); - $notice = NULL; - - return $ids; - } function repeatedToMe($offset=0, $limit=20, $since_id=null, $max_id=null) { @@ -1034,5 +965,4 @@ class User extends Memcached_DataObject return $apps; } - } diff --git a/classes/User_group.php b/classes/User_group.php index c044594f1f..8587f15771 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -87,42 +87,11 @@ class User_group extends Memcached_DataObject function getNotices($offset, $limit, $since_id=null, $max_id=null) { - $ids = Notice::stream(array($this, '_streamDirect'), - array(), - 'user_group:notice_ids:' . $this->id, - $offset, $limit, $since_id, $max_id); + $stream = new GroupNoticeStream($this); - return Notice::getStreamByIds($ids); + return $stream->getNotices($offset, $limit, $since_id, $max_id); } - function _streamDirect($offset, $limit, $since_id, $max_id) - { - $inbox = new Group_inbox(); - - $inbox->group_id = $this->id; - - $inbox->selectAdd(); - $inbox->selectAdd('notice_id'); - - Notice::addWhereSinceId($inbox, $since_id, 'notice_id'); - Notice::addWhereMaxId($inbox, $max_id, 'notice_id'); - - $inbox->orderBy('created DESC, notice_id DESC'); - - if (!is_null($offset)) { - $inbox->limit($offset, $limit); - } - - $ids = array(); - - if ($inbox->find()) { - while ($inbox->fetch()) { - $ids[] = $inbox->notice_id; - } - } - - return $ids; - } function allowedNickname($nickname) { @@ -306,11 +275,11 @@ class User_group extends Memcached_DataObject $oldaliases = $this->getAliases(); - # Delete stuff that's old that not in new + // Delete stuff that's old that not in new $to_delete = array_diff($oldaliases, $newaliases); - # Insert stuff that's in new and not in old + // Insert stuff that's in new and not in old $to_insert = array_diff($newaliases, $oldaliases); diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 376079946a..63061527af 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -337,6 +337,7 @@ location_id = 1 location_ns = 1 repeat_of = 1 object_type = 2 +scope = 1 [notice__keys] id = N @@ -627,6 +628,7 @@ smsreplies = 17 smsemail = 2 uri = 2 autosubscribe = 17 +subscribe_policy = 17 urlshorteningservice = 2 inboxed = 17 design_id = 1 diff --git a/db/core.php b/db/core.php index fa6b496fbf..ed3be4341a 100644 --- a/db/core.php +++ b/db/core.php @@ -118,6 +118,7 @@ $schema['user'] = array( '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'), + 'subscribe_policy' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'), 'urlshorteningservice' => array('type' => 'varchar', 'length' => 50, 'default' => 'internal', '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'), @@ -202,6 +203,9 @@ $schema['notice'] = array( 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), 'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + 'scope' => array('type' => 'int', + 'default' => '1', + 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers'), ), 'primary key' => array('id'), 'unique keys' => array( @@ -1099,3 +1103,5 @@ $schema['schema_version'] = array( ); $schema['group_join_queue'] = Group_join_queue::schemaDef(); + +$schema['subscription_queue'] = Subscription_queue::schemaDef(); diff --git a/extlib/Console/Getopt.php b/extlib/Console/Getopt.php index bb9d69ca22..d8abfc491d 100644 --- a/extlib/Console/Getopt.php +++ b/extlib/Console/Getopt.php @@ -1,32 +1,40 @@ | -// +----------------------------------------------------------------------+ -// -// $Id: Getopt.php,v 1.4 2007/06/12 14:58:56 cellog Exp $ +/** + * PHP Version 5 + * + * Copyright (c) 1997-2004 The PHP Group + * + * This source file is subject to version 3.0 of the PHP license, + * that is bundled with this package in the file LICENSE, and is + * available through the world-wide-web at the following url: + * http://www.php.net/license/3_0.txt. + * If you did not receive a copy of the PHP license and are unable to + * obtain it through the world-wide-web, please send a note to + * license@php.net so we can mail you a copy immediately. + * + * @category Console + * @package Console_Getopt + * @author Andrei Zmievski + * @license http://www.php.net/license/3_0.txt PHP 3.0 + * @version CVS: $Id: Getopt.php 306067 2010-12-08 00:13:31Z dufuz $ + * @link http://pear.php.net/package/Console_Getopt + */ require_once 'PEAR.php'; /** * Command-line options parsing class. * - * @author Andrei Zmievski - * + * @category Console + * @package Console_Getopt + * @author Andrei Zmievski + * @license http://www.php.net/license/3_0.txt PHP 3.0 + * @link http://pear.php.net/package/Console_Getopt */ -class Console_Getopt { +class Console_Getopt +{ + /** * Parses the command-line options. * @@ -53,45 +61,60 @@ class Console_Getopt { * * Most of the semantics of this function are based on GNU getopt_long(). * - * @param array $args an array of command-line arguments - * @param string $short_options specifies the list of allowed short options - * @param array $long_options specifies the list of allowed long options + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option * * @return array two-element array containing the list of parsed options and * the non-option arguments - * * @access public - * */ - function getopt2($args, $short_options, $long_options = null) + function getopt2($args, $short_options, $long_options = null, $skip_unknown = false) { - return Console_Getopt::doGetopt(2, $args, $short_options, $long_options); + return Console_Getopt::doGetopt(2, $args, $short_options, $long_options, $skip_unknown); } /** * This function expects $args to start with the script name (POSIX-style). * Preserved for backwards compatibility. + * + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * * @see getopt2() - */ - function getopt($args, $short_options, $long_options = null) + * @return array two-element array containing the list of parsed options and + * the non-option arguments + */ + function getopt($args, $short_options, $long_options = null, $skip_unknown = false) { - return Console_Getopt::doGetopt(1, $args, $short_options, $long_options); + return Console_Getopt::doGetopt(1, $args, $short_options, $long_options, $skip_unknown); } /** * The actual implementation of the argument parsing code. + * + * @param int $version Version to use + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option + * + * @return array */ - function doGetopt($version, $args, $short_options, $long_options = null) + function doGetopt($version, $args, $short_options, $long_options = null, $skip_unknown = false) { // in case you pass directly readPHPArgv() as the first arg if (PEAR::isError($args)) { return $args; } + if (empty($args)) { return array(array(), array()); } - $opts = array(); - $non_opts = array(); + + $non_opts = $opts = array(); settype($args, 'array'); @@ -111,7 +134,6 @@ class Console_Getopt { reset($args); while (list($i, $arg) = each($args)) { - /* The special element '--' means explicit end of options. Treat the rest of the arguments as non-options and end the loop. */ @@ -124,17 +146,27 @@ class Console_Getopt { $non_opts = array_merge($non_opts, array_slice($args, $i)); break; } elseif (strlen($arg) > 1 && $arg{1} == '-') { - $error = Console_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args); - if (PEAR::isError($error)) + $error = Console_Getopt::_parseLongOption(substr($arg, 2), + $long_options, + $opts, + $args, + $skip_unknown); + if (PEAR::isError($error)) { return $error; + } } elseif ($arg == '-') { // - is stdin $non_opts = array_merge($non_opts, array_slice($args, $i)); break; } else { - $error = Console_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args); - if (PEAR::isError($error)) + $error = Console_Getopt::_parseShortOption(substr($arg, 1), + $short_options, + $opts, + $args, + $skip_unknown); + if (PEAR::isError($error)) { return $error; + } } } @@ -142,19 +174,31 @@ class Console_Getopt { } /** - * @access private + * Parse short option * + * @param string $arg Argument + * @param string[] $short_options Available short options + * @param string[][] &$opts + * @param string[] &$args + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option + * + * @access private + * @return void */ - function _parseShortOption($arg, $short_options, &$opts, &$args) + function _parseShortOption($arg, $short_options, &$opts, &$args, $skip_unknown) { for ($i = 0; $i < strlen($arg); $i++) { - $opt = $arg{$i}; + $opt = $arg{$i}; $opt_arg = null; /* Try to find the short option in the specifier string. */ - if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') - { - return PEAR::raiseError("Console_Getopt: unrecognized option -- $opt"); + if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') { + if ($skip_unknown === true) { + break; + } + + $msg = "Console_Getopt: unrecognized option -- $opt"; + return PEAR::raiseError($msg); } if (strlen($spec) > 1 && $spec{1} == ':') { @@ -173,11 +217,14 @@ class Console_Getopt { break; } else if (list(, $opt_arg) = each($args)) { /* Else use the next argument. */; - if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { - return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt"); + if (Console_Getopt::_isShortOpt($opt_arg) + || Console_Getopt::_isLongOpt($opt_arg)) { + $msg = "option requires an argument --$opt"; + return PEAR::raiseError("Console_Getopt:" . $msg); } } else { - return PEAR::raiseError("Console_Getopt: option requires an argument -- $opt"); + $msg = "option requires an argument --$opt"; + return PEAR::raiseError("Console_Getopt:" . $msg); } } } @@ -187,36 +234,54 @@ class Console_Getopt { } /** - * @access private + * Checks if an argument is a short option * + * @param string $arg Argument to check + * + * @access private + * @return bool */ function _isShortOpt($arg) { - return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]); + return strlen($arg) == 2 && $arg[0] == '-' + && preg_match('/[a-zA-Z]/', $arg[1]); } /** - * @access private + * Checks if an argument is a long option * + * @param string $arg Argument to check + * + * @access private + * @return bool */ function _isLongOpt($arg) { return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' && - preg_match('/[a-zA-Z]+$/', substr($arg, 2)); + preg_match('/[a-zA-Z]+$/', substr($arg, 2)); } /** - * @access private + * Parse long option * + * @param string $arg Argument + * @param string[] $long_options Available long options + * @param string[][] &$opts + * @param string[] &$args + * + * @access private + * @return void|PEAR_Error */ - function _parseLongOption($arg, $long_options, &$opts, &$args) + function _parseLongOption($arg, $long_options, &$opts, &$args, $skip_unknown) { @list($opt, $opt_arg) = explode('=', $arg, 2); + $opt_len = strlen($opt); for ($i = 0; $i < count($long_options); $i++) { $long_opt = $long_options[$i]; $opt_start = substr($long_opt, 0, $opt_len); + $long_opt_name = str_replace('=', '', $long_opt); /* Option doesn't match. Go on to the next one. */ @@ -224,7 +289,7 @@ class Console_Getopt { continue; } - $opt_rest = substr($long_opt, $opt_len); + $opt_rest = substr($long_opt, $opt_len); /* Check that the options uniquely matches one of the allowed options. */ @@ -233,12 +298,15 @@ class Console_Getopt { } else { $next_option_rest = ''; } + if ($opt_rest != '' && $opt{0} != '=' && $i + 1 < count($long_options) && $opt == substr($long_options[$i+1], 0, $opt_len) && $next_option_rest != '' && $next_option_rest{0} != '=') { - return PEAR::raiseError("Console_Getopt: option --$opt is ambiguous"); + + $msg = "Console_Getopt: option --$opt is ambiguous"; + return PEAR::raiseError($msg); } if (substr($long_opt, -1) == '=') { @@ -246,37 +314,47 @@ class Console_Getopt { /* Long option requires an argument. Take the next argument if one wasn't specified. */; if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) { - return PEAR::raiseError("Console_Getopt: option --$opt requires an argument"); + $msg = "Console_Getopt: option requires an argument --$opt"; + return PEAR::raiseError($msg); } - if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { - return PEAR::raiseError("Console_Getopt: option requires an argument --$opt"); + + if (Console_Getopt::_isShortOpt($opt_arg) + || Console_Getopt::_isLongOpt($opt_arg)) { + $msg = "Console_Getopt: option requires an argument --$opt"; + return PEAR::raiseError($msg); } } } else if ($opt_arg) { - return PEAR::raiseError("Console_Getopt: option --$opt doesn't allow an argument"); + $msg = "Console_Getopt: option --$opt doesn't allow an argument"; + return PEAR::raiseError($msg); } $opts[] = array('--' . $opt, $opt_arg); return; } + if ($skip_unknown === true) { + return; + } + return PEAR::raiseError("Console_Getopt: unrecognized option --$opt"); } /** - * Safely read the $argv PHP array across different PHP configurations. - * Will take care on register_globals and register_argc_argv ini directives - * - * @access public - * @return mixed the $argv PHP array or PEAR error if not registered - */ + * Safely read the $argv PHP array across different PHP configurations. + * Will take care on register_globals and register_argc_argv ini directives + * + * @access public + * @return mixed the $argv PHP array or PEAR error if not registered + */ function readPHPArgv() { global $argv; if (!is_array($argv)) { if (!@is_array($_SERVER['argv'])) { if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { - return PEAR::raiseError("Console_Getopt: Could not read cmd args (register_argc_argv=Off?)"); + $msg = "Could not read cmd args (register_argc_argv=Off?)"; + return PEAR::raiseError("Console_Getopt: " . $msg); } return $GLOBALS['HTTP_SERVER_VARS']['argv']; } @@ -285,6 +363,4 @@ class Console_Getopt { return $argv; } -} - -?> +} \ No newline at end of file diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php index a69fbae86b..811d775965 100644 --- a/extlib/DB/DataObject.php +++ b/extlib/DB/DataObject.php @@ -15,7 +15,7 @@ * @author Alan Knowles * @copyright 1997-2006 The PHP Group * @license http://www.php.net/license/3_01.txt PHP License 3.01 - * @version CVS: $Id: DataObject.php 291349 2009-11-27 09:15:18Z alan_k $ + * @version CVS: $Id: DataObject.php 301030 2010-07-07 02:26:31Z alan_k $ * @link http://pear.php.net/package/DB_DataObject */ @@ -235,7 +235,7 @@ class DB_DataObject extends DB_DataObject_Overload * @access private * @var string */ - var $_DB_DataObject_version = "1.9.0"; + var $_DB_DataObject_version = "1.9.5"; /** * The Database table (used by table extends) @@ -369,6 +369,32 @@ class DB_DataObject extends DB_DataObject_Overload return $_DB_DATAOBJECT['CACHE'][$lclass][$key]; } + /** + * build the basic select query. + * + * @access private + */ + + function _build_select() + { + global $_DB_DATAOBJECT; + $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']); + if ($quoteIdentifiers) { + $this->_connect(); + $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]; + } + $sql = 'SELECT ' . + $this->_query['data_select'] . " \n" . + ' FROM ' . ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table) . " \n" . + $this->_join . " \n" . + $this->_query['condition'] . " \n" . + $this->_query['group_by'] . " \n" . + $this->_query['having'] . " \n"; + + return $sql; + } + + /** * find results, either normal or crosstable * @@ -411,20 +437,21 @@ class DB_DataObject extends DB_DataObject_Overload $query_before = $this->_query; $this->_build_condition($this->table()) ; - $quoteIdentifiers = !empty($_DB_DATAOBJECT['CONFIG']['quote_identifiers']); + $this->_connect(); $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]; - /* We are checking for method modifyLimitQuery as it is PEAR DB specific */ - $sql = 'SELECT ' . - $this->_query['data_select'] . " \n" . - ' FROM ' . ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table) . " \n" . - $this->_join . " \n" . - $this->_query['condition'] . " \n" . - $this->_query['group_by'] . " \n" . - $this->_query['having'] . " \n" . - $this->_query['order_by'] . " \n"; + $sql = $this->_build_select(); + + foreach ($this->_query['unions'] as $union_ar) { + $sql .= $union_ar[1] . $union_ar[0]->_build_select() . " \n"; + } + + $sql .= $this->_query['order_by'] . " \n"; + + + /* We are checking for method modifyLimitQuery as it is PEAR DB specific */ if ((!isset($_DB_DATAOBJECT['CONFIG']['db_driver'])) || ($_DB_DATAOBJECT['CONFIG']['db_driver'] == 'DB')) { /* PEAR DB specific */ @@ -578,6 +605,85 @@ class DB_DataObject extends DB_DataObject_Overload return true; } + + /** + * fetches all results as an array, + * + * return format is dependant on args. + * if selectAdd() has not been called on the object, then it will add the correct columns to the query. + * + * A) Array of values (eg. a list of 'id') + * + * $x = DB_DataObject::factory('mytable'); + * $x->whereAdd('something = 1') + * $ar = $x->fetchAll('id'); + * -- returns array(1,2,3,4,5) + * + * B) Array of values (not from table) + * + * $x = DB_DataObject::factory('mytable'); + * $x->whereAdd('something = 1'); + * $x->selectAdd(); + * $x->selectAdd('distinct(group_id) as group_id'); + * $ar = $x->fetchAll('group_id'); + * -- returns array(1,2,3,4,5) + * * + * C) A key=>value associative array + * + * $x = DB_DataObject::factory('mytable'); + * $x->whereAdd('something = 1') + * $ar = $x->fetchAll('id','name'); + * -- returns array(1=>'fred',2=>'blogs',3=> ....... + * + * D) array of objects + * $x = DB_DataObject::factory('mytable'); + * $x->whereAdd('something = 1'); + * $ar = $x->fetchAll(); + * + * E) array of arrays (for example) + * $x = DB_DataObject::factory('mytable'); + * $x->whereAdd('something = 1'); + * $ar = $x->fetchAll(false,false,'toArray'); + * + * + * @param string|false $k key + * @param string|false $v value + * @param string|false $method method to call on each result to get array value (eg. 'toArray') + * @access public + * @return array format dependant on arguments, may be empty + */ + function fetchAll($k= false, $v = false, $method = false) + { + // should it even do this!!!?!? + if ($k !== false && + ( // only do this is we have not been explicit.. + empty($this->_query['data_select']) || + ($this->_query['data_select'] == '*') + ) + ) { + $this->selectAdd(); + $this->selectAdd($k); + if ($v !== false) { + $this->selectAdd($v); + } + } + + $this->find(); + $ret = array(); + while ($this->fetch()) { + if ($v !== false) { + $ret[$this->$k] = $this->$v; + continue; + } + $ret[] = $k === false ? + ($method == false ? clone($this) : $this->$method()) + : $this->$k; + } + return $ret; + + } + + /** * Adds a condition to the WHERE statement, defaults to AND * @@ -622,6 +728,47 @@ class DB_DataObject extends DB_DataObject_Overload return $r; } + /** + * Adds a 'IN' condition to the WHERE statement + * + * $object->whereAddIn('id', $array, 'int'); //minimal usage + * $object->whereAddIn('price', $array, 'float', 'OR'); // cast to float, and call whereAdd with 'OR' + * $object->whereAddIn('name', $array, 'string'); // quote strings + * + * @param string $key key column to match + * @param array $list list of values to match + * @param string $type string|int|integer|float|bool cast to type. + * @param string $logic optional logic to call whereAdd with eg. "OR" (defaults to "AND") + * @access public + * @return string|PEAR::Error - previous condition or Error when invalid args found + */ + function whereAddIn($key, $list, $type, $logic = 'AND') + { + $not = ''; + if ($key[0] == '!') { + $not = 'NOT '; + $key = substr($key, 1); + } + // fix type for short entry. + $type = $type == 'int' ? 'integer' : $type; + + if ($type == 'string') { + $this->_connect(); + } + + $ar = array(); + foreach($list as $k) { + settype($k, $type); + $ar[] = $type =='string' ? $this->_quote($k) : $k; + } + if (!$ar) { + return $not ? $this->_query['condition'] : $this->whereAdd("1=0"); + } + return $this->whereAdd("$key $not IN (". implode(',', $ar). ')', $logic ); + } + + + /** * Adds a order by condition * @@ -1175,7 +1322,7 @@ class DB_DataObject extends DB_DataObject_Overload $seq = $this->sequenceKey(); if ($seq[0] !== false) { $keys = array($seq[0]); - if (empty($this->{$keys[0]}) && $dataObject !== true) { + if (!isset($this->{$keys[0]}) && $dataObject !== true) { $this->raiseError("update: trying to perform an update without the key set, and argument to update is not DB_DATAOBJECT_WHEREADD_ONLY @@ -1670,6 +1817,7 @@ class DB_DataObject extends DB_DataObject_Overload 'limit_start' => '', // the LIMIT condition 'limit_count' => '', // the LIMIT condition 'data_select' => '*', // the columns to be SELECTed + 'unions' => array(), // the added unions ); @@ -2032,9 +2180,9 @@ class DB_DataObject extends DB_DataObject_Overload $seqname = false; if (!empty($_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table])) { - $usekey = $_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table]; - if (strpos($usekey,':') !== false) { - list($usekey,$seqname) = explode(':',$usekey); + $seqname = $_DB_DATAOBJECT['CONFIG']['sequence_'.$this->__table]; + if (strpos($seqname,':') !== false) { + list($usekey,$seqname) = explode(':',$seqname); } } @@ -3068,9 +3216,9 @@ class DB_DataObject extends DB_DataObject_Overload } /** - * IS THIS SUPPORTED/USED ANYMORE???? - *return a list of options for a linked table - * + * getLinkArray + * Fetch an array of related objects. This should be used in conjunction with a .links.ini file configuration (see the introduction on linking for details on this). + * You may also use this with all parameters to specify, the column and related table. * This is highly dependant on naming columns 'correctly' :) * using colname = xxxxx_yyyyyy * xxxxxx = related table; (yyyyy = user defined..) @@ -3078,7 +3226,21 @@ class DB_DataObject extends DB_DataObject_Overload * stores it in $this->_xxxxx_yyyyy * * @access public - * @return array of results (empty array on failure) + * @param string $column - either column or column.xxxxx + * @param string $table - name of table to look up value in + * @return array - array of results (empty array on failure) + * + * Example - Getting the related objects + * + * $person = new DataObjects_Person; + * $person->get(12); + * $children = $person->getLinkArray('children'); + * + * echo 'There are ', count($children), ' descendant(s):
'; + * foreach ($children as $child) { + * echo $child->name, '
'; + * } + * */ function &getLinkArray($row, $table = null) { @@ -3123,6 +3285,46 @@ class DB_DataObject extends DB_DataObject_Overload return $ret; } + /** + * unionAdd - adds another dataobject to this, building a unioned query. + * + * usage: + * $doTable1 = DB_DataObject::factory("table1"); + * $doTable2 = DB_DataObject::factory("table2"); + * + * $doTable1->selectAdd(); + * $doTable1->selectAdd("col1,col2"); + * $doTable1->whereAdd("col1 > 100"); + * $doTable1->orderBy("col1"); + * + * $doTable2->selectAdd(); + * $doTable2->selectAdd("col1, col2"); + * $doTable2->whereAdd("col2 = 'v'"); + * + * $doTable1->unionAdd($doTable2); + * $doTable1->find(); + * + * Note: this model may be a better way to implement joinAdd?, eg. do the building in find? + * + * + * @param $obj object|false the union object or false to reset + * @param optional $is_all string 'ALL' to do all. + * @returns $obj object|array the added object, or old list if reset. + */ + + function unionAdd($obj,$is_all= '') + { + if ($obj === false) { + $ret = $this->_query['unions']; + $this->_query['unions'] = array(); + return $ret; + } + $this->_query['unions'][] = array($obj, 'UNION ' . $is_all . ' ') ; + return $obj; + } + + + /** * The JOIN condition * @@ -3248,31 +3450,46 @@ class DB_DataObject extends DB_DataObject_Overload /* otherwise see if there are any links from this table to the obj. */ //print_r($this->links()); if (($ofield === false) && ($links = $this->links())) { - foreach ($links as $k => $v) { - /* link contains {this column} = {linked table}:{linked column} */ - $ar = explode(':', $v); - // Feature Request #4266 - Allow joins with multiple keys - if (strpos($k, ',') !== false) { - $k = explode(',', $k); - } - if (strpos($ar[1], ',') !== false) { - $ar[1] = explode(',', $ar[1]); + // this enables for support for arrays of links in ini file. + // link contains this_column[] = linked_table:linked_column + // or standard way. + // link contains this_column = linked_table:linked_column + foreach ($links as $k => $linkVar) { + + if (!is_array($linkVar)) { + $linkVar = array($linkVar); } + foreach($linkVar as $v) { - if ($ar[0] == $obj->__table) { + + + /* link contains {this column} = {linked table}:{linked column} */ + $ar = explode(':', $v); + // Feature Request #4266 - Allow joins with multiple keys + if (strpos($k, ',') !== false) { + $k = explode(',', $k); + } + if (strpos($ar[1], ',') !== false) { + $ar[1] = explode(',', $ar[1]); + } + + if ($ar[0] != $obj->__table) { + continue; + } if ($joinCol !== false) { if ($k == $joinCol) { + // got it!? $tfield = $k; $ofield = $ar[1]; break; - } else { - continue; - } - } else { - $tfield = $k; - $ofield = $ar[1]; - break; - } + } + continue; + + } + $tfield = $k; + $ofield = $ar[1]; + break; + } } } @@ -3280,23 +3497,30 @@ class DB_DataObject extends DB_DataObject_Overload //print_r($obj->links()); if (!$ofield && ($olinks = $obj->links())) { - foreach ($olinks as $k => $v) { - /* link contains {this column} = {linked table}:{linked column} */ - $ar = explode(':', $v); - - // Feature Request #4266 - Allow joins with multiple keys - - $links_key_array = strpos($k,','); - if ($links_key_array !== false) { - $k = explode(',', $k); + foreach ($olinks as $k => $linkVar) { + /* link contains {this column} = array ( {linked table}:{linked column} )*/ + if (!is_array($linkVar)) { + $linkVar = array($linkVar); } - - $ar_array = strpos($ar[1],','); - if ($ar_array !== false) { - $ar[1] = explode(',', $ar[1]); - } - - if ($ar[0] == $this->__table) { + foreach($linkVar as $v) { + + /* link contains {this column} = {linked table}:{linked column} */ + $ar = explode(':', $v); + + // Feature Request #4266 - Allow joins with multiple keys + $links_key_array = strpos($k,','); + if ($links_key_array !== false) { + $k = explode(',', $k); + } + + $ar_array = strpos($ar[1],','); + if ($ar_array !== false) { + $ar[1] = explode(',', $ar[1]); + } + + if ($ar[0] != $this->__table) { + continue; + } // you have explictly specified the column // and the col is listed here.. @@ -3315,6 +3539,7 @@ class DB_DataObject extends DB_DataObject_Overload $ofield = $k; $tfield = $ar[1]; break; + } } } @@ -3573,7 +3798,14 @@ class DB_DataObject extends DB_DataObject_Overload if (!$k) { continue; // ignore empty keys!!! what } - if (is_object($from) && isset($from->{sprintf($format,$k)})) { + + $chk = is_object($from) && + (version_compare(phpversion(), "5.1.0" , ">=") ? + property_exists($from, sprintf($format,$k)) : // php5.1 + array_key_exists( sprintf($format,$k), get_class_vars($from)) //older + ); + // if from has property ($format($k) + if ($chk) { $kk = (strtolower($k) == 'from') ? '_from' : $k; if (method_exists($this,'set'.$kk)) { $ret = $this->{'set'.$kk}($from->{sprintf($format,$k)}); diff --git a/extlib/DB/DataObject/Generator.php b/extlib/DB/DataObject/Generator.php index 17d310f57c..7ea716dc7c 100644 --- a/extlib/DB/DataObject/Generator.php +++ b/extlib/DB/DataObject/Generator.php @@ -15,7 +15,7 @@ * @author Alan Knowles * @copyright 1997-2006 The PHP Group * @license http://www.php.net/license/3_01.txt PHP License 3.01 - * @version CVS: $Id: Generator.php 289384 2009-10-09 00:17:26Z alan_k $ + * @version CVS: $Id: Generator.php 298560 2010-04-25 23:01:51Z alan_k $ * @link http://pear.php.net/package/DB_DataObject */ @@ -383,8 +383,8 @@ class DB_DataObject_Generator extends DB_DataObject return false; } $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; - if (!in_array($__DB->phptype, array('mysql','mysqli'))) { - echo "WARNING: cant handle non-mysql introspection for defaults."; + if (!in_array($__DB->phptype, array('mysql', 'mysqli', 'pgsql'))) { + echo "WARNING: cant handle non-mysql and pgsql introspection for defaults."; return; // cant handle non-mysql introspection for defaults. } @@ -392,33 +392,72 @@ class DB_DataObject_Generator extends DB_DataObject $fk = array(); - foreach($this->tables as $this->table) { - $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($table) : $this->table; - - $res =& $DB->query('SHOW CREATE TABLE ' . $quotedTable ); - if (PEAR::isError($res)) { - die($res->getMessage()); - } + switch ($DB->phptype) { - $text = $res->fetchRow(DB_FETCHMODE_DEFAULT, 0); - $treffer = array(); - // Extract FOREIGN KEYS - preg_match_all( - "/FOREIGN KEY \(`(\w*)`\) REFERENCES `(\w*)` \(`(\w*)`\)/i", - $text[1], - $treffer, - PREG_SET_ORDER); - if (count($treffer) < 1) { - continue; - } - for ($i = 0; $i < count($treffer); $i++) { - $fk[$this->table][$treffer[$i][1]] = $treffer[$i][2] . ":" . $treffer[$i][3]; - } - + case 'pgsql': + foreach($this->tables as $this->table) { + $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($table) : $this->table; + $res =& $DB->query("SELECT + pg_catalog.pg_get_constraintdef(r.oid, true) AS condef + FROM pg_catalog.pg_constraint r, + pg_catalog.pg_class c + WHERE c.oid=r.conrelid + AND r.contype = 'f' + AND c.relname = '" . $quotedTable . "'"); + if (PEAR::isError($res)) { + die($res->getMessage()); + } + + while ($row = $res->fetchRow(DB_FETCHMODE_ASSOC)) { + $treffer = array(); + // this only picks up one of these.. see this for why: http://pear.php.net/bugs/bug.php?id=17049 + preg_match( + "/FOREIGN KEY \((\w*)\) REFERENCES (\w*)\((\w*)\)/i", + $row['condef'], + $treffer); + if (!count($treffer)) { + continue; + } + $fk[$this->table][$treffer[1]] = $treffer[2] . ":" . $treffer[3]; + } + } + break; + + + case 'mysql': + case 'mysqli': + default: + + foreach($this->tables as $this->table) { + $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($table) : $this->table; + + $res =& $DB->query('SHOW CREATE TABLE ' . $quotedTable ); + + if (PEAR::isError($res)) { + die($res->getMessage()); + } + + $text = $res->fetchRow(DB_FETCHMODE_DEFAULT, 0); + $treffer = array(); + // Extract FOREIGN KEYS + preg_match_all( + "/FOREIGN KEY \(`(\w*)`\) REFERENCES `(\w*)` \(`(\w*)`\)/i", + $text[1], + $treffer, + PREG_SET_ORDER); + + if (!count($treffer)) { + continue; + } + foreach($treffer as $i=> $tref) { + $fk[$this->table][$tref[1]] = $tref[2] . ":" . $tref[3]; + } + + } + } - $links_ini = ""; foreach($fk as $table => $details) { @@ -861,10 +900,8 @@ class DB_DataObject_Generator extends DB_DataObject $body = "\n ###START_AUTOCODE\n"; $body .= " /* the code below is auto generated do not remove the above tag */\n\n"; // table - $padding = (30 - strlen($this->table)); - $padding = ($padding < 2) ? 2 : $padding; - - $p = str_repeat(' ',$padding) ; + + $p = str_repeat(' ',max(2, (18 - strlen($this->table)))) ; $options = &PEAR::getStaticProperty('DB_DataObject','options'); @@ -887,6 +924,7 @@ class DB_DataObject_Generator extends DB_DataObject // Only include the $_database property if the omit_database_var is unset or false if (isset($options["database_{$this->_database}"]) && empty($GLOBALS['_DB_DATAOBJECT']['CONFIG']['generator_omit_database_var'])) { + $p = str_repeat(' ', max(2, (16 - strlen($this->table)))); $body .= " {$var} \$_database = '{$this->_database}'; {$p}// database name (used with database_{*} config)\n"; } @@ -900,6 +938,7 @@ class DB_DataObject_Generator extends DB_DataObject // show nice information! $connections = array(); $sets = array(); + foreach($defs as $t) { if (!strlen(trim($t->name))) { continue; @@ -915,19 +954,18 @@ class DB_DataObject_Generator extends DB_DataObject continue; } - - $padding = (30 - strlen($t->name)); - if ($padding < 2) $padding =2; - $p = str_repeat(' ',$padding) ; - + $p = str_repeat(' ',max(2, (30 - strlen($t->name)))); + $length = empty($t->len) ? '' : '('.$t->len.')'; $body .=" {$var} \${$t->name}; {$p}// {$t->type}$length {$t->flags}\n"; // can not do set as PEAR::DB table info doesnt support it. //if (substr($t->Type,0,3) == "set") // $sets[$t->Field] = "array".substr($t->Type,3); - $body .= $this->derivedHookVar($t,$padding); + $body .= $this->derivedHookVar($t,strlen($p)); } + + $body .= $this->derivedHookPostVar($defs); // THIS IS TOTALLY BORKED old FC creation // IT WILL BE REMOVED!!!!! in DataObjects 1.6 @@ -1078,7 +1116,21 @@ class DB_DataObject_Generator extends DB_DataObject // It MUST NOT be changed here!!! return ""; } - + /** + * hook for after var lines ( + * called at the end of the output of var line have generated, override to add extra var + * lines + * + * @param array cols containing array of objects with type,len,flags etc. from tableInfo call + * @access public + * @return string added to class eg. functions. + */ + function derivedHookPostVar($t) + { + // This is so derived generator classes can generate variabels + // It MUST NOT be changed here!!! + return ""; + } /** * hook to add extra page-level (in terms of phpDocumentor) DocBlock * diff --git a/extlib/PEAR.php b/extlib/PEAR.php index 4c24c6006a..fec8de45cc 100644 --- a/extlib/PEAR.php +++ b/extlib/PEAR.php @@ -6,21 +6,15 @@ * * PHP versions 4 and 5 * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * * @category pear * @package PEAR * @author Sterling Hughes * @author Stig Bakken * @author Tomas V.V.Cox * @author Greg Beaver - * @copyright 1997-2008 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version CVS: $Id: PEAR.php,v 1.104 2008/01/03 20:26:34 cellog Exp $ + * @copyright 1997-2010 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: PEAR.php 307683 2011-01-23 21:56:12Z dufuz $ * @link http://pear.php.net/package/PEAR * @since File available since Release 0.1 */ @@ -52,15 +46,6 @@ if (substr(PHP_OS, 0, 3) == 'WIN') { define('PEAR_OS', 'Unix'); // blatant assumption } -// instant backwards compatibility -if (!defined('PATH_SEPARATOR')) { - if (OS_WINDOWS) { - define('PATH_SEPARATOR', ';'); - } else { - define('PATH_SEPARATOR', ':'); - } -} - $GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; $GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; $GLOBALS['_PEAR_destructor_object_list'] = array(); @@ -92,8 +77,8 @@ $GLOBALS['_PEAR_error_handler_stack'] = array(); * @author Tomas V.V. Cox * @author Greg Beaver * @copyright 1997-2006 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.7.2 + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.2 * @link http://pear.php.net/package/PEAR * @see PEAR_Error * @since Class available since PHP 4.0.2 @@ -101,8 +86,6 @@ $GLOBALS['_PEAR_error_handler_stack'] = array(); */ class PEAR { - // {{{ properties - /** * Whether to enable internal debug messages. * @@ -153,10 +136,6 @@ class PEAR */ var $_expected_errors = array(); - // }}} - - // {{{ constructor - /** * Constructor. Registers this object in * $_PEAR_destructor_object_list for destructor emulation if a @@ -173,9 +152,11 @@ class PEAR if ($this->_debug) { print "PEAR constructor called, class=$classname\n"; } + if ($error_class !== null) { $this->_error_class = $error_class; } + while ($classname && strcasecmp($classname, "pear")) { $destructor = "_$classname"; if (method_exists($this, $destructor)) { @@ -192,9 +173,6 @@ class PEAR } } - // }}} - // {{{ destructor - /** * Destructor (the emulated type of...). Does nothing right now, * but is included for forward compatibility, so subclass @@ -212,9 +190,6 @@ class PEAR } } - // }}} - // {{{ getStaticProperty() - /** * If you have a class that's mostly/entirely static, and you need static * properties, you can use this method to simulate them. Eg. in your method(s) @@ -233,15 +208,14 @@ class PEAR if (!isset($properties[$class])) { $properties[$class] = array(); } + if (!array_key_exists($var, $properties[$class])) { $properties[$class][$var] = null; } + return $properties[$class][$var]; } - // }}} - // {{{ registerShutdownFunc() - /** * Use this function to register a shutdown method for static * classes. @@ -262,9 +236,6 @@ class PEAR $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); } - // }}} - // {{{ isError() - /** * Tell whether a value is a PEAR error. * @@ -278,20 +249,18 @@ class PEAR */ function isError($data, $code = null) { - if (is_a($data, 'PEAR_Error')) { - if (is_null($code)) { - return true; - } elseif (is_string($code)) { - return $data->getMessage() == $code; - } else { - return $data->getCode() == $code; - } + if (!is_a($data, 'PEAR_Error')) { + return false; } - return false; - } - // }}} - // {{{ setErrorHandling() + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } + + return $data->getCode() == $code; + } /** * Sets how errors generated by this object should be handled. @@ -331,7 +300,6 @@ class PEAR * * @since PHP 4.0.5 */ - function setErrorHandling($mode = null, $options = null) { if (isset($this) && is_a($this, 'PEAR')) { @@ -369,9 +337,6 @@ class PEAR } } - // }}} - // {{{ expectError() - /** * This method is used to tell which errors you expect to get. * Expected errors are always returned with error mode @@ -394,12 +359,9 @@ class PEAR } else { array_push($this->_expected_errors, array($code)); } - return sizeof($this->_expected_errors); + return count($this->_expected_errors); } - // }}} - // {{{ popExpect() - /** * This method pops one element off the expected error codes * stack. @@ -411,9 +373,6 @@ class PEAR return array_pop($this->_expected_errors); } - // }}} - // {{{ _checkDelExpect() - /** * This method checks unsets an error code if available * @@ -425,8 +384,7 @@ class PEAR function _checkDelExpect($error_code) { $deleted = false; - - foreach ($this->_expected_errors AS $key => $error_array) { + foreach ($this->_expected_errors as $key => $error_array) { if (in_array($error_code, $error_array)) { unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); $deleted = true; @@ -437,12 +395,10 @@ class PEAR unset($this->_expected_errors[$key]); } } + return $deleted; } - // }}} - // {{{ delExpect() - /** * This method deletes all occurences of the specified element from * the expected error codes stack. @@ -455,34 +411,26 @@ class PEAR function delExpect($error_code) { $deleted = false; - if ((is_array($error_code) && (0 != count($error_code)))) { - // $error_code is a non-empty array here; - // we walk through it trying to unset all - // values - foreach($error_code as $key => $error) { - if ($this->_checkDelExpect($error)) { - $deleted = true; - } else { - $deleted = false; - } + // $error_code is a non-empty array here; we walk through it trying + // to unset all values + foreach ($error_code as $key => $error) { + $deleted = $this->_checkDelExpect($error) ? true : false; } + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME } elseif (!empty($error_code)) { // $error_code comes alone, trying to unset it if ($this->_checkDelExpect($error_code)) { return true; - } else { - return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME } - } else { - // $error_code is empty - return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME - } - } - // }}} - // {{{ raiseError() + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } /** * This method is a wrapper that returns an instance of the @@ -538,13 +486,20 @@ class PEAR $message = $message->getMessage(); } - if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ( + isset($this) && + isset($this->_expected_errors) && + count($this->_expected_errors) > 0 && + count($exp = end($this->_expected_errors)) + ) { if ($exp[0] == "*" || (is_int(reset($exp)) && in_array($code, $exp)) || - (is_string(reset($exp)) && in_array($message, $exp))) { + (is_string(reset($exp)) && in_array($message, $exp)) + ) { $mode = PEAR_ERROR_RETURN; } } + // No mode given, try global ones if ($mode === null) { // Class error handler @@ -565,46 +520,52 @@ class PEAR } else { $ec = 'PEAR_Error'; } + if (intval(PHP_VERSION) < 5) { // little non-eval hack to fix bug #12147 include 'PEAR/FixPHP5PEARWarnings.php'; return $a; } + if ($skipmsg) { $a = new $ec($code, $mode, $options, $userinfo); } else { $a = new $ec($message, $code, $mode, $options, $userinfo); } + return $a; } - // }}} - // {{{ throwError() - /** * Simpler form of raiseError with fewer options. In most cases * message, code and userinfo are enough. * - * @param string $message + * @param mixed $message a text error message or a PEAR error object * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @access public + * @return object a PEAR error object + * @see PEAR::raiseError */ - function &throwError($message = null, - $code = null, - $userinfo = null) + function &throwError($message = null, $code = null, $userinfo = null) { if (isset($this) && is_a($this, 'PEAR')) { $a = &$this->raiseError($message, $code, null, null, $userinfo); return $a; - } else { - $a = &PEAR::raiseError($message, $code, null, null, $userinfo); - return $a; } + + $a = &PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; } - // }}} function staticPushErrorHandling($mode, $options = null) { - $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $stack = &$GLOBALS['_PEAR_error_handler_stack']; $def_mode = &$GLOBALS['_PEAR_default_error_mode']; $def_options = &$GLOBALS['_PEAR_default_error_options']; $stack[] = array($def_mode, $def_options); @@ -673,8 +634,6 @@ class PEAR return true; } - // {{{ pushErrorHandling() - /** * Push a new error handler on top of the error handler options stack. With this * you can easily override the actual error handler for some code and restore @@ -708,9 +667,6 @@ class PEAR return true; } - // }}} - // {{{ popErrorHandling() - /** * Pop the last error handler used * @@ -732,9 +688,6 @@ class PEAR return true; } - // }}} - // {{{ loadExtension() - /** * OS independant PHP extension load. Remember to take care * on the correct extension name for case sensitive OSes. @@ -744,31 +697,38 @@ class PEAR */ function loadExtension($ext) { - if (!extension_loaded($ext)) { - // if either returns true dl() will produce a FATAL error, stop that - if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { - return false; - } - if (OS_WINDOWS) { - $suffix = '.dll'; - } elseif (PHP_OS == 'HP-UX') { - $suffix = '.sl'; - } elseif (PHP_OS == 'AIX') { - $suffix = '.a'; - } elseif (PHP_OS == 'OSX') { - $suffix = '.bundle'; - } else { - $suffix = '.so'; - } - return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + if (extension_loaded($ext)) { + return true; } - return true; - } - // }}} + // if either returns true dl() will produce a FATAL error, stop that + if ( + function_exists('dl') === false || + ini_get('enable_dl') != 1 || + ini_get('safe_mode') == 1 + ) { + return false; + } + + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } } -// {{{ _PEAR_call_destructors() +if (PEAR_ZE2) { + include_once 'PEAR5.php'; +} function _PEAR_call_destructors() { @@ -777,9 +737,16 @@ function _PEAR_call_destructors() sizeof($_PEAR_destructor_object_list)) { reset($_PEAR_destructor_object_list); - if (PEAR::getStaticProperty('PEAR', 'destructlifo')) { + if (PEAR_ZE2) { + $destructLifoExists = PEAR5::getStaticProperty('PEAR', 'destructlifo'); + } else { + $destructLifoExists = PEAR::getStaticProperty('PEAR', 'destructlifo'); + } + + if ($destructLifoExists) { $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); } + while (list($k, $objref) = each($_PEAR_destructor_object_list)) { $classname = get_class($objref); while ($classname) { @@ -798,14 +765,17 @@ function _PEAR_call_destructors() } // Now call the shutdown functions - if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + if ( + isset($GLOBALS['_PEAR_shutdown_funcs']) && + is_array($GLOBALS['_PEAR_shutdown_funcs']) && + !empty($GLOBALS['_PEAR_shutdown_funcs']) + ) { foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { call_user_func_array($value[0], $value[1]); } } } -// }}} /** * Standard PEAR error class for PHP 4 * @@ -817,16 +787,14 @@ function _PEAR_call_destructors() * @author Tomas V.V. Cox * @author Gregory Beaver * @copyright 1997-2006 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.7.2 + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.2 * @link http://pear.php.net/manual/en/core.pear.pear-error.php * @see PEAR::raiseError(), PEAR::throwError() * @since Class available since PHP 4.0.2 */ class PEAR_Error { - // {{{ properties - var $error_message_prefix = ''; var $mode = PEAR_ERROR_RETURN; var $level = E_USER_NOTICE; @@ -835,9 +803,6 @@ class PEAR_Error var $userinfo = ''; var $backtrace = null; - // }}} - // {{{ constructor - /** * PEAR_Error constructor * @@ -868,12 +833,20 @@ class PEAR_Error $this->code = $code; $this->mode = $mode; $this->userinfo = $userinfo; - if (!PEAR::getStaticProperty('PEAR_Error', 'skiptrace')) { + + if (PEAR_ZE2) { + $skiptrace = PEAR5::getStaticProperty('PEAR_Error', 'skiptrace'); + } else { + $skiptrace = PEAR::getStaticProperty('PEAR_Error', 'skiptrace'); + } + + if (!$skiptrace) { $this->backtrace = debug_backtrace(); if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { unset($this->backtrace[0]['object']); } } + if ($mode & PEAR_ERROR_CALLBACK) { $this->level = E_USER_NOTICE; $this->callback = $options; @@ -881,20 +854,25 @@ class PEAR_Error if ($options === null) { $options = E_USER_NOTICE; } + $this->level = $options; $this->callback = null; } + if ($this->mode & PEAR_ERROR_PRINT) { if (is_null($options) || is_int($options)) { $format = "%s"; } else { $format = $options; } + printf($format, $this->getMessage()); } + if ($this->mode & PEAR_ERROR_TRIGGER) { trigger_error($this->getMessage(), $this->level); } + if ($this->mode & PEAR_ERROR_DIE) { $msg = $this->getMessage(); if (is_null($options) || is_int($options)) { @@ -907,47 +885,39 @@ class PEAR_Error } die(sprintf($format, $msg)); } - if ($this->mode & PEAR_ERROR_CALLBACK) { - if (is_callable($this->callback)) { - call_user_func($this->callback, $this); - } + + if ($this->mode & PEAR_ERROR_CALLBACK && is_callable($this->callback)) { + call_user_func($this->callback, $this); } + if ($this->mode & PEAR_ERROR_EXCEPTION) { trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); eval('$e = new Exception($this->message, $this->code);throw($e);'); } } - // }}} - // {{{ getMode() - /** * Get the error mode from an error object. * * @return int error mode * @access public */ - function getMode() { + function getMode() + { return $this->mode; } - // }}} - // {{{ getCallback() - /** * Get the callback function/method from an error object. * * @return mixed callback function or object/method array * @access public */ - function getCallback() { + function getCallback() + { return $this->callback; } - // }}} - // {{{ getMessage() - - /** * Get the error message from an error object. * @@ -959,10 +929,6 @@ class PEAR_Error return ($this->error_message_prefix . $this->message); } - - // }}} - // {{{ getCode() - /** * Get error code from an error object * @@ -974,9 +940,6 @@ class PEAR_Error return $this->code; } - // }}} - // {{{ getType() - /** * Get the name of this error/exception. * @@ -988,9 +951,6 @@ class PEAR_Error return get_class($this); } - // }}} - // {{{ getUserInfo() - /** * Get additional user-supplied information. * @@ -1002,9 +962,6 @@ class PEAR_Error return $this->userinfo; } - // }}} - // {{{ getDebugInfo() - /** * Get additional debug information supplied by the application. * @@ -1016,9 +973,6 @@ class PEAR_Error return $this->getUserInfo(); } - // }}} - // {{{ getBacktrace() - /** * Get the call backtrace from where the error was generated. * Supported with PHP 4.3.0 or newer. @@ -1038,9 +992,6 @@ class PEAR_Error return $this->backtrace[$frame]; } - // }}} - // {{{ addUserInfo() - function addUserInfo($info) { if (empty($this->userinfo)) { @@ -1050,14 +1001,10 @@ class PEAR_Error } } - // }}} - // {{{ toString() function __toString() { return $this->getMessage(); } - // }}} - // {{{ toString() /** * Make a string representation of this object. @@ -1065,7 +1012,8 @@ class PEAR_Error * @return string a string with an object summary * @access public */ - function toString() { + function toString() + { $modes = array(); $levels = array(E_USER_NOTICE => 'notice', E_USER_WARNING => 'warning', @@ -1104,8 +1052,6 @@ class PEAR_Error $this->error_message_prefix, $this->userinfo); } - - // }}} } /* @@ -1115,4 +1061,3 @@ class PEAR_Error * c-basic-offset: 4 * End: */ -?> diff --git a/extlib/PEAR/ErrorStack.php b/extlib/PEAR/ErrorStack.php new file mode 100644 index 0000000000..3229fba520 --- /dev/null +++ b/extlib/PEAR/ErrorStack.php @@ -0,0 +1,985 @@ + + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: ErrorStack.php 307683 2011-01-23 21:56:12Z dufuz $ + * @link http://pear.php.net/package/PEAR_ErrorStack + */ + +/** + * Singleton storage + * + * Format: + *
+ * array(
+ *  'package1' => PEAR_ErrorStack object,
+ *  'package2' => PEAR_ErrorStack object,
+ *  ...
+ * )
+ * 
+ * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] + */ +$GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] = array(); + +/** + * Global error callback (default) + * + * This is only used if set to non-false. * is the default callback for + * all packages, whereas specific packages may set a default callback + * for all instances, regardless of whether they are a singleton or not. + * + * To exclude non-singletons, only set the local callback for the singleton + * @see PEAR_ErrorStack::setDefaultCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] = array( + '*' => false, +); + +/** + * Global Log object (default) + * + * This is only used if set to non-false. Use to set a default log object for + * all stacks, regardless of instantiation order or location + * @see PEAR_ErrorStack::setDefaultLogger() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = false; + +/** + * Global Overriding Callback + * + * This callback will override any error callbacks that specific loggers have set. + * Use with EXTREME caution + * @see PEAR_ErrorStack::staticPushCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + +/**#@+ + * One of four possible return values from the error Callback + * @see PEAR_ErrorStack::_errorCallback() + */ +/** + * If this is returned, then the error will be both pushed onto the stack + * and logged. + */ +define('PEAR_ERRORSTACK_PUSHANDLOG', 1); +/** + * If this is returned, then the error will only be pushed onto the stack, + * and not logged. + */ +define('PEAR_ERRORSTACK_PUSH', 2); +/** + * If this is returned, then the error will only be logged, but not pushed + * onto the error stack. + */ +define('PEAR_ERRORSTACK_LOG', 3); +/** + * If this is returned, then the error is completely ignored. + */ +define('PEAR_ERRORSTACK_IGNORE', 4); +/** + * If this is returned, then the error is logged and die() is called. + */ +define('PEAR_ERRORSTACK_DIE', 5); +/**#@-*/ + +/** + * Error code for an attempt to instantiate a non-class as a PEAR_ErrorStack in + * the singleton method. + */ +define('PEAR_ERRORSTACK_ERR_NONCLASS', 1); + +/** + * Error code for an attempt to pass an object into {@link PEAR_ErrorStack::getMessage()} + * that has no __toString() method + */ +define('PEAR_ERRORSTACK_ERR_OBJTOSTRING', 2); +/** + * Error Stack Implementation + * + * Usage: + * + * // global error stack + * $global_stack = &PEAR_ErrorStack::singleton('MyPackage'); + * // local error stack + * $local_stack = new PEAR_ErrorStack('MyPackage'); + * + * @author Greg Beaver + * @version 1.9.2 + * @package PEAR_ErrorStack + * @category Debugging + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: ErrorStack.php 307683 2011-01-23 21:56:12Z dufuz $ + * @link http://pear.php.net/package/PEAR_ErrorStack + */ +class PEAR_ErrorStack { + /** + * Errors are stored in the order that they are pushed on the stack. + * @since 0.4alpha Errors are no longer organized by error level. + * This renders pop() nearly unusable, and levels could be more easily + * handled in a callback anyway + * @var array + * @access private + */ + var $_errors = array(); + + /** + * Storage of errors by level. + * + * Allows easy retrieval and deletion of only errors from a particular level + * @since PEAR 1.4.0dev + * @var array + * @access private + */ + var $_errorsByLevel = array(); + + /** + * Package name this error stack represents + * @var string + * @access protected + */ + var $_package; + + /** + * Determines whether a PEAR_Error is thrown upon every error addition + * @var boolean + * @access private + */ + var $_compat = false; + + /** + * If set to a valid callback, this will be used to generate the error + * message from the error code, otherwise the message passed in will be + * used + * @var false|string|array + * @access private + */ + var $_msgCallback = false; + + /** + * If set to a valid callback, this will be used to generate the error + * context for an error. For PHP-related errors, this will be a file + * and line number as retrieved from debug_backtrace(), but can be + * customized for other purposes. The error might actually be in a separate + * configuration file, or in a database query. + * @var false|string|array + * @access protected + */ + var $_contextCallback = false; + + /** + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one an PEAR_ERRORSTACK_* constant + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @var false|string|array + * @access protected + */ + var $_errorCallback = array(); + + /** + * PEAR::Log object for logging errors + * @var false|Log + * @access protected + */ + var $_logger = false; + + /** + * Error messages - designed to be overridden + * @var array + * @abstract + */ + var $_errorMsgs = array(); + + /** + * Set up a new error stack + * + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + */ + function PEAR_ErrorStack($package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false) + { + $this->_package = $package; + $this->setMessageCallback($msgCallback); + $this->setContextCallback($contextCallback); + $this->_compat = $throwPEAR_Error; + } + + /** + * Return a single error stack for this package. + * + * Note that all parameters are ignored if the stack for package $package + * has already been instantiated + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + * @param string $stackClass class to instantiate + * @static + * @return PEAR_ErrorStack + */ + function &singleton($package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false, $stackClass = 'PEAR_ErrorStack') + { + if (isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + if (!class_exists($stackClass)) { + if (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + } + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_NONCLASS, + 'exception', array('stackclass' => $stackClass), + 'stack class "%stackclass%" is not a valid class name (should be like PEAR_ErrorStack)', + false, $trace); + } + $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package] = + new $stackClass($package, $msgCallback, $contextCallback, $throwPEAR_Error); + + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + + /** + * Internal error handler for PEAR_ErrorStack class + * + * Dies if the error is an exception (and would have died anyway) + * @access private + */ + function _handleError($err) + { + if ($err['level'] == 'exception') { + $message = $err['message']; + if (isset($_SERVER['REQUEST_URI'])) { + echo '
'; + } else { + echo "\n"; + } + var_dump($err['context']); + die($message); + } + } + + /** + * Set up a PEAR::Log object for all error stacks that don't have one + * @param Log $log + * @static + */ + function setDefaultLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } elseif (is_callable($log)) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } + } + + /** + * Set up a PEAR::Log object for this error stack + * @param Log $log + */ + function setLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $this->_logger = &$log; + } elseif (is_callable($log)) { + $this->_logger = &$log; + } + } + + /** + * Set an error code => error message mapping callback + * + * This method sets the callback that can be used to generate error + * messages for any instance + * @param array|string Callback function/method + */ + function setMessageCallback($msgCallback) + { + if (!$msgCallback) { + $this->_msgCallback = array(&$this, 'getErrorMessage'); + } else { + if (is_callable($msgCallback)) { + $this->_msgCallback = $msgCallback; + } + } + } + + /** + * Get an error code => error message mapping callback + * + * This method returns the current callback that can be used to generate error + * messages + * @return array|string|false Callback function/method or false if none + */ + function getMessageCallback() + { + return $this->_msgCallback; + } + + /** + * Sets a default callback to be used by all error stacks + * + * This method sets the callback that can be used to generate error + * messages for a singleton + * @param array|string Callback function/method + * @param string Package name, or false for all packages + * @static + */ + function setDefaultCallback($callback = false, $package = false) + { + if (!is_callable($callback)) { + $callback = false; + } + $package = $package ? $package : '*'; + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$package] = $callback; + } + + /** + * Set a callback that generates context information (location of error) for an error stack + * + * This method sets the callback that can be used to generate context + * information for an error. Passing in NULL will disable context generation + * and remove the expensive call to debug_backtrace() + * @param array|string|null Callback function/method + */ + function setContextCallback($contextCallback) + { + if ($contextCallback === null) { + return $this->_contextCallback = false; + } + if (!$contextCallback) { + $this->_contextCallback = array(&$this, 'getFileLine'); + } else { + if (is_callable($contextCallback)) { + $this->_contextCallback = $contextCallback; + } + } + } + + /** + * Set an error Callback + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one of the ERRORSTACK_* constants. + * + * This functionality can be used to emulate PEAR's pushErrorHandling, and + * the PEAR_ERROR_CALLBACK mode, without affecting the integrity of + * the error stack or logging + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see popCallback() + * @param string|array $cb + */ + function pushCallback($cb) + { + array_push($this->_errorCallback, $cb); + } + + /** + * Remove a callback from the error callback stack + * @see pushCallback() + * @return array|string|false + */ + function popCallback() + { + if (!count($this->_errorCallback)) { + return false; + } + return array_pop($this->_errorCallback); + } + + /** + * Set a temporary overriding error callback for every package error stack + * + * Use this to temporarily disable all existing callbacks (can be used + * to emulate the @ operator, for instance) + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see staticPopCallback(), pushCallback() + * @param string|array $cb + * @static + */ + function staticPushCallback($cb) + { + array_push($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'], $cb); + } + + /** + * Remove a temporary overriding error callback + * @see staticPushCallback() + * @return array|string|false + * @static + */ + function staticPopCallback() + { + $ret = array_pop($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK']); + if (!is_array($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'])) { + $GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + } + return $ret; + } + + /** + * Add an error to the stack + * + * If the message generator exists, it is called with 2 parameters. + * - the current Error Stack object + * - an array that is in the same format as an error. Available indices + * are 'code', 'package', 'time', 'params', 'level', and 'context' + * + * Next, if the error should contain context information, this is + * handled by the context grabbing method. + * Finally, the error is pushed onto the proper error stack + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. If a PEAR_Error is returned, the userinfo + * property is set to the following array: + * + * + * array( + * 'code' => $code, + * 'params' => $params, + * 'package' => $this->_package, + * 'level' => $level, + * 'time' => time(), + * 'context' => $context, + * 'message' => $msg, + * //['repackage' => $err] repackaged error array/Exception class + * ); + * + * + * Normally, the previous array is returned. + */ + function push($code, $level = 'error', $params = array(), $msg = false, + $repackage = false, $backtrace = false) + { + $context = false; + // grab error context + if ($this->_contextCallback) { + if (!$backtrace) { + $backtrace = debug_backtrace(); + } + $context = call_user_func($this->_contextCallback, $code, $params, $backtrace); + } + + // save error + $time = explode(' ', microtime()); + $time = $time[1] + $time[0]; + $err = array( + 'code' => $code, + 'params' => $params, + 'package' => $this->_package, + 'level' => $level, + 'time' => $time, + 'context' => $context, + 'message' => $msg, + ); + + if ($repackage) { + $err['repackage'] = $repackage; + } + + // set up the error message, if necessary + if ($this->_msgCallback) { + $msg = call_user_func_array($this->_msgCallback, + array(&$this, $err)); + $err['message'] = $msg; + } + $push = $log = true; + $die = false; + // try the overriding callback first + $callback = $this->staticPopCallback(); + if ($callback) { + $this->staticPushCallback($callback); + } + if (!is_callable($callback)) { + // try the local callback next + $callback = $this->popCallback(); + if (is_callable($callback)) { + $this->pushCallback($callback); + } else { + // try the default callback + $callback = isset($GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package]) ? + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package] : + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK']['*']; + } + } + if (is_callable($callback)) { + switch(call_user_func($callback, $err)){ + case PEAR_ERRORSTACK_IGNORE: + return $err; + break; + case PEAR_ERRORSTACK_PUSH: + $log = false; + break; + case PEAR_ERRORSTACK_LOG: + $push = false; + break; + case PEAR_ERRORSTACK_DIE: + $die = true; + break; + // anything else returned has the same effect as pushandlog + } + } + if ($push) { + array_unshift($this->_errors, $err); + if (!isset($this->_errorsByLevel[$err['level']])) { + $this->_errorsByLevel[$err['level']] = array(); + } + $this->_errorsByLevel[$err['level']][] = &$this->_errors[0]; + } + if ($log) { + if ($this->_logger || $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']) { + $this->_log($err); + } + } + if ($die) { + die(); + } + if ($this->_compat && $push) { + return $this->raiseError($msg, $code, null, null, $err); + } + return $err; + } + + /** + * Static version of {@link push()} + * + * @param string $package Package name this error belongs to + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. see docs for {@link push()} + * @static + */ + function staticPush($package, $code, $level = 'error', $params = array(), + $msg = false, $repackage = false, $backtrace = false) + { + $s = &PEAR_ErrorStack::singleton($package); + if ($s->_contextCallback) { + if (!$backtrace) { + if (function_exists('debug_backtrace')) { + $backtrace = debug_backtrace(); + } + } + } + return $s->push($code, $level, $params, $msg, $repackage, $backtrace); + } + + /** + * Log an error using PEAR::Log + * @param array $err Error array + * @param array $levels Error level => Log constant map + * @access protected + */ + function _log($err) + { + if ($this->_logger) { + $logger = &$this->_logger; + } else { + $logger = &$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']; + } + if (is_a($logger, 'Log')) { + $levels = array( + 'exception' => PEAR_LOG_CRIT, + 'alert' => PEAR_LOG_ALERT, + 'critical' => PEAR_LOG_CRIT, + 'error' => PEAR_LOG_ERR, + 'warning' => PEAR_LOG_WARNING, + 'notice' => PEAR_LOG_NOTICE, + 'info' => PEAR_LOG_INFO, + 'debug' => PEAR_LOG_DEBUG); + if (isset($levels[$err['level']])) { + $level = $levels[$err['level']]; + } else { + $level = PEAR_LOG_INFO; + } + $logger->log($err['message'], $level, $err); + } else { // support non-standard logs + call_user_func($logger, $err); + } + } + + + /** + * Pop an error off of the error stack + * + * @return false|array + * @since 0.4alpha it is no longer possible to specify a specific error + * level to return - the last error pushed will be returned, instead + */ + function pop() + { + $err = @array_shift($this->_errors); + if (!is_null($err)) { + @array_pop($this->_errorsByLevel[$err['level']]); + if (!count($this->_errorsByLevel[$err['level']])) { + unset($this->_errorsByLevel[$err['level']]); + } + } + return $err; + } + + /** + * Pop an error off of the error stack, static method + * + * @param string package name + * @return boolean + * @since PEAR1.5.0a1 + */ + function staticPop($package) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->pop(); + } + } + + /** + * Determine whether there are any errors on the stack + * @param string|array Level name. Use to determine if any errors + * of level (string), or levels (array) have been pushed + * @return boolean + */ + function hasErrors($level = false) + { + if ($level) { + return isset($this->_errorsByLevel[$level]); + } + return count($this->_errors); + } + + /** + * Retrieve all errors since last purge + * + * @param boolean set in order to empty the error stack + * @param string level name, to return only errors of a particular severity + * @return array + */ + function getErrors($purge = false, $level = false) + { + if (!$purge) { + if ($level) { + if (!isset($this->_errorsByLevel[$level])) { + return array(); + } else { + return $this->_errorsByLevel[$level]; + } + } else { + return $this->_errors; + } + } + if ($level) { + $ret = $this->_errorsByLevel[$level]; + foreach ($this->_errorsByLevel[$level] as $i => $unused) { + // entries are references to the $_errors array + $this->_errorsByLevel[$level][$i] = false; + } + // array_filter removes all entries === false + $this->_errors = array_filter($this->_errors); + unset($this->_errorsByLevel[$level]); + return $ret; + } + $ret = $this->_errors; + $this->_errors = array(); + $this->_errorsByLevel = array(); + return $ret; + } + + /** + * Determine whether there are any errors on a single error stack, or on any error stack + * + * The optional parameter can be used to test the existence of any errors without the need of + * singleton instantiation + * @param string|false Package name to check for errors + * @param string Level name to check for a particular severity + * @return boolean + * @static + */ + function staticHasErrors($package = false, $level = false) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->hasErrors($level); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + if ($obj->hasErrors($level)) { + return true; + } + } + return false; + } + + /** + * Get a list of all errors since last purge, organized by package + * @since PEAR 1.4.0dev BC break! $level is now in the place $merge used to be + * @param boolean $purge Set to purge the error stack of existing errors + * @param string $level Set to a level name in order to retrieve only errors of a particular level + * @param boolean $merge Set to return a flat array, not organized by package + * @param array $sortfunc Function used to sort a merged array - default + * sorts by time, and should be good for most cases + * @static + * @return array + */ + function staticGetErrors($purge = false, $level = false, $merge = false, + $sortfunc = array('PEAR_ErrorStack', '_sortErrors')) + { + $ret = array(); + if (!is_callable($sortfunc)) { + $sortfunc = array('PEAR_ErrorStack', '_sortErrors'); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + $test = $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->getErrors($purge, $level); + if ($test) { + if ($merge) { + $ret = array_merge($ret, $test); + } else { + $ret[$package] = $test; + } + } + } + if ($merge) { + usort($ret, $sortfunc); + } + return $ret; + } + + /** + * Error sorting function, sorts by time + * @access private + */ + function _sortErrors($a, $b) + { + if ($a['time'] == $b['time']) { + return 0; + } + if ($a['time'] < $b['time']) { + return 1; + } + return -1; + } + + /** + * Standard file/line number/function/class context callback + * + * This function uses a backtrace generated from {@link debug_backtrace()} + * and so will not work at all in PHP < 4.3.0. The frame should + * reference the frame that contains the source of the error. + * @return array|false either array('file' => file, 'line' => line, + * 'function' => function name, 'class' => class name) or + * if this doesn't work, then false + * @param unused + * @param integer backtrace frame. + * @param array Results of debug_backtrace() + * @static + */ + function getFileLine($code, $params, $backtrace = null) + { + if ($backtrace === null) { + return false; + } + $frame = 0; + $functionframe = 1; + if (!isset($backtrace[1])) { + $functionframe = 0; + } else { + while (isset($backtrace[$functionframe]['function']) && + $backtrace[$functionframe]['function'] == 'eval' && + isset($backtrace[$functionframe + 1])) { + $functionframe++; + } + } + if (isset($backtrace[$frame])) { + if (!isset($backtrace[$frame]['file'])) { + $frame++; + } + $funcbacktrace = $backtrace[$functionframe]; + $filebacktrace = $backtrace[$frame]; + $ret = array('file' => $filebacktrace['file'], + 'line' => $filebacktrace['line']); + // rearrange for eval'd code or create function errors + if (strpos($filebacktrace['file'], '(') && + preg_match(';^(.*?)\((\d+)\) : (.*?)\\z;', $filebacktrace['file'], + $matches)) { + $ret['file'] = $matches[1]; + $ret['line'] = $matches[2] + 0; + } + if (isset($funcbacktrace['function']) && isset($backtrace[1])) { + if ($funcbacktrace['function'] != 'eval') { + if ($funcbacktrace['function'] == '__lambda_func') { + $ret['function'] = 'create_function() code'; + } else { + $ret['function'] = $funcbacktrace['function']; + } + } + } + if (isset($funcbacktrace['class']) && isset($backtrace[1])) { + $ret['class'] = $funcbacktrace['class']; + } + return $ret; + } + return false; + } + + /** + * Standard error message generation callback + * + * This method may also be called by a custom error message generator + * to fill in template values from the params array, simply + * set the third parameter to the error message template string to use + * + * The special variable %__msg% is reserved: use it only to specify + * where a message passed in by the user should be placed in the template, + * like so: + * + * Error message: %msg% - internal error + * + * If the message passed like so: + * + * + * $stack->push(ERROR_CODE, 'error', array(), 'server error 500'); + * + * + * The returned error message will be "Error message: server error 500 - + * internal error" + * @param PEAR_ErrorStack + * @param array + * @param string|false Pre-generated error message template + * @static + * @return string + */ + function getErrorMessage(&$stack, $err, $template = false) + { + if ($template) { + $mainmsg = $template; + } else { + $mainmsg = $stack->getErrorMessageTemplate($err['code']); + } + $mainmsg = str_replace('%__msg%', $err['message'], $mainmsg); + if (is_array($err['params']) && count($err['params'])) { + foreach ($err['params'] as $name => $val) { + if (is_array($val)) { + // @ is needed in case $val is a multi-dimensional array + $val = @implode(', ', $val); + } + if (is_object($val)) { + if (method_exists($val, '__toString')) { + $val = $val->__toString(); + } else { + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_OBJTOSTRING, + 'warning', array('obj' => get_class($val)), + 'object %obj% passed into getErrorMessage, but has no __toString() method'); + $val = 'Object'; + } + } + $mainmsg = str_replace('%' . $name . '%', $val, $mainmsg); + } + } + return $mainmsg; + } + + /** + * Standard Error Message Template generator from code + * @return string + */ + function getErrorMessageTemplate($code) + { + if (!isset($this->_errorMsgs[$code])) { + return '%__msg%'; + } + return $this->_errorMsgs[$code]; + } + + /** + * Set the Error Message Template array + * + * The array format must be: + *
+     * array(error code => 'message template',...)
+     * 
+ * + * Error message parameters passed into {@link push()} will be used as input + * for the error message. If the template is 'message %foo% was %bar%', and the + * parameters are array('foo' => 'one', 'bar' => 'six'), the error message returned will + * be 'message one was six' + * @return string + */ + function setErrorMessageTemplate($template) + { + $this->_errorMsgs = $template; + } + + + /** + * emulate PEAR::raiseError() + * + * @return PEAR_Error + */ + function raiseError() + { + require_once 'PEAR.php'; + $args = func_get_args(); + return call_user_func_array(array('PEAR', 'raiseError'), $args); + } +} +$stack = &PEAR_ErrorStack::singleton('PEAR_ErrorStack'); +$stack->pushCallback(array('PEAR_ErrorStack', '_handleError')); +?> diff --git a/extlib/PEAR/Exception.php b/extlib/PEAR/Exception.php index b3d75b20c9..d0327b141b 100644 --- a/extlib/PEAR/Exception.php +++ b/extlib/PEAR/Exception.php @@ -5,21 +5,15 @@ * * PHP versions 4 and 5 * - * LICENSE: This source file is subject to version 3.0 of the PHP license - * that is available through the world-wide-web at the following URI: - * http://www.php.net/license/3_0.txt. If you did not receive a copy of - * the PHP License and are unable to obtain it through the web, please - * send a note to license@php.net so we can mail you a copy immediately. - * * @category pear * @package PEAR * @author Tomas V. V. Cox * @author Hans Lellelid * @author Bertrand Mansion * @author Greg Beaver - * @copyright 1997-2008 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version CVS: $Id: Exception.php,v 1.29 2008/01/03 20:26:35 cellog Exp $ + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Exception.php 307683 2011-01-23 21:56:12Z dufuz $ * @link http://pear.php.net/package/PEAR * @since File available since Release 1.3.3 */ @@ -93,9 +87,9 @@ * @author Hans Lellelid * @author Bertrand Mansion * @author Greg Beaver - * @copyright 1997-2008 The PHP Group - * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.7.2 + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.2 * @link http://pear.php.net/package/PEAR * @since Class available since Release 1.3.3 * @@ -295,7 +289,7 @@ class PEAR_Exception extends Exception } public function getTraceSafe() - { + { if (!isset($this->_trace)) { $this->_trace = $this->getTrace(); if (empty($this->_trace)) { @@ -331,21 +325,21 @@ class PEAR_Exception extends Exception $trace = $this->getTraceSafe(); $causes = array(); $this->getCauseMessage($causes); - $html = '' . "\n"; + $html = '
' . "\n"; foreach ($causes as $i => $cause) { - $html .= '\n"; } - $html .= '' . "\n" - . '' - . '' - . '' . "\n"; + $html .= '' . "\n" + . '' + . '' + . '' . "\n"; foreach ($trace as $k => $v) { - $html .= '' + $html .= '' . '' . "\n"; } - $html .= '' + $html .= '' . '' . '' . "\n" . '
' + $html .= '
' . str_repeat('-', $i) . ' ' . $cause['class'] . ': ' . htmlspecialchars($cause['message']) . ' in ' . $cause['file'] . ' ' . 'on line ' . $cause['line'] . '' . "
Exception trace
#FunctionLocation
Exception trace
#FunctionLocation
' . $k . '
' . $k . ''; if (!empty($v['class'])) { $html .= $v['class'] . $v['type']; @@ -373,7 +367,7 @@ class PEAR_Exception extends Exception . ':' . (isset($v['line']) ? $v['line'] : 'unknown') . '
' . ($k+1) . '
' . ($k+1) . '{main} 
'; @@ -392,6 +386,4 @@ class PEAR_Exception extends Exception } return $causeMsg . $this->getTraceAsString(); } -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/extlib/PEAR5.php b/extlib/PEAR5.php new file mode 100644 index 0000000000..428606780b --- /dev/null +++ b/extlib/PEAR5.php @@ -0,0 +1,33 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: System.php 307683 2011-01-23 21:56:12Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR.php'; +require_once 'Console/Getopt.php'; + +$GLOBALS['_System_temp_files'] = array(); + +/** +* System offers cross plattform compatible system functions +* +* Static functions for different operations. Should work under +* Unix and Windows. The names and usage has been taken from its respectively +* GNU commands. The functions will return (bool) false on error and will +* trigger the error with the PHP trigger_error() function (you can silence +* the error by prefixing a '@' sign after the function call, but this +* is not recommended practice. Instead use an error handler with +* {@link set_error_handler()}). +* +* Documentation on this class you can find in: +* http://pear.php.net/manual/ +* +* Example usage: +* if (!@System::rm('-r file1 dir1')) { +* print "could not delete file1 or dir1"; +* } +* +* In case you need to to pass file names with spaces, +* pass the params as an array: +* +* System::rm(array('-r', $file1, $dir1)); +* +* @category pear +* @package System +* @author Tomas V.V. Cox +* @copyright 1997-2006 The PHP Group +* @license http://opensource.org/licenses/bsd-license.php New BSD License +* @version Release: 1.9.2 +* @link http://pear.php.net/package/PEAR +* @since Class available since Release 0.1 +* @static +*/ +class System +{ + /** + * returns the commandline arguments of a function + * + * @param string $argv the commandline + * @param string $short_options the allowed option short-tags + * @param string $long_options the allowed option long-tags + * @return array the given options and there values + * @static + * @access private + */ + function _parseArgs($argv, $short_options, $long_options = null) + { + if (!is_array($argv) && $argv !== null) { + $argv = preg_split('/\s+/', $argv, -1, PREG_SPLIT_NO_EMPTY); + } + return Console_Getopt::getopt2($argv, $short_options); + } + + /** + * Output errors with PHP trigger_error(). You can silence the errors + * with prefixing a "@" sign to the function call: @System::mkdir(..); + * + * @param mixed $error a PEAR error or a string with the error message + * @return bool false + * @static + * @access private + */ + function raiseError($error) + { + if (PEAR::isError($error)) { + $error = $error->getMessage(); + } + trigger_error($error, E_USER_WARNING); + return false; + } + + /** + * Creates a nested array representing the structure of a directory + * + * System::_dirToStruct('dir1', 0) => + * Array + * ( + * [dirs] => Array + * ( + * [0] => dir1 + * ) + * + * [files] => Array + * ( + * [0] => dir1/file2 + * [1] => dir1/file3 + * ) + * ) + * @param string $sPath Name of the directory + * @param integer $maxinst max. deep of the lookup + * @param integer $aktinst starting deep of the lookup + * @param bool $silent if true, do not emit errors. + * @return array the structure of the dir + * @static + * @access private + */ + function _dirToStruct($sPath, $maxinst, $aktinst = 0, $silent = false) + { + $struct = array('dirs' => array(), 'files' => array()); + if (($dir = @opendir($sPath)) === false) { + if (!$silent) { + System::raiseError("Could not open dir $sPath"); + } + return $struct; // XXX could not open error + } + + $struct['dirs'][] = $sPath = realpath($sPath); // XXX don't add if '.' or '..' ? + $list = array(); + while (false !== ($file = readdir($dir))) { + if ($file != '.' && $file != '..') { + $list[] = $file; + } + } + + closedir($dir); + natsort($list); + if ($aktinst < $maxinst || $maxinst == 0) { + foreach ($list as $val) { + $path = $sPath . DIRECTORY_SEPARATOR . $val; + if (is_dir($path) && !is_link($path)) { + $tmp = System::_dirToStruct($path, $maxinst, $aktinst+1, $silent); + $struct = array_merge_recursive($struct, $tmp); + } else { + $struct['files'][] = $path; + } + } + } + + return $struct; + } + + /** + * Creates a nested array representing the structure of a directory and files + * + * @param array $files Array listing files and dirs + * @return array + * @static + * @see System::_dirToStruct() + */ + function _multipleToStruct($files) + { + $struct = array('dirs' => array(), 'files' => array()); + settype($files, 'array'); + foreach ($files as $file) { + if (is_dir($file) && !is_link($file)) { + $tmp = System::_dirToStruct($file, 0); + $struct = array_merge_recursive($tmp, $struct); + } else { + if (!in_array($file, $struct['files'])) { + $struct['files'][] = $file; + } + } + } + return $struct; + } + + /** + * The rm command for removing files. + * Supports multiple files and dirs and also recursive deletes + * + * @param string $args the arguments for rm + * @return mixed PEAR_Error or true for success + * @static + * @access public + */ + function rm($args) + { + $opts = System::_parseArgs($args, 'rf'); // "f" does nothing but I like it :-) + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + foreach ($opts[0] as $opt) { + if ($opt[0] == 'r') { + $do_recursive = true; + } + } + $ret = true; + if (isset($do_recursive)) { + $struct = System::_multipleToStruct($opts[1]); + foreach ($struct['files'] as $file) { + if (!@unlink($file)) { + $ret = false; + } + } + + rsort($struct['dirs']); + foreach ($struct['dirs'] as $dir) { + if (!@rmdir($dir)) { + $ret = false; + } + } + } else { + foreach ($opts[1] as $file) { + $delete = (is_dir($file)) ? 'rmdir' : 'unlink'; + if (!@$delete($file)) { + $ret = false; + } + } + } + return $ret; + } + + /** + * Make directories. + * + * The -p option will create parent directories + * @param string $args the name of the director(y|ies) to create + * @return bool True for success + * @static + * @access public + */ + function mkDir($args) + { + $opts = System::_parseArgs($args, 'pm:'); + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + + $mode = 0777; // default mode + foreach ($opts[0] as $opt) { + if ($opt[0] == 'p') { + $create_parents = true; + } elseif ($opt[0] == 'm') { + // if the mode is clearly an octal number (starts with 0) + // convert it to decimal + if (strlen($opt[1]) && $opt[1]{0} == '0') { + $opt[1] = octdec($opt[1]); + } else { + // convert to int + $opt[1] += 0; + } + $mode = $opt[1]; + } + } + + $ret = true; + if (isset($create_parents)) { + foreach ($opts[1] as $dir) { + $dirstack = array(); + while ((!file_exists($dir) || !is_dir($dir)) && + $dir != DIRECTORY_SEPARATOR) { + array_unshift($dirstack, $dir); + $dir = dirname($dir); + } + + while ($newdir = array_shift($dirstack)) { + if (!is_writeable(dirname($newdir))) { + $ret = false; + break; + } + + if (!mkdir($newdir, $mode)) { + $ret = false; + } + } + } + } else { + foreach($opts[1] as $dir) { + if ((@file_exists($dir) || !is_dir($dir)) && !mkdir($dir, $mode)) { + $ret = false; + } + } + } + + return $ret; + } + + /** + * Concatenate files + * + * Usage: + * 1) $var = System::cat('sample.txt test.txt'); + * 2) System::cat('sample.txt test.txt > final.txt'); + * 3) System::cat('sample.txt test.txt >> final.txt'); + * + * Note: as the class use fopen, urls should work also (test that) + * + * @param string $args the arguments + * @return boolean true on success + * @static + * @access public + */ + function &cat($args) + { + $ret = null; + $files = array(); + if (!is_array($args)) { + $args = preg_split('/\s+/', $args, -1, PREG_SPLIT_NO_EMPTY); + } + + $count_args = count($args); + for ($i = 0; $i < $count_args; $i++) { + if ($args[$i] == '>') { + $mode = 'wb'; + $outputfile = $args[$i+1]; + break; + } elseif ($args[$i] == '>>') { + $mode = 'ab+'; + $outputfile = $args[$i+1]; + break; + } else { + $files[] = $args[$i]; + } + } + $outputfd = false; + if (isset($mode)) { + if (!$outputfd = fopen($outputfile, $mode)) { + $err = System::raiseError("Could not open $outputfile"); + return $err; + } + $ret = true; + } + foreach ($files as $file) { + if (!$fd = fopen($file, 'r')) { + System::raiseError("Could not open $file"); + continue; + } + while ($cont = fread($fd, 2048)) { + if (is_resource($outputfd)) { + fwrite($outputfd, $cont); + } else { + $ret .= $cont; + } + } + fclose($fd); + } + if (is_resource($outputfd)) { + fclose($outputfd); + } + return $ret; + } + + /** + * Creates temporary files or directories. This function will remove + * the created files when the scripts finish its execution. + * + * Usage: + * 1) $tempfile = System::mktemp("prefix"); + * 2) $tempdir = System::mktemp("-d prefix"); + * 3) $tempfile = System::mktemp(); + * 4) $tempfile = System::mktemp("-t /var/tmp prefix"); + * + * prefix -> The string that will be prepended to the temp name + * (defaults to "tmp"). + * -d -> A temporary dir will be created instead of a file. + * -t -> The target dir where the temporary (file|dir) will be created. If + * this param is missing by default the env vars TMP on Windows or + * TMPDIR in Unix will be used. If these vars are also missing + * c:\windows\temp or /tmp will be used. + * + * @param string $args The arguments + * @return mixed the full path of the created (file|dir) or false + * @see System::tmpdir() + * @static + * @access public + */ + function mktemp($args = null) + { + static $first_time = true; + $opts = System::_parseArgs($args, 't:d'); + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + + foreach ($opts[0] as $opt) { + if ($opt[0] == 'd') { + $tmp_is_dir = true; + } elseif ($opt[0] == 't') { + $tmpdir = $opt[1]; + } + } + + $prefix = (isset($opts[1][0])) ? $opts[1][0] : 'tmp'; + if (!isset($tmpdir)) { + $tmpdir = System::tmpdir(); + } + + if (!System::mkDir(array('-p', $tmpdir))) { + return false; + } + + $tmp = tempnam($tmpdir, $prefix); + if (isset($tmp_is_dir)) { + unlink($tmp); // be careful possible race condition here + if (!mkdir($tmp, 0700)) { + return System::raiseError("Unable to create temporary directory $tmpdir"); + } + } + + $GLOBALS['_System_temp_files'][] = $tmp; + if (isset($tmp_is_dir)) { + //$GLOBALS['_System_temp_files'][] = dirname($tmp); + } + + if ($first_time) { + PEAR::registerShutdownFunc(array('System', '_removeTmpFiles')); + $first_time = false; + } + + return $tmp; + } + + /** + * Remove temporary files created my mkTemp. This function is executed + * at script shutdown time + * + * @static + * @access private + */ + function _removeTmpFiles() + { + if (count($GLOBALS['_System_temp_files'])) { + $delete = $GLOBALS['_System_temp_files']; + array_unshift($delete, '-r'); + System::rm($delete); + $GLOBALS['_System_temp_files'] = array(); + } + } + + /** + * Get the path of the temporal directory set in the system + * by looking in its environments variables. + * Note: php.ini-recommended removes the "E" from the variables_order setting, + * making unavaible the $_ENV array, that s why we do tests with _ENV + * + * @static + * @return string The temporary directory on the system + */ + function tmpdir() + { + if (OS_WINDOWS) { + if ($var = isset($_ENV['TMP']) ? $_ENV['TMP'] : getenv('TMP')) { + return $var; + } + if ($var = isset($_ENV['TEMP']) ? $_ENV['TEMP'] : getenv('TEMP')) { + return $var; + } + if ($var = isset($_ENV['USERPROFILE']) ? $_ENV['USERPROFILE'] : getenv('USERPROFILE')) { + return $var; + } + if ($var = isset($_ENV['windir']) ? $_ENV['windir'] : getenv('windir')) { + return $var; + } + return getenv('SystemRoot') . '\temp'; + } + if ($var = isset($_ENV['TMPDIR']) ? $_ENV['TMPDIR'] : getenv('TMPDIR')) { + return $var; + } + return realpath('/tmp'); + } + + /** + * The "which" command (show the full path of a command) + * + * @param string $program The command to search for + * @param mixed $fallback Value to return if $program is not found + * + * @return mixed A string with the full path or false if not found + * @static + * @author Stig Bakken + */ + function which($program, $fallback = false) + { + // enforce API + if (!is_string($program) || '' == $program) { + return $fallback; + } + + // full path given + if (basename($program) != $program) { + $path_elements[] = dirname($program); + $program = basename($program); + } else { + // Honor safe mode + if (!ini_get('safe_mode') || !$path = ini_get('safe_mode_exec_dir')) { + $path = getenv('PATH'); + if (!$path) { + $path = getenv('Path'); // some OSes are just stupid enough to do this + } + } + $path_elements = explode(PATH_SEPARATOR, $path); + } + + if (OS_WINDOWS) { + $exe_suffixes = getenv('PATHEXT') + ? explode(PATH_SEPARATOR, getenv('PATHEXT')) + : array('.exe','.bat','.cmd','.com'); + // allow passing a command.exe param + if (strpos($program, '.') !== false) { + array_unshift($exe_suffixes, ''); + } + // is_executable() is not available on windows for PHP4 + $pear_is_executable = (function_exists('is_executable')) ? 'is_executable' : 'is_file'; + } else { + $exe_suffixes = array(''); + $pear_is_executable = 'is_executable'; + } + + foreach ($exe_suffixes as $suff) { + foreach ($path_elements as $dir) { + $file = $dir . DIRECTORY_SEPARATOR . $program . $suff; + if (@$pear_is_executable($file)) { + return $file; + } + } + } + return $fallback; + } + + /** + * The "find" command + * + * Usage: + * + * System::find($dir); + * System::find("$dir -type d"); + * System::find("$dir -type f"); + * System::find("$dir -name *.php"); + * System::find("$dir -name *.php -name *.htm*"); + * System::find("$dir -maxdepth 1"); + * + * Params implmented: + * $dir -> Start the search at this directory + * -type d -> return only directories + * -type f -> return only files + * -maxdepth -> max depth of recursion + * -name -> search pattern (bash style). Multiple -name param allowed + * + * @param mixed Either array or string with the command line + * @return array Array of found files + * @static + * + */ + function find($args) + { + if (!is_array($args)) { + $args = preg_split('/\s+/', $args, -1, PREG_SPLIT_NO_EMPTY); + } + $dir = realpath(array_shift($args)); + if (!$dir) { + return array(); + } + $patterns = array(); + $depth = 0; + $do_files = $do_dirs = true; + $args_count = count($args); + for ($i = 0; $i < $args_count; $i++) { + switch ($args[$i]) { + case '-type': + if (in_array($args[$i+1], array('d', 'f'))) { + if ($args[$i+1] == 'd') { + $do_files = false; + } else { + $do_dirs = false; + } + } + $i++; + break; + case '-name': + $name = preg_quote($args[$i+1], '#'); + // our magic characters ? and * have just been escaped, + // so now we change the escaped versions to PCRE operators + $name = strtr($name, array('\?' => '.', '\*' => '.*')); + $patterns[] = '('.$name.')'; + $i++; + break; + case '-maxdepth': + $depth = $args[$i+1]; + break; + } + } + $path = System::_dirToStruct($dir, $depth, 0, true); + if ($do_files && $do_dirs) { + $files = array_merge($path['files'], $path['dirs']); + } elseif ($do_dirs) { + $files = $path['dirs']; + } else { + $files = $path['files']; + } + if (count($patterns)) { + $dsq = preg_quote(DIRECTORY_SEPARATOR, '#'); + $pattern = '#(^|'.$dsq.')'.implode('|', $patterns).'($|'.$dsq.')#'; + $ret = array(); + $files_count = count($files); + for ($i = 0; $i < $files_count; $i++) { + // only search in the part of the file below the current directory + $filepart = basename($files[$i]); + if (preg_match($pattern, $filepart)) { + $ret[] = $files[$i]; + } + } + return $ret; + } + return $files; + } +} \ No newline at end of file diff --git a/js/util.js b/js/util.js index 98fb6c9850..ee5cbefd88 100644 --- a/js/util.js +++ b/js/util.js @@ -1590,6 +1590,18 @@ var SN = { // StatusNet SN.U.FormXHR($(this)); return false; }); + $('form.ajax input[type=submit]').live('click', function() { + // Some forms rely on knowing which submit button was clicked. + // Save a hidden input field which'll be picked up during AJAX + // submit... + var button = $(this); + var form = button.closest('form'); + form.find('.hidden-submit-button').remove(); + $('') + .attr('name', button.attr('name')) + .val(button.val()) + .appendTo(form); + }); }, /** diff --git a/js/util.min.js b/js/util.min.js index cfc06cda5a..64adcd478c 100644 --- a/js/util.min.js +++ b/js/util.min.js @@ -1 +1 @@ -var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find(".notice_data-text:first");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find(".notice_data-text:first").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(d,e,c){var b=null;if(d.responseXML){b=$("#error",d.responseXML).text()}alert(b||c||e);a.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled)},success:function(b,c){if(typeof($("form",b)[0])!="undefined"){form_new=document._importNode($("form",b)[0],true);a.replaceWith(form_new)}else{if(typeof($("p",b)[0])!="undefined"){a.replaceWith(document._importNode($("p",b)[0],true))}else{alert("Unknown error.")}}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append('');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('

').addClass(d).text(e))};var a=function(){b.find(".form_response").remove()};b.ajaxForm({dataType:"xml",timeout:"60000",beforeSend:function(d){if(b.find(".notice_data-text:first").val()==""){b.addClass(SN.C.S.Warning);return false}b.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled);SN.U.normalizeGeoData(b);return true},error:function(f,g,e){b.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled,SN.C.S.Disabled);a();if(g=="timeout"){c("error","Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.")}else{var d=SN.U.GetResponseXML(f);if($("."+SN.C.S.Error,d).length>0){b.append(document._importNode($("."+SN.C.S.Error,d)[0],true))}else{if(parseInt(f.status)===0||jQuery.inArray(parseInt(f.status),SN.C.I.HTTP20x30x)>=0){b.resetForm().find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}else{c("error","(Sorry! We had trouble sending your notice ("+f.status+" "+f.statusText+"). Please report the problem to the site administrator if this happens again.")}}}},success:function(i,f){a();var o=$("#"+SN.C.S.Error,i);if(o.length>0){c("error",o.text())}else{if($("body")[0].id=="bookmarklet"){self.close()}var d=$("#"+SN.C.S.CommandResult,i);if(d.length>0){c("success",d.text())}else{var n=document._importNode($("li",i)[0],true);var j=$("#notices_primary .notices:first");var l=b.closest("li.notice-reply");if(l.length>0){var k=b.closest(".threaded-replies");var m=k.find(".notice-reply-placeholder");l.remove();var e=$(n).attr("id");if($("#"+e).length==0){$(n).insertBefore(m)}else{}m.show()}else{if(j.length>0&&SN.U.belongsOnTimeline(n)){if($("#"+n.id).length===0){var h=b.find("[name=inreplyto]").val();var g="#notices_primary #notice-"+h;if($("body")[0].id=="conversation"){if(h.length>0&&$(g+" .notices").length<1){$(g).append('
    ')}$($(g+" .notices")[0]).append(n)}else{j.prepend(n)}$("#"+n.id).css({display:"none"}).fadeIn(2500);SN.U.NoticeWithAttachment($("#"+n.id));SN.U.switchInputFormTab("placeholder")}}else{c("success",$("title",i).text())}}}b.resetForm();b.find("[name=inreplyto]").val("");b.find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}},complete:function(d,e){b.removeClass(SN.C.S.Processing).find(".submit").removeAttr(SN.C.S.Disabled).removeClass(SN.C.S.Disabled);b.find("[name=lat]").val(SN.C.I.NoticeDataGeo.NLat);b.find("[name=lon]").val(SN.C.I.NoticeDataGeo.NLon);b.find("[name=location_ns]").val(SN.C.I.NoticeDataGeo.NLNS);b.find("[name=location_id]").val(SN.C.I.NoticeDataGeo.NLID);b.find("[name=notice_data-geo]").attr("checked",SN.C.I.NoticeDataGeo.NDG)}})},FormProfileSearchXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:a.attr("action"),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(d,f){var b=$("#profile_search_results");if(typeof($("ul",d)[0])!="undefined"){var c=document._importNode($("ul",d)[0],true);b.replaceWith(c)}else{var e=$("
  • ").append(document._importNode($("p",d)[0],true));b.html(e)}a.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,false)}})},FormPeopletagsXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:a.attr("action"),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(d,e){var c=a.parents(".entity_tags");if(typeof($(".entity_tags",d)[0])!="undefined"){var b=document._importNode($(".entity_tags",d)[0],true);$(b).find(".editable").append($('').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
    ');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$("").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
    ").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("data-api");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('
    ');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change();return false});e.append(h)}var b;if(c){b=$("").attr("href",c)}else{b=$("")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".vcard.author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");if(a=="placeholder"){$("#input_form_nav_status").addClass("current")}else{$("#input_form_nav_"+a).addClass("current")}$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current").find(".ajax-notice").each(function(){var b=$(this);SN.Init.NoticeFormSetup(b)}).find("textarea:first").focus()}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("#input_form_placeholder input.placeholder").focus(function(){SN.U.switchInputFormTab("status")});$("body").bind("click",function(g){var d=$("#content .input_forms div.current");if(d.length>0){if($("#content .input_forms").has(g.target).length==0){var a=d.find('textarea, input[type=text], input[type=""]');var c=false;a.each(function(){c=c||$(this).val()});if(!c){SN.U.switchInputFormTab("placeholder")}}}var b=$("li.notice-reply");if(b.length>0){var f=$(g.target);b.each(function(){var j=$(this);if(j.has(g.target).length==0){var h=j.find(".notice_data-text:first");var i=$.trim(h.val());if(i==""||i==h.data("initialText")){var e=j.closest("li.notice");j.remove();e.find("li.notice-reply-placeholder").show()}}})}})}},NoticeFormSetup:function(a){if(!a.data("NoticeFormSetup")){SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a);a.data("NoticeFormSetup",true)}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){$(".form_user_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_join").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_leave").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_nudge").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_add_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_remove_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});SN.U.NewDirectMessage()}},ProfileSearch:function(){if($("body.user_in").length>0){$(".form_peopletag_edit_user_search input.submit").live("click",function(){SN.U.FormProfileSearchXHR($(this).parents("form"));return false})}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},PeopletagAutocomplete:function(){$(".form_tag_user #tags").tagInput({tags:SN.C.PtagACData,tagSeparator:" ",animate:false,formatLine:function(d,g,c,f){var a=""+g.tag.substring(0,c.length)+""+g.tag.substring(c.length);var b=$("
    ").addClass("mode-"+g.mode);b.append($("
    "+a+" "+g.mode+"
    "));if(g.freq){b.append("
    "+g.freq+"
    ")}return b}})},PeopleTags:function(){$(".user_profile_tags .editable").append($('').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
    ');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$("").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
    ").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("data-api");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('
    ');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change();return false});e.append(h)}var b;if(c){b=$("").attr("href",c)}else{b=$("")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".vcard.author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");if(a=="placeholder"){$("#input_form_nav_status").addClass("current")}else{$("#input_form_nav_"+a).addClass("current")}$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current").find(".ajax-notice").each(function(){var b=$(this);SN.Init.NoticeFormSetup(b)}).find("textarea:first").focus()}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("#input_form_placeholder input.placeholder").focus(function(){SN.U.switchInputFormTab("status")});$("body").bind("click",function(g){var d=$("#content .input_forms div.current");if(d.length>0){if($("#content .input_forms").has(g.target).length==0){var a=d.find('textarea, input[type=text], input[type=""]');var c=false;a.each(function(){c=c||$(this).val()});if(!c){SN.U.switchInputFormTab("placeholder")}}}var b=$("li.notice-reply");if(b.length>0){var f=$(g.target);b.each(function(){var j=$(this);if(j.has(g.target).length==0){var h=j.find(".notice_data-text:first");var i=$.trim(h.val());if(i==""||i==h.data("initialText")){var e=j.closest("li.notice");j.remove();e.find("li.notice-reply-placeholder").show()}}})}})}},NoticeFormSetup:function(a){if(!a.data("NoticeFormSetup")){SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a);a.data("NoticeFormSetup",true)}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){$(".form_user_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_join").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_group_leave").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_nudge").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_subscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_peopletag_unsubscribe").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_add_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});$(".form_user_remove_peopletag").live("click",function(){SN.U.FormXHR($(this));return false});SN.U.NewDirectMessage()}},ProfileSearch:function(){if($("body.user_in").length>0){$(".form_peopletag_edit_user_search input.submit").live("click",function(){SN.U.FormProfileSearchXHR($(this).parents("form"));return false})}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},PeopletagAutocomplete:function(){$(".form_tag_user #tags").tagInput({tags:SN.C.PtagACData,tagSeparator:" ",animate:false,formatLine:function(d,g,c,f){var a=""+g.tag.substring(0,c.length)+""+g.tag.substring(c.length);var b=$("
    ").addClass("mode-"+g.mode);b.append($("
    "+a+" "+g.mode+"
    "));if(g.freq){b.append("
    "+g.freq+"
    ")}return b}})},PeopleTags:function(){$(".user_profile_tags .editable").append($('