diff --git a/EVENTS.txt b/EVENTS.txt
index 6719ba737a..f675c199a0 100644
--- a/EVENTS.txt
+++ b/EVENTS.txt
@@ -1057,3 +1057,43 @@ StartCloseNoticeListItemElement: Before the closing of a notice list eleme
EndCloseNoticeListItemElement: After the closing of a notice list element
- $nli: The notice list item being shown
+
+StartGroupEditFormData: Beginning the group edit form entries
+- $form: The form widget being shown
+
+EndGroupEditFormData: Ending the group edit form entries
+- $form: The form widget being shown
+
+StartGroupSave: After initializing but before saving a group
+- &$group: group about to be saved
+
+EndGroupSave: After saving a group, aliases, and first member
+- $group: group that was saved
+
+StartInterpretCommand: Before running a command
+- $cmd: First word in the string, 'foo' in 'foo argument'
+- $arg: Argument, if any, like 'argument' in 'foo argument'
+- $user: User who issued the command
+- &$result: Resulting command; you can set this!
+
+EndInterpretCommand: Before running a command
+- $cmd: First word in the string, 'foo' in 'foo argument'
+- $arg: Argument, if any, like 'argument' in 'foo argument'
+- $user: User who issued the command
+- $result: Resulting command
+
+StartGroupActionsList: Start the list of actions on a group profile page (after
, before first
)
+- $action: action being executed (for output and params)
+- $group: group for the page
+
+EndGroupActionsList: End the list of actions on a group profile page (before
, after last )
+- $action: action being executed (for output and params)
+- $group: group for the page
+
+StartGroupProfileElements: Start showing stuff about the group on its profile page
+- $action: action being executed (for output and params)
+- $group: group for the page
+
+EndGroupProfileElements: Start showing stuff about the group on its profile page
+- $action: action being executed (for output and params)
+- $group: group for the page
diff --git a/README b/README
index d972bf5676..f290ef4244 100644
--- a/README
+++ b/README
@@ -1279,7 +1279,7 @@ biolimit: max character length of bio; 0 means no limit; null means to use
backup: whether users can backup their own profiles. Defaults to true.
restore: whether users can restore their profiles from backup files. Defaults
to true.
-delete: whether users can delete their own accounts. Defaults to true.
+delete: whether users can delete their own accounts. Defaults to false.
move: whether users can move their accounts to another server. Defaults
to true.
@@ -1572,6 +1572,23 @@ proxy_user: Username to use for authenticating to the HTTP proxy. Default null.
proxy_password: Password to use for authenticating to the HTTP proxy. Default null.
proxy_auth_scheme: Scheme to use for authenticating to the HTTP proxy. Default null.
+plugins
+-------
+
+default: associative array mapping plugin name to array of arguments. To disable
+ a default plugin, unset its value in this array.
+locale_path: path for finding plugin locale files. In the plugin's directory
+ by default.
+server: Server to find static files for a plugin when the page is plain old HTTP.
+ Defaults to site/server (same as pages). Use this to move plugin CSS and
+ JS files to a CDN.
+sslserver: Server to find static files for a plugin when the page is HTTPS. Defaults
+ to site/server (same as pages). Use this to move plugin CSS and JS files
+ to a CDN.
+path: Path to the plugin files. defaults to site/path + '/plugins/'. Expects that
+ each plugin will have a subdirectory at plugins/NameOfPlugin. Change this
+ if you're using a CDN.
+
Plugins
=======
diff --git a/actions/editgroup.php b/actions/editgroup.php
index ab4dbb2836..0e04170051 100644
--- a/actions/editgroup.php
+++ b/actions/editgroup.php
@@ -177,116 +177,121 @@ class EditgroupAction extends GroupDesignAction
return;
}
- $nickname = Nickname::normalize($this->trimmed('nickname'));
- $fullname = $this->trimmed('fullname');
- $homepage = $this->trimmed('homepage');
- $description = $this->trimmed('description');
- $location = $this->trimmed('location');
- $aliasstring = $this->trimmed('aliases');
+ if (Event::handle('StartGroupSaveForm', array($this))) {
- if ($this->nicknameExists($nickname)) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Nickname already in use. Try another one.'));
- return;
- } else if (!User_group::allowedNickname($nickname)) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Not a valid nickname.'));
- return;
- } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
- !Validate::uri($homepage,
- array('allowed_schemes' =>
- array('http', 'https')))) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Homepage is not a valid URL.'));
- return;
- } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Full name is too long (maximum 255 characters).'));
- return;
- } else if (User_group::descriptionTooLong($description)) {
- $this->showForm(sprintf(
+ $nickname = Nickname::normalize($this->trimmed('nickname'));
+ $fullname = $this->trimmed('fullname');
+ $homepage = $this->trimmed('homepage');
+ $description = $this->trimmed('description');
+ $location = $this->trimmed('location');
+ $aliasstring = $this->trimmed('aliases');
+
+ if ($this->nicknameExists($nickname)) {
// TRANS: Group edit form validation error.
- _m('Description is too long (maximum %d character).',
- 'Description is too long (maximum %d characters).',
- User_group::maxDescription()),
+ $this->showForm(_('Nickname already in use. Try another one.'));
+ return;
+ } else if (!User_group::allowedNickname($nickname)) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(_('Not a valid nickname.'));
+ return;
+ } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
+ !Validate::uri($homepage,
+ array('allowed_schemes' =>
+ array('http', 'https')))) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(_('Homepage is not a valid URL.'));
+ return;
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(_('Full name is too long (maximum 255 characters).'));
+ return;
+ } else if (User_group::descriptionTooLong($description)) {
+ $this->showForm(sprintf(
+ // TRANS: Group edit form validation error.
+ _m('Description is too long (maximum %d character).',
+ 'Description is too long (maximum %d characters).',
+ User_group::maxDescription()),
User_group::maxDescription()));
- return;
- } else if (!is_null($location) && mb_strlen($location) > 255) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Location is too long (maximum 255 characters).'));
- return;
- }
-
- if (!empty($aliasstring)) {
- $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
- } else {
- $aliases = array();
- }
-
- if (count($aliases) > common_config('group', 'maxaliases')) {
- // TRANS: Group edit form validation error.
- // TRANS: %d is the maximum number of allowed aliases.
- $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
- 'Too many aliases! Maximum %d allowed.',
- common_config('group', 'maxaliases')),
- common_config('group', 'maxaliases')));
- return;
- }
-
- foreach ($aliases as $alias) {
- if (!Nickname::isValid($alias)) {
+ return;
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
// TRANS: Group edit form validation error.
- $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ $this->showForm(_('Location is too long (maximum 255 characters).'));
return;
}
- if ($this->nicknameExists($alias)) {
+
+ if (!empty($aliasstring)) {
+ $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
+ } else {
+ $aliases = array();
+ }
+
+ if (count($aliases) > common_config('group', 'maxaliases')) {
// TRANS: Group edit form validation error.
- $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
- $alias));
+ // TRANS: %d is the maximum number of allowed aliases.
+ $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
+ 'Too many aliases! Maximum %d allowed.',
+ common_config('group', 'maxaliases')),
+ common_config('group', 'maxaliases')));
return;
}
- // XXX assumes alphanum nicknames
- if (strcmp($alias, $nickname) == 0) {
- // TRANS: Group edit form validation error.
- $this->showForm(_('Alias can\'t be the same as nickname.'));
- return;
+
+ foreach ($aliases as $alias) {
+ if (!Nickname::isValid($alias)) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ return;
+ }
+ if ($this->nicknameExists($alias)) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
+ $alias));
+ return;
+ }
+ // XXX assumes alphanum nicknames
+ if (strcmp($alias, $nickname) == 0) {
+ // TRANS: Group edit form validation error.
+ $this->showForm(_('Alias can\'t be the same as nickname.'));
+ return;
+ }
}
+
+ $this->group->query('BEGIN');
+
+ $orig = clone($this->group);
+
+ $this->group->nickname = $nickname;
+ $this->group->fullname = $fullname;
+ $this->group->homepage = $homepage;
+ $this->group->description = $description;
+ $this->group->location = $location;
+ $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname));
+
+ $result = $this->group->update($orig);
+
+ if (!$result) {
+ common_log_db_error($this->group, 'UPDATE', __FILE__);
+ // TRANS: Server error displayed when editing a group fails.
+ $this->serverError(_('Could not update group.'));
+ }
+
+ $result = $this->group->setAliases($aliases);
+
+ if (!$result) {
+ // TRANS: Server error displayed when group aliases could not be added.
+ $this->serverError(_('Could not create aliases.'));
+ }
+
+ if ($nickname != $orig->nickname) {
+ common_log(LOG_INFO, "Saving local group info.");
+ $local = Local_group::staticGet('group_id', $this->group->id);
+ $local->setNickname($nickname);
+ }
+
+ $this->group->query('COMMIT');
+
+ Event::handle('EndGroupSaveForm', array($this));
}
- $this->group->query('BEGIN');
-
- $orig = clone($this->group);
-
- $this->group->nickname = $nickname;
- $this->group->fullname = $fullname;
- $this->group->homepage = $homepage;
- $this->group->description = $description;
- $this->group->location = $location;
- $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname));
-
- $result = $this->group->update($orig);
-
- if (!$result) {
- common_log_db_error($this->group, 'UPDATE', __FILE__);
- // TRANS: Server error displayed when editing a group fails.
- $this->serverError(_('Could not update group.'));
- }
-
- $result = $this->group->setAliases($aliases);
-
- if (!$result) {
- // TRANS: Server error displayed when group aliases could not be added.
- $this->serverError(_('Could not create aliases.'));
- }
-
- if ($nickname != $orig->nickname) {
- common_log(LOG_INFO, "Saving local group info.");
- $local = Local_group::staticGet('group_id', $this->group->id);
- $local->setNickname($nickname);
- }
-
- $this->group->query('COMMIT');
-
if ($this->group->nickname != $orig->nickname) {
common_redirect(common_local_url('editgroup',
array('nickname' => $nickname)),
diff --git a/actions/inbox.php b/actions/inbox.php
index 3a50f4964f..6ab58f9751 100644
--- a/actions/inbox.php
+++ b/actions/inbox.php
@@ -90,18 +90,9 @@ class InboxAction extends MailboxAction
}
}
- /**
- * Returns the profile we want to show with the message
- *
- * For inboxes, we show the sender; for outboxes, the recipient.
- *
- * @param Message $message The message to get the profile for
- *
- * @return Profile The profile that matches the message
- */
- function getMessageProfile($message)
+ function getMessageList($message)
{
- return $message->getFrom();
+ return new InboxMessageList($this, $message);
}
/**
@@ -115,3 +106,24 @@ class InboxAction extends MailboxAction
return _('This is your inbox, which lists your incoming private messages.');
}
}
+
+class InboxMessageList extends MessageList
+{
+ function newItem($message)
+ {
+ return new InboxMessageListItem($this->out, $message);
+ }
+}
+
+class InboxMessageListItem extends MessageListItem
+{
+ /**
+ * Returns the profile we want to show with the message
+ *
+ * @return Profile The profile that matches the message
+ */
+ function getMessageProfile()
+ {
+ return $this->message->getFrom();
+ }
+}
\ No newline at end of file
diff --git a/actions/newgroup.php b/actions/newgroup.php
index 53c95d03f0..9682b875cb 100644
--- a/actions/newgroup.php
+++ b/actions/newgroup.php
@@ -120,103 +120,109 @@ class NewgroupAction extends Action
function trySave()
{
- try {
- $nickname = Nickname::normalize($this->trimmed('nickname'));
- } catch (NicknameException $e) {
- $this->showForm($e->getMessage());
- }
- $fullname = $this->trimmed('fullname');
- $homepage = $this->trimmed('homepage');
- $description = $this->trimmed('description');
- $location = $this->trimmed('location');
- $aliasstring = $this->trimmed('aliases');
+ if (Event::handle('StartGroupSaveForm', array($this))) {
+ try {
+ $nickname = Nickname::normalize($this->trimmed('nickname'));
+ } catch (NicknameException $e) {
+ $this->showForm($e->getMessage());
+ }
+ $fullname = $this->trimmed('fullname');
+ $homepage = $this->trimmed('homepage');
+ $description = $this->trimmed('description');
+ $location = $this->trimmed('location');
+ $aliasstring = $this->trimmed('aliases');
- if ($this->nicknameExists($nickname)) {
- // TRANS: Group create form validation error.
- $this->showForm(_('Nickname already in use. Try another one.'));
- return;
- } else if (!User_group::allowedNickname($nickname)) {
- // TRANS: Group create form validation error.
- $this->showForm(_('Not a valid nickname.'));
- return;
- } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
- !Validate::uri($homepage,
- array('allowed_schemes' =>
- array('http', 'https')))) {
- // TRANS: Group create form validation error.
- $this->showForm(_('Homepage is not a valid URL.'));
- return;
- } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
- // TRANS: Group create form validation error.
- $this->showForm(_('Full name is too long (maximum 255 characters).'));
- return;
- } else if (User_group::descriptionTooLong($description)) {
- // TRANS: Group create form validation error.
- // TRANS: %d is the maximum number of allowed characters.
- $this->showForm(sprintf(_m('Description is too long (maximum %d character).',
- 'Description is too long (maximum %d characters).',
- User_group::maxDescription()),
- User_group::maxDescription()));
- return;
- } else if (!is_null($location) && mb_strlen($location) > 255) {
- // TRANS: Group create form validation error.
- $this->showForm(_('Location is too long (maximum 255 characters).'));
- return;
- }
-
- if (!empty($aliasstring)) {
- $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
- } else {
- $aliases = array();
- }
-
- if (count($aliases) > common_config('group', 'maxaliases')) {
- // TRANS: Group create form validation error.
- // TRANS: %d is the maximum number of allowed aliases.
- $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
- 'Too many aliases! Maximum %d allowed.',
- common_config('group', 'maxaliases')),
- common_config('group', 'maxaliases')));
- return;
- }
-
- foreach ($aliases as $alias) {
- if (!Nickname::isValid($alias)) {
+ if ($this->nicknameExists($nickname)) {
// TRANS: Group create form validation error.
- // TRANS: %s is the invalid alias.
- $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ $this->showForm(_('Nickname already in use. Try another one.'));
return;
- }
- if ($this->nicknameExists($alias)) {
- // TRANS: Group create form validation error. %s is the already used alias.
- $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
- $alias));
- return;
- }
- // XXX assumes alphanum nicknames
- if (strcmp($alias, $nickname) == 0) {
+ } else if (!User_group::allowedNickname($nickname)) {
// TRANS: Group create form validation error.
- $this->showForm(_('Alias cannot be the same as nickname.'));
+ $this->showForm(_('Not a valid nickname.'));
+ return;
+ } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
+ !Validate::uri($homepage,
+ array('allowed_schemes' =>
+ array('http', 'https')))) {
+ // TRANS: Group create form validation error.
+ $this->showForm(_('Homepage is not a valid URL.'));
+ return;
+ } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
+ // TRANS: Group create form validation error.
+ $this->showForm(_('Full name is too long (maximum 255 characters).'));
+ return;
+ } else if (User_group::descriptionTooLong($description)) {
+ // TRANS: Group create form validation error.
+ // TRANS: %d is the maximum number of allowed characters.
+ $this->showForm(sprintf(_m('Description is too long (maximum %d character).',
+ 'Description is too long (maximum %d characters).',
+ User_group::maxDescription()),
+ User_group::maxDescription()));
+ return;
+ } else if (!is_null($location) && mb_strlen($location) > 255) {
+ // TRANS: Group create form validation error.
+ $this->showForm(_('Location is too long (maximum 255 characters).'));
return;
}
+
+ if (!empty($aliasstring)) {
+ $aliases = array_map('common_canonical_nickname', array_unique(preg_split('/[\s,]+/', $aliasstring)));
+ } else {
+ $aliases = array();
+ }
+
+ if (count($aliases) > common_config('group', 'maxaliases')) {
+ // TRANS: Group create form validation error.
+ // TRANS: %d is the maximum number of allowed aliases.
+ $this->showForm(sprintf(_m('Too many aliases! Maximum %d allowed.',
+ 'Too many aliases! Maximum %d allowed.',
+ common_config('group', 'maxaliases')),
+ common_config('group', 'maxaliases')));
+ return;
+ }
+
+ foreach ($aliases as $alias) {
+ if (!Nickname::isValid($alias)) {
+ // TRANS: Group create form validation error.
+ // TRANS: %s is the invalid alias.
+ $this->showForm(sprintf(_('Invalid alias: "%s"'), $alias));
+ return;
+ }
+ if ($this->nicknameExists($alias)) {
+ // TRANS: Group create form validation error. %s is the already used alias.
+ $this->showForm(sprintf(_('Alias "%s" already in use. Try another one.'),
+ $alias));
+ return;
+ }
+ // XXX assumes alphanum nicknames
+ if (strcmp($alias, $nickname) == 0) {
+ // TRANS: Group create form validation error.
+ $this->showForm(_('Alias cannot be the same as nickname.'));
+ return;
+ }
+ }
+
+ $cur = common_current_user();
+
+ // Checked in prepare() above
+
+ assert(!is_null($cur));
+
+ $group = User_group::register(array('nickname' => $nickname,
+ 'fullname' => $fullname,
+ 'homepage' => $homepage,
+ 'description' => $description,
+ 'location' => $location,
+ 'aliases' => $aliases,
+ 'userid' => $cur->id,
+ 'local' => true));
+
+ $this->group = $group;
+
+ Event::handle('EndGroupSaveForm', array($this));
+
+ common_redirect($group->homeUrl(), 303);
}
-
- $cur = common_current_user();
-
- // Checked in prepare() above
-
- assert(!is_null($cur));
-
- $group = User_group::register(array('nickname' => $nickname,
- 'fullname' => $fullname,
- 'homepage' => $homepage,
- 'description' => $description,
- 'location' => $location,
- 'aliases' => $aliases,
- 'userid' => $cur->id,
- 'local' => true));
-
- common_redirect($group->homeUrl(), 303);
}
function nicknameExists($nickname)
diff --git a/actions/outbox.php b/actions/outbox.php
index b81d4b9d0d..cad19bba24 100644
--- a/actions/outbox.php
+++ b/actions/outbox.php
@@ -88,21 +88,9 @@ class OutboxAction extends MailboxAction
}
}
- /**
- * returns the profile we want to show with the message
- *
- * For outboxes, we show the recipient.
- *
- * @param Message $message The message to get the profile for
- *
- * @return Profile The profile of the message recipient
- *
- * @see MailboxAction::getMessageProfile()
- */
-
- function getMessageProfile($message)
+ function getMessageList($message)
{
- return $message->getTo();
+ return new OutboxMessageList($this, $message);
}
/**
@@ -116,3 +104,24 @@ class OutboxAction extends MailboxAction
return _('This is your outbox, which lists private messages you have sent.');
}
}
+
+class OutboxMessageList extends MessageList
+{
+ function newItem($message)
+ {
+ return new OutboxMessageListItem($this->out, $message);
+ }
+}
+
+class OutboxMessageListItem extends MessageListItem
+{
+ /**
+ * Returns the profile we want to show with the message
+ *
+ * @return Profile The profile that matches the message
+ */
+ function getMessageProfile()
+ {
+ return $this->message->getTo();
+ }
+}
\ No newline at end of file
diff --git a/actions/showgroup.php b/actions/showgroup.php
index f38cd420ac..2806944452 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -181,6 +181,7 @@ class ShowgroupAction extends GroupDesignAction
function showContent()
{
$this->showGroupProfile();
+ $this->showGroupActions();
$this->showGroupNotices();
}
@@ -216,112 +217,123 @@ class ShowgroupAction extends GroupDesignAction
$this->elementStart('div', array('id' => 'i',
'class' => 'entity_profile vcard author'));
- // TRANS: Group profile header (h2). Text hidden by default.
- $this->element('h2', null, _('Group profile'));
+ if (Event::handle('StartGroupProfileElements', array($this, $this->group))) {
- $this->elementStart('dl', 'entity_depiction');
- // TRANS: Label for group avatar (dt). Text hidden by default.
- $this->element('dt', null, _('Avatar'));
- $this->elementStart('dd');
+ // TRANS: Group profile header (h2). Text hidden by default.
+ $this->element('h2', null, _('Group profile'));
- $logo = ($this->group->homepage_logo) ?
- $this->group->homepage_logo : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
-
- $this->element('img', array('src' => $logo,
- 'class' => 'photo avatar',
- 'width' => AVATAR_PROFILE_SIZE,
- 'height' => AVATAR_PROFILE_SIZE,
- 'alt' => $this->group->nickname));
- $this->elementEnd('dd');
- $this->elementEnd('dl');
-
- $this->elementStart('dl', 'entity_nickname');
- // TRANS: Label for group nickname (dt). Text hidden by default.
- $this->element('dt', null, _('Nickname'));
- $this->elementStart('dd');
- $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid';
- $this->element('a', array('href' => $this->group->homeUrl(),
- 'rel' => 'me', 'class' => $hasFN),
- $this->group->nickname);
- $this->elementEnd('dd');
- $this->elementEnd('dl');
-
- if ($this->group->fullname) {
- $this->elementStart('dl', 'entity_fn');
- // TRANS: Label for full group name (dt). Text hidden by default.
- $this->element('dt', null, _('Full name'));
+ $this->elementStart('dl', 'entity_depiction');
+ // TRANS: Label for group avatar (dt). Text hidden by default.
+ $this->element('dt', null, _('Avatar'));
$this->elementStart('dd');
- $this->element('span', 'fn org', $this->group->fullname);
+
+ $logo = ($this->group->homepage_logo) ?
+ $this->group->homepage_logo : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
+
+ $this->element('img', array('src' => $logo,
+ 'class' => 'photo avatar',
+ 'width' => AVATAR_PROFILE_SIZE,
+ 'height' => AVATAR_PROFILE_SIZE,
+ 'alt' => $this->group->nickname));
$this->elementEnd('dd');
$this->elementEnd('dl');
- }
- if ($this->group->location) {
- $this->elementStart('dl', 'entity_location');
- // TRANS: Label for group location (dt). Text hidden by default.
- $this->element('dt', null, _('Location'));
- $this->element('dd', 'label', $this->group->location);
- $this->elementEnd('dl');
- }
-
- if ($this->group->homepage) {
- $this->elementStart('dl', 'entity_url');
- // TRANS: Label for group URL (dt). Text hidden by default.
- $this->element('dt', null, _('URL'));
+ $this->elementStart('dl', 'entity_nickname');
+ // TRANS: Label for group nickname (dt). Text hidden by default.
+ $this->element('dt', null, _('Nickname'));
$this->elementStart('dd');
- $this->element('a', array('href' => $this->group->homepage,
- 'rel' => 'me', 'class' => 'url'),
- $this->group->homepage);
+ $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid';
+ $this->element('a', array('href' => $this->group->homeUrl(),
+ 'rel' => 'me', 'class' => $hasFN),
+ $this->group->nickname);
$this->elementEnd('dd');
$this->elementEnd('dl');
- }
- if ($this->group->description) {
- $this->elementStart('dl', 'entity_note');
- // TRANS: Label for group description or group note (dt). Text hidden by default.
- $this->element('dt', null, _('Note'));
- $this->element('dd', 'note', $this->group->description);
- $this->elementEnd('dl');
- }
-
- if (common_config('group', 'maxaliases') > 0) {
- $aliases = $this->group->getAliases();
-
- if (!empty($aliases)) {
- $this->elementStart('dl', 'entity_aliases');
- // TRANS: Label for group aliases (dt). Text hidden by default.
- $this->element('dt', null, _('Aliases'));
- $this->element('dd', 'aliases', implode(' ', $aliases));
+ if ($this->group->fullname) {
+ $this->elementStart('dl', 'entity_fn');
+ // TRANS: Label for full group name (dt). Text hidden by default.
+ $this->element('dt', null, _('Full name'));
+ $this->elementStart('dd');
+ $this->element('span', 'fn org', $this->group->fullname);
+ $this->elementEnd('dd');
$this->elementEnd('dl');
}
+
+ if ($this->group->location) {
+ $this->elementStart('dl', 'entity_location');
+ // TRANS: Label for group location (dt). Text hidden by default.
+ $this->element('dt', null, _('Location'));
+ $this->element('dd', 'label', $this->group->location);
+ $this->elementEnd('dl');
+ }
+
+ if ($this->group->homepage) {
+ $this->elementStart('dl', 'entity_url');
+ // TRANS: Label for group URL (dt). Text hidden by default.
+ $this->element('dt', null, _('URL'));
+ $this->elementStart('dd');
+ $this->element('a', array('href' => $this->group->homepage,
+ 'rel' => 'me', 'class' => 'url'),
+ $this->group->homepage);
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+ }
+
+ if ($this->group->description) {
+ $this->elementStart('dl', 'entity_note');
+ // TRANS: Label for group description or group note (dt). Text hidden by default.
+ $this->element('dt', null, _('Note'));
+ $this->element('dd', 'note', $this->group->description);
+ $this->elementEnd('dl');
+ }
+
+ if (common_config('group', 'maxaliases') > 0) {
+ $aliases = $this->group->getAliases();
+
+ if (!empty($aliases)) {
+ $this->elementStart('dl', 'entity_aliases');
+ // TRANS: Label for group aliases (dt). Text hidden by default.
+ $this->element('dt', null, _('Aliases'));
+ $this->element('dd', 'aliases', implode(' ', $aliases));
+ $this->elementEnd('dl');
+ }
+ }
+
+ Event::handle('EndGroupProfileElements', array($this, $this->group));
}
$this->elementEnd('div');
+ }
+ function showGroupActions()
+ {
$cur = common_current_user();
$this->elementStart('div', 'entity_actions');
// TRANS: Group actions header (h2). Text hidden by default.
$this->element('h2', null, _('Group actions'));
$this->elementStart('ul');
- $this->elementStart('li', 'entity_subscribe');
- if (Event::handle('StartGroupSubscribe', array($this, $this->group))) {
- if ($cur) {
- if ($cur->isMember($this->group)) {
- $lf = new LeaveForm($this, $this->group);
- $lf->show();
- } else if (!Group_block::isBlocked($this->group, $cur->getProfile())) {
- $jf = new JoinForm($this, $this->group);
- $jf->show();
+ if (Event::handle('StartGroupActionsList', array($this, $this->group))) {
+ $this->elementStart('li', 'entity_subscribe');
+ if (Event::handle('StartGroupSubscribe', array($this, $this->group))) {
+ if ($cur) {
+ if ($cur->isMember($this->group)) {
+ $lf = new LeaveForm($this, $this->group);
+ $lf->show();
+ } else if (!Group_block::isBlocked($this->group, $cur->getProfile())) {
+ $jf = new JoinForm($this, $this->group);
+ $jf->show();
+ }
}
+ Event::handle('EndGroupSubscribe', array($this, $this->group));
}
- Event::handle('EndGroupSubscribe', array($this, $this->group));
- }
- $this->elementEnd('li');
- if ($cur && $cur->hasRight(Right::DELETEGROUP)) {
- $this->elementStart('li', 'entity_delete');
- $df = new DeleteGroupForm($this, $this->group);
- $df->show();
$this->elementEnd('li');
+ if ($cur && $cur->hasRight(Right::DELETEGROUP)) {
+ $this->elementStart('li', 'entity_delete');
+ $df = new DeleteGroupForm($this, $this->group);
+ $df->show();
+ $this->elementEnd('li');
+ }
+ Event::handle('EndGroupActionsList', array($this, $this->group));
}
$this->elementEnd('ul');
$this->elementEnd('div');
diff --git a/actions/showmessage.php b/actions/showmessage.php
index d737f85d3a..1c867af119 100644
--- a/actions/showmessage.php
+++ b/actions/showmessage.php
@@ -30,20 +30,17 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
-require_once INSTALLDIR.'/lib/mailbox.php';
-
/**
* Show a single message
*
- * // XXX: It is totally weird how this works!
- *
* @category Personal
* @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 ShowmessageAction extends MailboxAction
+
+class ShowmessageAction extends Action
{
/**
* Message object to show
@@ -82,22 +79,20 @@ class ShowmessageAction extends MailboxAction
$this->user = common_current_user();
+ if (empty($this->user) ||
+ ($this->user->id != $this->message->from_profile &&
+ $this->user->id != $this->message->to_profile)) {
+ // TRANS: Client error displayed requesting a single direct message the requesting user was not a party in.
+ throw new ClientException(_('Only the sender and recipient ' .
+ 'may read this message.'), 403);
+ }
+
return true;
}
function handle($args)
{
- Action::handle($args);
-
- if ($this->user && ($this->user->id == $this->message->from_profile ||
- $this->user->id == $this->message->to_profile)) {
- $this->showPage();
- } else {
- // TRANS: Client error displayed requesting a single direct message the requesting user was not a party in.
- $this->clientError(_('Only the sender and recipient ' .
- 'may read this message.'), 403);
- return;
- }
+ $this->showPage();
}
function title()
@@ -121,12 +116,38 @@ class ShowmessageAction extends MailboxAction
}
}
- function getMessages()
+
+ function showContent()
{
- $message = new Message();
- $message->id = $this->message->id;
- $message->find();
- return $message;
+ $this->elementStart('ul', 'notices messages');
+ $ml = new ShowMessageListItem($this, $this->message, $this->user);
+ $ml->show();
+ $this->elementEnd('ul');
+ }
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Don't show aside
+ *
+ * @return void
+ */
+
+ function showAside() {
+ }
+}
+
+class ShowMessageListItem extends MessageListItem
+{
+ var $user;
+
+ function __construct($out, $message, $user)
+ {
+ parent::__construct($out, $message);
+ $this->user = $user;
}
function getMessageProfile()
@@ -140,46 +161,4 @@ class ShowmessageAction extends MailboxAction
return null;
}
}
-
- /**
- * Don't show local navigation
- *
- * @return void
- */
- function showLocalNavBlock()
- {
- }
-
- /**
- * Don't show page notice
- *
- * @return void
- */
- function showPageNoticeBlock()
- {
- }
-
- /**
- * Don't show aside
- *
- * @return void
- */
- function showAside()
- {
- }
-
- /**
- * Don't show any instructions
- *
- * @return string
- */
- function getInstructions()
- {
- return '';
- }
-
- function isReadOnly($args)
- {
- return true;
- }
}
diff --git a/classes/File.php b/classes/File.php
index 29a8f0f1c5..e9a0131c4e 100644
--- a/classes/File.php
+++ b/classes/File.php
@@ -55,14 +55,20 @@ class File extends Memcached_DataObject
return 'http://www.facebook.com/login.php' === $url;
}
- function getAttachments($post_id) {
- $query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id);
- $this->query($query);
+ /**
+ * Get the attachments for a particlar notice.
+ *
+ * @param int $post_id
+ * @return array of File objects
+ */
+ static function getAttachments($post_id) {
+ $file = new File();
+ $query = "select file.* from file join file_to_post on (file_id = file.id) where post_id = " . $file->escape($post_id);
+ $file = Memcached_DataObject::cachedQuery('File', $query);
$att = array();
- while ($this->fetch()) {
- $att[] = clone($this);
+ while ($file->fetch()) {
+ $att[] = clone($file);
}
- $this->free();
return $att;
}
diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php
index a3c2de8e64..867b40cf3c 100644
--- a/classes/Memcached_DataObject.php
+++ b/classes/Memcached_DataObject.php
@@ -340,6 +340,7 @@ class Memcached_DataObject extends Safe_DataObject
$start = microtime(true);
$result = null;
if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
+ common_perf_counter('query', $string);
$result = parent::_query($string);
Event::handle('EndDBQuery', array($this, $string, &$result));
}
diff --git a/classes/Notice.php b/classes/Notice.php
index e9ea479e14..4522d1fc38 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -446,7 +446,10 @@ class Notice extends Memcached_DataObject
function blowOnInsert($conversation = false)
{
self::blow('profile:notice_ids:%d', $this->profile_id);
- self::blow('public');
+
+ if ($this->isPublic()) {
+ self::blow('public');
+ }
// XXX: Before we were blowing the casche only if the notice id
// was not the root of the conversation. What to do now?
@@ -481,7 +484,10 @@ class Notice extends Memcached_DataObject
$this->blowOnInsert();
self::blow('profile:notice_ids:%d;last', $this->profile_id);
- self::blow('public;last');
+
+ if ($this->isPublic()) {
+ self::blow('public;last');
+ }
}
/** save all urls in the notice to the db
@@ -958,7 +964,7 @@ class Notice extends Memcached_DataObject
$groups = array();
/* extract all !group */
- $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
+ $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/',
strtolower($this->content),
$match);
if (!$count) {
@@ -2107,4 +2113,14 @@ class Notice extends Memcached_DataObject
$obj->whereAdd($max);
}
}
+
+ function isPublic()
+ {
+ if (common_config('public', 'localonly')) {
+ return ($this->is_local == Notice::LOCAL_PUBLIC);
+ } else {
+ return (($this->is_local != Notice::LOCAL_NONPUBLIC) &&
+ ($this->is_local != Notice::GATEWAY));
+ }
+ }
}
diff --git a/classes/Profile.php b/classes/Profile.php
index adad0c6157..03f9300962 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -918,6 +918,31 @@ class Profile extends Memcached_DataObject
return $xs->getString();
}
+ /**
+ * Extra profile info for atom entries
+ *
+ * Clients use some extra profile info in the atom stream.
+ * This gives it to them.
+ *
+ * @param User $cur Current user
+ *
+ * @return array representation of element
+ */
+
+ function profileInfo($cur)
+ {
+ $profileInfoAttr = array();
+
+ if ($cur != null) {
+ // Whether the current user is a subscribed to this profile
+ $profileInfoAttr['following'] = $cur->isSubscribed($this) ? 'true' : 'false';
+ // Whether the current user is has blocked this profile
+ $profileInfoAttr['blocking'] = $cur->hasBlocked($this) ? 'true' : 'false';
+ }
+
+ return array('statusnet:profile_info', $profileInfoAttr, null);
+ }
+
/**
* Returns an XML string fragment with profile information as an
* Activity Streams element.
diff --git a/classes/Session.php b/classes/Session.php
index e1c83ad4dc..166b89815a 100644
--- a/classes/Session.php
+++ b/classes/Session.php
@@ -156,6 +156,13 @@ class Session extends Memcached_DataObject
$session->selectAdd();
$session->selectAdd('id');
+ $limit = common_config('sessions', 'gc_limit');
+ if ($limit > 0) {
+ // On large sites, too many sessions to expire
+ // at once will just result in failure.
+ $session->limit($limit);
+ }
+
$session->find();
while ($session->fetch()) {
diff --git a/classes/User_group.php b/classes/User_group.php
index d402ed4773..5a9991fe9e 100644
--- a/classes/User_group.php
+++ b/classes/User_group.php
@@ -512,64 +512,70 @@ class User_group extends Memcached_DataObject
$group->mainpage = $mainpage;
$group->created = common_sql_now();
- $result = $group->insert();
+ if (Event::handle('StartGroupSave', array(&$group))) {
- if (!$result) {
- common_log_db_error($group, 'INSERT', __FILE__);
- // TRANS: Server exception thrown when creating a group failed.
- throw new ServerException(_('Could not create group.'));
- }
-
- if (!isset($uri) || empty($uri)) {
- $orig = clone($group);
- $group->uri = common_local_url('groupbyid', array('id' => $group->id));
- $result = $group->update($orig);
- if (!$result) {
- common_log_db_error($group, 'UPDATE', __FILE__);
- // TRANS: Server exception thrown when updating a group URI failed.
- throw new ServerException(_('Could not set group URI.'));
- }
- }
-
- $result = $group->setAliases($aliases);
-
- if (!$result) {
- // TRANS: Server exception thrown when creating group aliases failed.
- throw new ServerException(_('Could not create aliases.'));
- }
-
- $member = new Group_member();
-
- $member->group_id = $group->id;
- $member->profile_id = $userid;
- $member->is_admin = 1;
- $member->created = $group->created;
-
- $result = $member->insert();
-
- if (!$result) {
- common_log_db_error($member, 'INSERT', __FILE__);
- // TRANS: Server exception thrown when setting group membership failed.
- throw new ServerException(_('Could not set group membership.'));
- }
-
- if ($local) {
- $local_group = new Local_group();
-
- $local_group->group_id = $group->id;
- $local_group->nickname = $nickname;
- $local_group->created = common_sql_now();
-
- $result = $local_group->insert();
+ $result = $group->insert();
if (!$result) {
- common_log_db_error($local_group, 'INSERT', __FILE__);
- // TRANS: Server exception thrown when saving local group information failed.
- throw new ServerException(_('Could not save local group info.'));
+ common_log_db_error($group, 'INSERT', __FILE__);
+ // TRANS: Server exception thrown when creating a group failed.
+ throw new ServerException(_('Could not create group.'));
}
+
+ if (!isset($uri) || empty($uri)) {
+ $orig = clone($group);
+ $group->uri = common_local_url('groupbyid', array('id' => $group->id));
+ $result = $group->update($orig);
+ if (!$result) {
+ common_log_db_error($group, 'UPDATE', __FILE__);
+ // TRANS: Server exception thrown when updating a group URI failed.
+ throw new ServerException(_('Could not set group URI.'));
+ }
+ }
+
+ $result = $group->setAliases($aliases);
+
+ if (!$result) {
+ // TRANS: Server exception thrown when creating group aliases failed.
+ throw new ServerException(_('Could not create aliases.'));
+ }
+
+ $member = new Group_member();
+
+ $member->group_id = $group->id;
+ $member->profile_id = $userid;
+ $member->is_admin = 1;
+ $member->created = $group->created;
+
+ $result = $member->insert();
+
+ if (!$result) {
+ common_log_db_error($member, 'INSERT', __FILE__);
+ // TRANS: Server exception thrown when setting group membership failed.
+ throw new ServerException(_('Could not set group membership.'));
+ }
+
+ if ($local) {
+ $local_group = new Local_group();
+
+ $local_group->group_id = $group->id;
+ $local_group->nickname = $nickname;
+ $local_group->created = common_sql_now();
+
+ $result = $local_group->insert();
+
+ if (!$result) {
+ common_log_db_error($local_group, 'INSERT', __FILE__);
+ // TRANS: Server exception thrown when saving local group information failed.
+ throw new ServerException(_('Could not save local group info.'));
+ }
+ }
+
+ $group->query('COMMIT');
+
+ Event::handle('EndGroupSave', array($group));
}
- $group->query('COMMIT');
return $group;
}
diff --git a/extlib/Auth/SASL.php b/extlib/Auth/SASL.php
new file mode 100644
index 0000000000..e70f0146bc
--- /dev/null
+++ b/extlib/Auth/SASL.php
@@ -0,0 +1,104 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: SASL.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Client implementation of various SASL mechanisms
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('PEAR.php');
+
+class Auth_SASL
+{
+ /**
+ * Factory class. Returns an object of the request
+ * type.
+ *
+ * @param string $type One of: Anonymous
+ * Plain
+ * CramMD5
+ * DigestMD5
+ * Types are not case sensitive
+ */
+ function &factory($type)
+ {
+ switch (strtolower($type)) {
+ case 'anonymous':
+ $filename = 'Auth/SASL/Anonymous.php';
+ $classname = 'Auth_SASL_Anonymous';
+ break;
+
+ case 'login':
+ $filename = 'Auth/SASL/Login.php';
+ $classname = 'Auth_SASL_Login';
+ break;
+
+ case 'plain':
+ $filename = 'Auth/SASL/Plain.php';
+ $classname = 'Auth_SASL_Plain';
+ break;
+
+ case 'external':
+ $filename = 'Auth/SASL/External.php';
+ $classname = 'Auth_SASL_External';
+ break;
+
+ case 'crammd5':
+ $filename = 'Auth/SASL/CramMD5.php';
+ $classname = 'Auth_SASL_CramMD5';
+ break;
+
+ case 'digestmd5':
+ $filename = 'Auth/SASL/DigestMD5.php';
+ $classname = 'Auth_SASL_DigestMD5';
+ break;
+
+ default:
+ return PEAR::raiseError('Invalid SASL mechanism type');
+ break;
+ }
+
+ require_once($filename);
+ $obj = new $classname();
+ return $obj;
+ }
+}
+
+?>
diff --git a/extlib/Auth/SASL/Anonymous.php b/extlib/Auth/SASL/Anonymous.php
new file mode 100644
index 0000000000..4eaaa399e6
--- /dev/null
+++ b/extlib/Auth/SASL/Anonymous.php
@@ -0,0 +1,71 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: Anonymous.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Implmentation of ANONYMOUS SASL mechanism
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_Anonymous extends Auth_SASL_Common
+{
+ /**
+ * Not much to do here except return the token supplied.
+ * No encoding, hashing or encryption takes place for this
+ * mechanism, simply one of:
+ * o An email address
+ * o An opaque string not containing "@" that can be interpreted
+ * by the sysadmin
+ * o Nothing
+ *
+ * We could have some logic here for the second option, but this
+ * would by no means create something interpretable.
+ *
+ * @param string $token Optional email address or string to provide
+ * as trace information.
+ * @return string The unaltered input token
+ */
+ function getResponse($token = '')
+ {
+ return $token;
+ }
+}
+?>
\ No newline at end of file
diff --git a/extlib/Auth/SASL/Common.php b/extlib/Auth/SASL/Common.php
new file mode 100644
index 0000000000..44181645c4
--- /dev/null
+++ b/extlib/Auth/SASL/Common.php
@@ -0,0 +1,74 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: Common.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Common functionality to SASL mechanisms
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+class Auth_SASL_Common
+{
+ /**
+ * Function which implements HMAC MD5 digest
+ *
+ * @param string $key The secret key
+ * @param string $data The data to protect
+ * @return string The HMAC MD5 digest
+ */
+ function _HMAC_MD5($key, $data)
+ {
+ if (strlen($key) > 64) {
+ $key = pack('H32', md5($key));
+ }
+
+ if (strlen($key) < 64) {
+ $key = str_pad($key, 64, chr(0));
+ }
+
+ $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64);
+ $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64);
+
+ $inner = pack('H32', md5($k_ipad . $data));
+ $digest = md5($k_opad . $inner);
+
+ return $digest;
+ }
+}
+?>
diff --git a/extlib/Auth/SASL/CramMD5.php b/extlib/Auth/SASL/CramMD5.php
new file mode 100644
index 0000000000..177279c06b
--- /dev/null
+++ b/extlib/Auth/SASL/CramMD5.php
@@ -0,0 +1,68 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: CramMD5.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Implmentation of CRAM-MD5 SASL mechanism
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_CramMD5 extends Auth_SASL_Common
+{
+ /**
+ * Implements the CRAM-MD5 SASL mechanism
+ * This DOES NOT base64 encode the return value,
+ * you will need to do that yourself.
+ *
+ * @param string $user Username
+ * @param string $pass Password
+ * @param string $challenge The challenge supplied by the server.
+ * this should be already base64_decoded.
+ *
+ * @return string The string to pass back to the server, of the form
+ * "". This is NOT base64_encoded.
+ */
+ function getResponse($user, $pass, $challenge)
+ {
+ return $user . ' ' . $this->_HMAC_MD5($pass, $challenge);
+ }
+}
+?>
\ No newline at end of file
diff --git a/extlib/Auth/SASL/DigestMD5.php b/extlib/Auth/SASL/DigestMD5.php
new file mode 100644
index 0000000000..8c58787dfe
--- /dev/null
+++ b/extlib/Auth/SASL/DigestMD5.php
@@ -0,0 +1,197 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: DigestMD5.php 294702 2010-02-07 16:03:55Z cweiske $
+
+/**
+* Implmentation of DIGEST-MD5 SASL mechanism
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_DigestMD5 extends Auth_SASL_Common
+{
+ /**
+ * Provides the (main) client response for DIGEST-MD5
+ * requires a few extra parameters than the other
+ * mechanisms, which are unavoidable.
+ *
+ * @param string $authcid Authentication id (username)
+ * @param string $pass Password
+ * @param string $challenge The digest challenge sent by the server
+ * @param string $hostname The hostname of the machine you're connecting to
+ * @param string $service The servicename (eg. imap, pop, acap etc)
+ * @param string $authzid Authorization id (username to proxy as)
+ * @return string The digest response (NOT base64 encoded)
+ * @access public
+ */
+ function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '')
+ {
+ $challenge = $this->_parseChallenge($challenge);
+ $authzid_string = '';
+ if ($authzid != '') {
+ $authzid_string = ',authzid="' . $authzid . '"';
+ }
+
+ if (!empty($challenge)) {
+ $cnonce = $this->_getCnonce();
+ $digest_uri = sprintf('%s/%s', $service, $hostname);
+ $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid);
+
+ if ($challenge['realm']) {
+ return sprintf('username="%s",realm="%s"' . $authzid_string .
+',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']);
+ } else {
+ return sprintf('username="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']);
+ }
+ } else {
+ return PEAR::raiseError('Invalid digest challenge');
+ }
+ }
+
+ /**
+ * Parses and verifies the digest challenge*
+ *
+ * @param string $challenge The digest challenge
+ * @return array The parsed challenge as an assoc
+ * array in the form "directive => value".
+ * @access private
+ */
+ function _parseChallenge($challenge)
+ {
+ $tokens = array();
+ while (preg_match('/^([a-z-]+)=("[^"]+(?
diff --git a/extlib/Auth/SASL/External.php b/extlib/Auth/SASL/External.php
new file mode 100644
index 0000000000..86a17cb7ab
--- /dev/null
+++ b/extlib/Auth/SASL/External.php
@@ -0,0 +1,63 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: External.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Implmentation of EXTERNAL SASL mechanism
+*
+* @author Christoph Schulz
+* @access public
+* @version 1.0.3
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_External extends Auth_SASL_Common
+{
+ /**
+ * Returns EXTERNAL response
+ *
+ * @param string $authcid Authentication id (username)
+ * @param string $pass Password
+ * @param string $authzid Autorization id
+ * @return string EXTERNAL Response
+ */
+ function getResponse($authcid, $pass, $authzid = '')
+ {
+ return $authzid;
+ }
+}
+?>
diff --git a/extlib/Auth/SASL/Login.php b/extlib/Auth/SASL/Login.php
new file mode 100644
index 0000000000..a925f53a79
--- /dev/null
+++ b/extlib/Auth/SASL/Login.php
@@ -0,0 +1,65 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: Login.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* This is technically not a SASL mechanism, however
+* it's used by Net_Sieve, Net_Cyrus and potentially
+* other protocols , so here is a good place to abstract
+* it.
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_Login extends Auth_SASL_Common
+{
+ /**
+ * Pseudo SASL LOGIN mechanism
+ *
+ * @param string $user Username
+ * @param string $pass Password
+ * @return string LOGIN string
+ */
+ function getResponse($user, $pass)
+ {
+ return sprintf('LOGIN %s %s', $user, $pass);
+ }
+}
+?>
\ No newline at end of file
diff --git a/extlib/Auth/SASL/Plain.php b/extlib/Auth/SASL/Plain.php
new file mode 100644
index 0000000000..912710169e
--- /dev/null
+++ b/extlib/Auth/SASL/Plain.php
@@ -0,0 +1,63 @@
+ |
+// +-----------------------------------------------------------------------+
+//
+// $Id: Plain.php 286825 2009-08-05 06:23:42Z cweiske $
+
+/**
+* Implmentation of PLAIN SASL mechanism
+*
+* @author Richard Heyes
+* @access public
+* @version 1.0
+* @package Auth_SASL
+*/
+
+require_once('Auth/SASL/Common.php');
+
+class Auth_SASL_Plain extends Auth_SASL_Common
+{
+ /**
+ * Returns PLAIN response
+ *
+ * @param string $authcid Authentication id (username)
+ * @param string $pass Password
+ * @param string $authzid Autorization id
+ * @return string PLAIN Response
+ */
+ function getResponse($authcid, $pass, $authzid = '')
+ {
+ return $authzid . chr(0) . $authcid . chr(0) . $pass;
+ }
+}
+?>
diff --git a/index.php b/index.php
index 37c157b01f..7f2afffb5a 100644
--- a/index.php
+++ b/index.php
@@ -38,6 +38,7 @@
*/
$_startTime = microtime(true);
+$_perfCounters = array();
define('INSTALLDIR', dirname(__FILE__));
define('STATUSNET', true);
@@ -45,6 +46,8 @@ define('LACONICA', true); // compatibility
require_once INSTALLDIR . '/lib/common.php';
+register_shutdown_function('common_log_perf_counters');
+
$user = null;
$action = null;
diff --git a/js/jquery.form.js b/js/jquery.form.js
index 14e14572af..936b847abe 100644
--- a/js/jquery.form.js
+++ b/js/jquery.form.js
@@ -1,803 +1,632 @@
-/*!
- * jQuery Form Plugin
- * version: 2.63 (29-JAN-2011)
- * @requires jQuery v1.3.2 or later
- *
- * Examples and documentation at: http://malsup.com/jquery/form/
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- */
-;(function($) {
-
-/*
- Usage Note:
- -----------
- Do not use both ajaxSubmit and ajaxForm on the same form. These
- functions are intended to be exclusive. Use ajaxSubmit if you want
- to bind your own submit handler to the form. For example,
-
- $(document).ready(function() {
- $('#myForm').bind('submit', function(e) {
- e.preventDefault(); // <-- important
- $(this).ajaxSubmit({
- target: '#output'
- });
- });
- });
-
- Use ajaxForm when you want the plugin to manage all the event binding
- for you. For example,
-
- $(document).ready(function() {
- $('#myForm').ajaxForm({
- target: '#output'
- });
- });
-
- When using ajaxForm, the ajaxSubmit function will be invoked for you
- at the appropriate time.
-*/
-
-/**
- * ajaxSubmit() provides a mechanism for immediately submitting
- * an HTML form using AJAX.
- */
-$.fn.ajaxSubmit = function(options) {
- // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
- if (!this.length) {
- log('ajaxSubmit: skipping submit process - no element selected');
- return this;
- }
-
- if (typeof options == 'function') {
- options = { success: options };
- }
-
- var action = this.attr('action');
- var url = (typeof action === 'string') ? $.trim(action) : '';
- if (url) {
- // clean url (don't include hash vaue)
- url = (url.match(/^([^#]+)/)||[])[1];
- }
- url = url || window.location.href || '';
-
- options = $.extend(true, {
- url: url,
- type: this[0].getAttribute('method') || 'GET', // IE7 massage (see issue 57)
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
- }, options);
-
- // hook for manipulating the form data before it is extracted;
- // convenient for use with rich editors like tinyMCE or FCKEditor
- var veto = {};
- this.trigger('form-pre-serialize', [this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
- return this;
- }
-
- // provide opportunity to alter form data before it is serialized
- if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSerialize callback');
- return this;
- }
-
- var n,v,a = this.formToArray(options.semantic);
- if (options.data) {
- options.extraData = options.data;
- for (n in options.data) {
- if(options.data[n] instanceof Array) {
- for (var k in options.data[n]) {
- a.push( { name: n, value: options.data[n][k] } );
- }
- }
- else {
- v = options.data[n];
- v = $.isFunction(v) ? v() : v; // if value is fn, invoke it
- a.push( { name: n, value: v } );
- }
- }
- }
-
- // give pre-submit callback an opportunity to abort the submit
- if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSubmit callback');
- return this;
- }
-
- // fire vetoable 'validate' event
- this.trigger('form-submit-validate', [a, this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
- return this;
- }
-
- var q = $.param(a);
-
- if (options.type.toUpperCase() == 'GET') {
- options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
- options.data = null; // data is null for 'get'
- }
- else {
- options.data = q; // data is the query string for 'post'
- }
-
- var $form = this, callbacks = [];
- if (options.resetForm) {
- callbacks.push(function() { $form.resetForm(); });
- }
- if (options.clearForm) {
- callbacks.push(function() { $form.clearForm(); });
- }
-
- // perform a load on the target only if dataType is not provided
- if (!options.dataType && options.target) {
- var oldSuccess = options.success || function(){};
- callbacks.push(function(data) {
- var fn = options.replaceTarget ? 'replaceWith' : 'html';
- $(options.target)[fn](data).each(oldSuccess, arguments);
- });
- }
- else if (options.success) {
- callbacks.push(options.success);
- }
-
- options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
- var context = options.context || options; // jQuery 1.4+ supports scope context
- for (var i=0, max=callbacks.length; i < max; i++) {
- callbacks[i].apply(context, [data, status, xhr || $form, $form]);
- }
- };
-
- // are there files to upload?
- var fileInputs = $('input:file', this).length > 0;
- var mp = 'multipart/form-data';
- var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
-
- // options.iframe allows user to force iframe mode
- // 06-NOV-09: now defaulting to iframe mode if file input is detected
- if (options.iframe !== false && (fileInputs || options.iframe || multipart)) {
- // hack to fix Safari hang (thanks to Tim Molendijk for this)
- // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
- if (options.closeKeepAlive) {
- $.get(options.closeKeepAlive, fileUpload);
- }
- else {
- fileUpload();
- }
- }
- else {
- $.ajax(options);
- }
-
- // fire 'notify' event
- this.trigger('form-submit-notify', [this, options]);
- return this;
-
-
- // private function for handling file uploads (hat tip to YAHOO!)
- function fileUpload() {
- var form = $form[0];
-
- if ($(':input[name=submit],:input[id=submit]', form).length) {
- // if there is an input with a name or id of 'submit' then we won't be
- // able to invoke the submit fn on the form (at least not x-browser)
- alert('Error: Form elements must not have name or id of "submit".');
- return;
- }
-
- var s = $.extend(true, {}, $.ajaxSettings, options);
- s.context = s.context || s;
- var id = 'jqFormIO' + (new Date().getTime()), fn = '_'+id;
- var $io = $('');
- var io = $io[0];
-
- $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
-
- var xhr = { // mock object
- aborted: 0,
- responseText: null,
- responseXML: null,
- status: 0,
- statusText: 'n/a',
- getAllResponseHeaders: function() {},
- getResponseHeader: function() {},
- setRequestHeader: function() {},
- abort: function() {
- this.aborted = 1;
- $io.attr('src', s.iframeSrc); // abort op in progress
- }
- };
-
- var g = s.global;
- // trigger ajax global events so that activity/block indicators work like normal
- if (g && ! $.active++) {
- $.event.trigger("ajaxStart");
- }
- if (g) {
- $.event.trigger("ajaxSend", [xhr, s]);
- }
-
- if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
- if (s.global) {
- $.active--;
- }
- return;
- }
- if (xhr.aborted) {
- return;
- }
-
- var timedOut = 0;
-
- // add submitting element to data if we know it
- var sub = form.clk;
- if (sub) {
- var n = sub.name;
- if (n && !sub.disabled) {
- s.extraData = s.extraData || {};
- s.extraData[n] = sub.value;
- if (sub.type == "image") {
- s.extraData[n+'.x'] = form.clk_x;
- s.extraData[n+'.y'] = form.clk_y;
- }
- }
- }
-
- // take a breath so that pending repaints get some cpu time before the upload starts
- function doSubmit() {
- // make sure form attrs are set
- var t = $form.attr('target'), a = $form.attr('action');
-
- // update form attrs in IE friendly way
- form.setAttribute('target',id);
- if (form.getAttribute('method') != 'POST') {
- form.setAttribute('method', 'POST');
- }
- if (form.getAttribute('action') != s.url) {
- form.setAttribute('action', s.url);
- }
-
- // ie borks in some cases when setting encoding
- if (! s.skipEncodingOverride) {
- $form.attr({
- encoding: 'multipart/form-data',
- enctype: 'multipart/form-data'
- });
- }
-
- // support timout
- if (s.timeout) {
- setTimeout(function() { timedOut = true; cb(); }, s.timeout);
- }
-
- // add "extra" data to form if provided in options
- var extraInputs = [];
- try {
- if (s.extraData) {
- for (var n in s.extraData) {
- extraInputs.push(
- $('')
- .appendTo(form)[0]);
- }
- }
-
- // add iframe to doc and submit the form
- $io.appendTo('body');
- io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
- form.submit();
- }
- finally {
- // reset attrs and remove "extra" input elements
- form.setAttribute('action',a);
- if(t) {
- form.setAttribute('target', t);
- } else {
- $form.removeAttr('target');
- }
- $(extraInputs).remove();
- }
- }
-
- if (s.forceSync) {
- doSubmit();
- }
- else {
- setTimeout(doSubmit, 10); // this lets dom updates render
- }
-
- var data, doc, domCheckCount = 50;
-
- function cb() {
- doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
- if (!doc || doc.location.href == s.iframeSrc) {
- // response not received yet
- return;
- }
- io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
-
- var ok = true;
- try {
- if (timedOut) {
- throw 'timeout';
- }
-
- var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
- log('isXml='+isXml);
- if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) {
- if (--domCheckCount) {
- // in some browsers (Opera) the iframe DOM is not always traversable when
- // the onload callback fires, so we loop a bit to accommodate
- log('requeing onLoad callback, DOM not available');
- setTimeout(cb, 250);
- return;
- }
- // let this fall through because server response could be an empty document
- //log('Could not access iframe DOM after mutiple tries.');
- //throw 'DOMException: not available';
- }
-
- //log('response detected');
- xhr.responseText = doc.body ? doc.body.innerHTML : doc.documentElement ? doc.documentElement.innerHTML : null;
- xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
- xhr.getResponseHeader = function(header){
- var headers = {'content-type': s.dataType};
- return headers[header];
- };
-
- var scr = /(json|script)/.test(s.dataType);
- if (scr || s.textarea) {
- // see if user embedded response in textarea
- var ta = doc.getElementsByTagName('textarea')[0];
- if (ta) {
- xhr.responseText = ta.value;
- }
- else if (scr) {
- // account for browsers injecting pre around json response
- var pre = doc.getElementsByTagName('pre')[0];
- var b = doc.getElementsByTagName('body')[0];
- if (pre) {
- xhr.responseText = pre.textContent;
- }
- else if (b) {
- xhr.responseText = b.innerHTML;
- }
- }
- }
- else if (s.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
- xhr.responseXML = toXml(xhr.responseText);
- }
-
- data = httpData(xhr, s.dataType, s);
- }
- catch(e){
- log('error caught:',e);
- ok = false;
- xhr.error = e;
- s.error.call(s.context, xhr, 'error', e);
- g && $.event.trigger("ajaxError", [xhr, s, e]);
- }
-
- if (xhr.aborted) {
- log('upload aborted');
- ok = false;
- }
-
- // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
- if (ok) {
- s.success.call(s.context, data, 'success', xhr);
- g && $.event.trigger("ajaxSuccess", [xhr, s]);
- }
-
- g && $.event.trigger("ajaxComplete", [xhr, s]);
-
- if (g && ! --$.active) {
- $.event.trigger("ajaxStop");
- }
-
- s.complete && s.complete.call(s.context, xhr, ok ? 'success' : 'error');
-
- // clean up
- setTimeout(function() {
- $io.removeData('form-plugin-onload');
- $io.remove();
- xhr.responseXML = null;
- }, 100);
- }
-
- var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
- if (window.ActiveXObject) {
- doc = new ActiveXObject('Microsoft.XMLDOM');
- doc.async = 'false';
- doc.loadXML(s);
- }
- else {
- doc = (new DOMParser()).parseFromString(s, 'text/xml');
- }
- return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
- };
- var parseJSON = $.parseJSON || function(s) {
- return window['eval']('(' + s + ')');
- };
-
- var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
- var ct = xhr.getResponseHeader('content-type') || '',
- xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
- data = xml ? xhr.responseXML : xhr.responseText;
-
- if (xml && data.documentElement.nodeName === 'parsererror') {
- $.error && $.error('parsererror');
- }
- if (s && s.dataFilter) {
- data = s.dataFilter(data, type);
- }
- if (typeof data === 'string') {
- if (type === 'json' || !type && ct.indexOf('json') >= 0) {
- data = parseJSON(data);
- } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
- $.globalEval(data);
- }
- }
- return data;
- };
- }
-};
-
-/**
- * ajaxForm() provides a mechanism for fully automating form submission.
- *
- * The advantages of using this method instead of ajaxSubmit() are:
- *
- * 1: This method will include coordinates for elements (if the element
- * is used to submit the form).
- * 2. This method will include the submit element's name/value data (for the element that was
- * used to submit the form).
- * 3. This method binds the submit() method to the form for you.
- *
- * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
- * passes the options argument along after properly binding events for submit elements and
- * the form itself.
- */
-$.fn.ajaxForm = function(options) {
- // in jQuery 1.3+ we can fix mistakes with the ready state
- if (this.length === 0) {
- var o = { s: this.selector, c: this.context };
- if (!$.isReady && o.s) {
- log('DOM not ready, queuing ajaxForm');
- $(function() {
- $(o.s,o.c).ajaxForm(options);
- });
- return this;
- }
- // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
- log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
- return this;
- }
-
- return this.ajaxFormUnbind().bind('submit.form-plugin', function(e) {
- if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
- e.preventDefault();
- $(this).ajaxSubmit(options);
- }
- }).bind('click.form-plugin', function(e) {
- var target = e.target;
- var $el = $(target);
- if (!($el.is(":submit,input:image"))) {
- // is this a child element of the submit el? (ex: a span within a button)
- var t = $el.closest(':submit');
- if (t.length == 0) {
- return;
- }
- target = t[0];
- }
- var form = this;
- form.clk = target;
- if (target.type == 'image') {
- if (e.offsetX != undefined) {
- form.clk_x = e.offsetX;
- form.clk_y = e.offsetY;
- } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
- var offset = $el.offset();
- form.clk_x = e.pageX - offset.left;
- form.clk_y = e.pageY - offset.top;
- } else {
- form.clk_x = e.pageX - target.offsetLeft;
- form.clk_y = e.pageY - target.offsetTop;
- }
- }
- // clear form vars
- setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
- });
-};
-
-// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
-$.fn.ajaxFormUnbind = function() {
- return this.unbind('submit.form-plugin click.form-plugin');
-};
-
-/**
- * formToArray() gathers form element data into an array of objects that can
- * be passed to any of the following ajax functions: $.get, $.post, or load.
- * Each object in the array has both a 'name' and 'value' property. An example of
- * an array for a simple login form might be:
- *
- * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
- *
- * It is this array that is passed to pre-submit callback functions provided to the
- * ajaxSubmit() and ajaxForm() methods.
- */
-$.fn.formToArray = function(semantic) {
- var a = [];
- if (this.length === 0) {
- return a;
- }
-
- var form = this[0];
- var els = semantic ? form.getElementsByTagName('*') : form.elements;
- if (!els) {
- return a;
- }
-
- var i,j,n,v,el,max,jmax;
- for(i=0, max=els.length; i < max; i++) {
- el = els[i];
- n = el.name;
- if (!n) {
- continue;
- }
-
- if (semantic && form.clk && el.type == "image") {
- // handle image inputs on the fly when semantic == true
- if(!el.disabled && form.clk == el) {
- a.push({name: n, value: $(el).val()});
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- }
- continue;
- }
-
- v = $.fieldValue(el, true);
- if (v && v.constructor == Array) {
- for(j=0, jmax=v.length; j < jmax; j++) {
- a.push({name: n, value: v[j]});
- }
- }
- else if (v !== null && typeof v != 'undefined') {
- a.push({name: n, value: v});
- }
- }
-
- if (!semantic && form.clk) {
- // input type=='image' are not found in elements array! handle it here
- var $input = $(form.clk), input = $input[0];
- n = input.name;
- if (n && !input.disabled && input.type == 'image') {
- a.push({name: n, value: $input.val()});
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- }
- }
- return a;
-};
-
-/**
- * Serializes form data into a 'submittable' string. This method will return a string
- * in the format: name1=value1&name2=value2
- */
-$.fn.formSerialize = function(semantic) {
- //hand off to jQuery.param for proper encoding
- return $.param(this.formToArray(semantic));
-};
-
-/**
- * Serializes all field elements in the jQuery object into a query string.
- * This method will return a string in the format: name1=value1&name2=value2
- */
-$.fn.fieldSerialize = function(successful) {
- var a = [];
- this.each(function() {
- var n = this.name;
- if (!n) {
- return;
- }
- var v = $.fieldValue(this, successful);
- if (v && v.constructor == Array) {
- for (var i=0,max=v.length; i < max; i++) {
- a.push({name: n, value: v[i]});
- }
- }
- else if (v !== null && typeof v != 'undefined') {
- a.push({name: this.name, value: v});
- }
- });
- //hand off to jQuery.param for proper encoding
- return $.param(a);
-};
-
-/**
- * Returns the value(s) of the element in the matched set. For example, consider the following form:
- *
- *
- *
- * var v = $(':text').fieldValue();
- * // if no values are entered into the text inputs
- * v == ['','']
- * // if values entered into the text inputs are 'foo' and 'bar'
- * v == ['foo','bar']
- *
- * var v = $(':checkbox').fieldValue();
- * // if neither checkbox is checked
- * v === undefined
- * // if both checkboxes are checked
- * v == ['B1', 'B2']
- *
- * var v = $(':radio').fieldValue();
- * // if neither radio is checked
- * v === undefined
- * // if first radio is checked
- * v == ['C1']
- *
- * The successful argument controls whether or not the field element must be 'successful'
- * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
- * The default value of the successful argument is true. If this value is false the value(s)
- * for each element is returned.
- *
- * Note: This method *always* returns an array. If no valid value can be determined the
- * array will be empty, otherwise it will contain one or more values.
- */
-$.fn.fieldValue = function(successful) {
- for (var val=[], i=0, max=this.length; i < max; i++) {
- var el = this[i];
- var v = $.fieldValue(el, successful);
- if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
- continue;
- }
- v.constructor == Array ? $.merge(val, v) : val.push(v);
- }
- return val;
-};
-
-/**
- * Returns the value of the field element.
- */
-$.fieldValue = function(el, successful) {
- var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
- if (successful === undefined) {
- successful = true;
- }
-
- if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
- (t == 'checkbox' || t == 'radio') && !el.checked ||
- (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
- tag == 'select' && el.selectedIndex == -1)) {
- return null;
- }
-
- if (tag == 'select') {
- var index = el.selectedIndex;
- if (index < 0) {
- return null;
- }
- var a = [], ops = el.options;
- var one = (t == 'select-one');
- var max = (one ? index+1 : ops.length);
- for(var i=(one ? index : 0); i < max; i++) {
- var op = ops[i];
- if (op.selected) {
- var v = op.value;
- if (!v) { // extra pain for IE...
- v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
- }
- if (one) {
- return v;
- }
- a.push(v);
- }
- }
- return a;
- }
- return $(el).val();
-};
-
-/**
- * Clears the form data. Takes the following actions on the form's input fields:
- * - input text fields will have their 'value' property set to the empty string
- * - select elements will have their 'selectedIndex' property set to -1
- * - checkbox and radio inputs will have their 'checked' property set to false
- * - inputs of type submit, button, reset, and hidden will *not* be effected
- * - button elements will *not* be effected
- */
-$.fn.clearForm = function() {
- return this.each(function() {
- $('input,select,textarea', this).clearFields();
- });
-};
-
-/**
- * Clears the selected form elements.
- */
-$.fn.clearFields = $.fn.clearInputs = function() {
- return this.each(function() {
- var t = this.type, tag = this.tagName.toLowerCase();
- if (t == 'text' || t == 'password' || tag == 'textarea') {
- this.value = '';
- }
- else if (t == 'checkbox' || t == 'radio') {
- this.checked = false;
- }
- else if (tag == 'select') {
- this.selectedIndex = -1;
- }
- });
-};
-
-/**
- * Resets the form data. Causes all form elements to be reset to their original value.
- */
-$.fn.resetForm = function() {
- return this.each(function() {
- // guard against an input with the name of 'reset'
- // note that IE reports the reset function as an 'object'
- if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
- this.reset();
- }
- });
-};
-
-/**
- * Enables or disables any matching elements.
- */
-$.fn.enable = function(b) {
- if (b === undefined) {
- b = true;
- }
- return this.each(function() {
- this.disabled = !b;
- });
-};
-
-/**
- * Checks/unchecks any matching checkboxes or radio buttons and
- * selects/deselects and matching option elements.
- */
-$.fn.selected = function(select) {
- if (select === undefined) {
- select = true;
- }
- return this.each(function() {
- var t = this.type;
- if (t == 'checkbox' || t == 'radio') {
- this.checked = select;
- }
- else if (this.tagName.toLowerCase() == 'option') {
- var $sel = $(this).parent('select');
- if (select && $sel[0] && $sel[0].type == 'select-one') {
- // deselect all other options
- $sel.find('option').selected(false);
- }
- this.selected = select;
- }
- });
-};
-
-// helper fn for console logging
-// set $.fn.ajaxSubmit.debug to true to enable debug logging
-function log() {
- if ($.fn.ajaxSubmit.debug) {
- var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
- if (window.console && window.console.log) {
- window.console.log(msg);
- }
- else if (window.opera && window.opera.postError) {
- window.opera.postError(msg);
- }
- }
-};
-
-})(jQuery);
+/*
+ * jQuery Form Plugin
+ * version: 2.17 (06-NOV-2008)
+ * @requires jQuery v1.2.2 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id$
+ */
+;(function($) {
+
+/*
+ Usage Note:
+ -----------
+ Do not use both ajaxSubmit and ajaxForm on the same form. These
+ functions are intended to be exclusive. Use ajaxSubmit if you want
+ to bind your own submit handler to the form. For example,
+
+ $(document).ready(function() {
+ $('#myForm').bind('submit', function() {
+ $(this).ajaxSubmit({
+ target: '#output'
+ });
+ return false; // <-- important!
+ });
+ });
+
+ Use ajaxForm when you want the plugin to manage all the event binding
+ for you. For example,
+
+ $(document).ready(function() {
+ $('#myForm').ajaxForm({
+ target: '#output'
+ });
+ });
+
+ When using ajaxForm, the ajaxSubmit function will be invoked for you
+ at the appropriate time.
+*/
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+ // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+ if (!this.length) {
+ log('ajaxSubmit: skipping submit process - no element selected');
+ return this;
+ }
+
+ if (typeof options == 'function')
+ options = { success: options };
+
+ options = $.extend({
+ url: this.attr('action') || window.location.toString(),
+ type: this.attr('method') || 'GET'
+ }, options || {});
+
+ // hook for manipulating the form data before it is extracted;
+ // convenient for use with rich editors like tinyMCE or FCKEditor
+ var veto = {};
+ this.trigger('form-pre-serialize', [this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+ return this;
+ }
+
+ // provide opportunity to alter form data before it is serialized
+ if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSerialize callback');
+ return this;
+ }
+
+ var a = this.formToArray(options.semantic);
+ if (options.data) {
+ options.extraData = options.data;
+ for (var n in options.data) {
+ if(options.data[n] instanceof Array) {
+ for (var k in options.data[n])
+ a.push( { name: n, value: options.data[n][k] } )
+ }
+ else
+ a.push( { name: n, value: options.data[n] } );
+ }
+ }
+
+ // give pre-submit callback an opportunity to abort the submit
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSubmit callback');
+ return this;
+ }
+
+ // fire vetoable 'validate' event
+ this.trigger('form-submit-validate', [a, this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+ return this;
+ }
+
+ var q = $.param(a);
+
+ if (options.type.toUpperCase() == 'GET') {
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+ options.data = null; // data is null for 'get'
+ }
+ else
+ options.data = q; // data is the query string for 'post'
+
+ var $form = this, callbacks = [];
+ if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
+ if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
+
+ // perform a load on the target only if dataType is not provided
+ if (!options.dataType && options.target) {
+ var oldSuccess = options.success || function(){};
+ callbacks.push(function(data) {
+ $(options.target).html(data).each(oldSuccess, arguments);
+ });
+ }
+ else if (options.success)
+ callbacks.push(options.success);
+
+ options.success = function(data, status) {
+ for (var i=0, max=callbacks.length; i < max; i++)
+ callbacks[i].apply(options, [data, status, $form]);
+ };
+
+ // are there files to upload?
+ var files = $('input:file', this).fieldValue();
+ var found = false;
+ for (var j=0; j < files.length; j++)
+ if (files[j])
+ found = true;
+
+ // options.iframe allows user to force iframe mode
+ if (options.iframe || found) {
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+ if ($.browser.safari && options.closeKeepAlive)
+ $.get(options.closeKeepAlive, fileUpload);
+ else
+ fileUpload();
+ }
+ else
+ $.ajax(options);
+
+ // fire 'notify' event
+ this.trigger('form-submit-notify', [this, options]);
+ return this;
+
+
+ // private function for handling file uploads (hat tip to YAHOO!)
+ function fileUpload() {
+ var form = $form[0];
+
+ if ($(':input[name=submit]', form).length) {
+ alert('Error: Form elements must not be named "submit".');
+ return;
+ }
+
+ var opts = $.extend({}, $.ajaxSettings, options);
+ var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
+
+ var id = 'jqFormIO' + (new Date().getTime());
+ var $io = $('');
+ var io = $io[0];
+
+ if ($.browser.msie || $.browser.opera)
+ io.src = 'javascript:false;document.write("");';
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+
+ var xhr = { // mock object
+ aborted: 0,
+ responseText: null,
+ responseXML: null,
+ status: 0,
+ statusText: 'n/a',
+ getAllResponseHeaders: function() {},
+ getResponseHeader: function() {},
+ setRequestHeader: function() {},
+ abort: function() {
+ this.aborted = 1;
+ $io.attr('src','about:blank'); // abort op in progress
+ }
+ };
+
+ var g = opts.global;
+ // trigger ajax global events so that activity/block indicators work like normal
+ if (g && ! $.active++) $.event.trigger("ajaxStart");
+ if (g) $.event.trigger("ajaxSend", [xhr, opts]);
+
+ if (s.beforeSend && s.beforeSend(xhr, s) === false) {
+ s.global && jQuery.active--;
+ return;
+ }
+ if (xhr.aborted)
+ return;
+
+ var cbInvoked = 0;
+ var timedOut = 0;
+
+ // add submitting element to data if we know it
+ var sub = form.clk;
+ if (sub) {
+ var n = sub.name;
+ if (n && !sub.disabled) {
+ options.extraData = options.extraData || {};
+ options.extraData[n] = sub.value;
+ if (sub.type == "image") {
+ options.extraData[name+'.x'] = form.clk_x;
+ options.extraData[name+'.y'] = form.clk_y;
+ }
+ }
+ }
+
+ // take a breath so that pending repaints get some cpu time before the upload starts
+ setTimeout(function() {
+ // make sure form attrs are set
+ var t = $form.attr('target'), a = $form.attr('action');
+ $form.attr({
+ target: id,
+ method: 'POST',
+ action: opts.url
+ });
+
+ // ie borks in some cases when setting encoding
+ if (! options.skipEncodingOverride) {
+ $form.attr({
+ encoding: 'multipart/form-data',
+ enctype: 'multipart/form-data'
+ });
+ }
+
+ // support timout
+ if (opts.timeout)
+ setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
+
+ // add "extra" data to form if provided in options
+ var extraInputs = [];
+ try {
+ if (options.extraData)
+ for (var n in options.extraData)
+ extraInputs.push(
+ $('')
+ .appendTo(form)[0]);
+
+ // add iframe to doc and submit the form
+ $io.appendTo('body');
+ io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
+ form.submit();
+ }
+ finally {
+ // reset attrs and remove "extra" input elements
+ $form.attr('action', a);
+ t ? $form.attr('target', t) : $form.removeAttr('target');
+ $(extraInputs).remove();
+ }
+ }, 10);
+
+ function cb() {
+ if (cbInvoked++) return;
+
+ io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
+
+ var operaHack = 0;
+ var ok = true;
+ try {
+ if (timedOut) throw 'timeout';
+ // extract the server response from the iframe
+ var data, doc;
+
+ doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
+
+ if (doc.body == null && !operaHack && $.browser.opera) {
+ // In Opera 9.2.x the iframe DOM is not always traversable when
+ // the onload callback fires so we give Opera 100ms to right itself
+ operaHack = 1;
+ cbInvoked--;
+ setTimeout(cb, 100);
+ return;
+ }
+
+ xhr.responseText = doc.body ? doc.body.innerHTML : null;
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+ xhr.getResponseHeader = function(header){
+ var headers = {'content-type': opts.dataType};
+ return headers[header];
+ };
+
+ if (opts.dataType == 'json' || opts.dataType == 'script') {
+ var ta = doc.getElementsByTagName('textarea')[0];
+ xhr.responseText = ta ? ta.value : xhr.responseText;
+ }
+ else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+ xhr.responseXML = toXml(xhr.responseText);
+ }
+ data = $.httpData(xhr, opts.dataType);
+ }
+ catch(e){
+ ok = false;
+ $.handleError(opts, xhr, 'error', e);
+ }
+
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+ if (ok) {
+ opts.success(data, 'success');
+ if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
+ }
+ if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
+ if (g && ! --$.active) $.event.trigger("ajaxStop");
+ if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
+
+ // clean up
+ setTimeout(function() {
+ $io.remove();
+ xhr.responseXML = null;
+ }, 100);
+ };
+
+ function toXml(s, doc) {
+ if (window.ActiveXObject) {
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = 'false';
+ doc.loadXML(s);
+ }
+ else
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
+ return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
+ };
+ };
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for elements (if the element
+ * is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ * used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+ return this.ajaxFormUnbind().bind('submit.form-plugin',function() {
+ $(this).ajaxSubmit(options);
+ return false;
+ }).each(function() {
+ // store options in hash
+ $(":submit,input:image", this).bind('click.form-plugin',function(e) {
+ var form = this.form;
+ form.clk = this;
+ if (this.type == 'image') {
+ if (e.offsetX != undefined) {
+ form.clk_x = e.offsetX;
+ form.clk_y = e.offsetY;
+ } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+ var offset = $(this).offset();
+ form.clk_x = e.pageX - offset.left;
+ form.clk_y = e.pageY - offset.top;
+ } else {
+ form.clk_x = e.pageX - this.offsetLeft;
+ form.clk_y = e.pageY - this.offsetTop;
+ }
+ }
+ // clear form vars
+ setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
+ });
+ });
+};
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+ this.unbind('submit.form-plugin');
+ return this.each(function() {
+ $(":submit,input:image", this).unbind('click.form-plugin');
+ });
+
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+ var a = [];
+ if (this.length == 0) return a;
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) return a;
+ for(var i=0, max=els.length; i < max; i++) {
+ var el = els[i];
+ var n = el.name;
+ if (!n) continue;
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ continue;
+ }
+
+ var v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ for(var j=0, jmax=v.length; j < jmax; j++)
+ a.push({name: n, value: v[j]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: n, value: v});
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle them here
+ var inputs = form.getElementsByTagName("input");
+ for(var i=0, max=inputs.length; i < max; i++) {
+ var input = inputs[i];
+ var n = input.name;
+ if(n && !input.disabled && input.type == "image" && form.clk == input)
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+ //hand off to jQuery.param for proper encoding
+ return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+ var a = [];
+ this.each(function() {
+ var n = this.name;
+ if (!n) return;
+ var v = $.fieldValue(this, successful);
+ if (v && v.constructor == Array) {
+ for (var i=0,max=v.length; i < max; i++)
+ a.push({name: n, value: v[i]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: this.name, value: v});
+ });
+ //hand off to jQuery.param for proper encoding
+ return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set. For example, consider the following form:
+ *
+ *
+ *
+ * var v = $(':text').fieldValue();
+ * // if no values are entered into the text inputs
+ * v == ['','']
+ * // if values entered into the text inputs are 'foo' and 'bar'
+ * v == ['foo','bar']
+ *
+ * var v = $(':checkbox').fieldValue();
+ * // if neither checkbox is checked
+ * v === undefined
+ * // if both checkboxes are checked
+ * v == ['B1', 'B2']
+ *
+ * var v = $(':radio').fieldValue();
+ * // if neither radio is checked
+ * v === undefined
+ * // if first radio is checked
+ * v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array. If no valid value can be determined the
+ * array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+ for (var val=[], i=0, max=this.length; i < max; i++) {
+ var el = this[i];
+ var v = $.fieldValue(el, successful);
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
+ continue;
+ v.constructor == Array ? $.merge(val, v) : val.push(v);
+ }
+ return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (typeof successful == 'undefined') successful = true;
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1))
+ return null;
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) return null;
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ // extra pain for IE...
+ var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value;
+ if (one) return v;
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return el.value;
+};
+
+/**
+ * Clears the form data. Takes the following actions on the form's input fields:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ */
+$.fn.clearForm = function() {
+ return this.each(function() {
+ $('input,select,textarea', this).clearFields();
+ });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function() {
+ return this.each(function() {
+ var t = this.type, tag = this.tagName.toLowerCase();
+ if (t == 'file' || t == 'text' || t == 'password' || tag == 'textarea')
+ this.value = '';
+ else if (t == 'checkbox' || t == 'radio')
+ this.checked = false;
+ else if (tag == 'select')
+ this.selectedIndex = -1;
+ });
+};
+
+/**
+ * Resets the form data. Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+ return this.each(function() {
+ // guard against an input with the name of 'reset'
+ // note that IE reports the reset function as an 'object'
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
+ this.reset();
+ });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+ if (b == undefined) b = true;
+ return this.each(function() {
+ this.disabled = !b
+ });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+ if (select == undefined) select = true;
+ return this.each(function() {
+ var t = this.type;
+ if (t == 'checkbox' || t == 'radio')
+ this.checked = select;
+ else if (this.tagName.toLowerCase() == 'option') {
+ var $sel = $(this).parent('select');
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
+ // deselect all other options
+ $sel.find('option').selected(false);
+ }
+ this.selected = select;
+ }
+ });
+};
+
+// helper fn for console logging
+// set $.fn.ajaxSubmit.debug to true to enable debug logging
+function log() {
+ if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
+ window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
+};
+
+})(jQuery);
diff --git a/js/jquery.form.min.js b/js/jquery.form.min.js
index 7ecc4ca923..b6a0ed1eb2 100644
--- a/js/jquery.form.min.js
+++ b/js/jquery.form.min.js
@@ -1,11 +1 @@
-/*
- * jQuery Form Plugin
- * version: 2.63 (29-JAN-2011)
- * @requires jQuery v1.3.2 or later
- *
- * Examples and documentation at: http://malsup.com/jquery/form/
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- */
-(function(b){b.fn.ajaxSubmit=function(t){if(!this.length){a("ajaxSubmit: skipping submit process - no element selected");return this}if(typeof t=="function"){t={success:t}}var h=this.attr("action");var d=(typeof h==="string")?b.trim(h):"";if(d){d=(d.match(/^([^#]+)/)||[])[1]}d=d||window.location.href||"";t=b.extend(true,{url:d,type:this[0].getAttribute("method")||"GET",iframeSrc:/^https/i.test(window.location.href||"")?"javascript:false":"about:blank"},t);var u={};this.trigger("form-pre-serialize",[this,t,u]);if(u.veto){a("ajaxSubmit: submit vetoed via form-pre-serialize trigger");return this}if(t.beforeSerialize&&t.beforeSerialize(this,t)===false){a("ajaxSubmit: submit aborted via beforeSerialize callback");return this}var f,p,m=this.formToArray(t.semantic);if(t.data){t.extraData=t.data;for(f in t.data){if(t.data[f] instanceof Array){for(var i in t.data[f]){m.push({name:f,value:t.data[f][i]})}}else{p=t.data[f];p=b.isFunction(p)?p():p;m.push({name:f,value:p})}}}if(t.beforeSubmit&&t.beforeSubmit(m,this,t)===false){a("ajaxSubmit: submit aborted via beforeSubmit callback");return this}this.trigger("form-submit-validate",[m,this,t,u]);if(u.veto){a("ajaxSubmit: submit vetoed via form-submit-validate trigger");return this}var c=b.param(m);if(t.type.toUpperCase()=="GET"){t.url+=(t.url.indexOf("?")>=0?"&":"?")+c;t.data=null}else{t.data=c}var s=this,l=[];if(t.resetForm){l.push(function(){s.resetForm()})}if(t.clearForm){l.push(function(){s.clearForm()})}if(!t.dataType&&t.target){var r=t.success||function(){};l.push(function(n){var k=t.replaceTarget?"replaceWith":"html";b(t.target)[k](n).each(r,arguments)})}else{if(t.success){l.push(t.success)}}t.success=function(w,n,x){var v=t.context||t;for(var q=0,k=l.length;q0;var e="multipart/form-data";var j=(s.attr("enctype")==e||s.attr("encoding")==e);if(t.iframe!==false&&(g||t.iframe||j)){if(t.closeKeepAlive){b.get(t.closeKeepAlive,o)}else{o()}}else{b.ajax(t)}this.trigger("form-submit-notify",[this,t]);return this;function o(){var v=s[0];if(b(":input[name=submit],:input[id=submit]",v).length){alert('Error: Form elements must not have name or id of "submit".');return}var B=b.extend(true,{},b.ajaxSettings,t);B.context=B.context||B;var E="jqFormIO"+(new Date().getTime()),z="_"+E;var w=b('');var A=w[0];w.css({position:"absolute",top:"-1000px",left:"-1000px"});var x={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(){this.aborted=1;w.attr("src",B.iframeSrc)}};var I=B.global;if(I&&!b.active++){b.event.trigger("ajaxStart")}if(I){b.event.trigger("ajaxSend",[x,B])}if(B.beforeSend&&B.beforeSend.call(B.context,x,B)===false){if(B.global){b.active--}return}if(x.aborted){return}var H=0;var y=v.clk;if(y){var F=y.name;if(F&&!y.disabled){B.extraData=B.extraData||{};B.extraData[F]=y.value;if(y.type=="image"){B.extraData[F+".x"]=v.clk_x;B.extraData[F+".y"]=v.clk_y}}}function G(){var O=s.attr("target"),M=s.attr("action");v.setAttribute("target",E);if(v.getAttribute("method")!="POST"){v.setAttribute("method","POST")}if(v.getAttribute("action")!=B.url){v.setAttribute("action",B.url)}if(!B.skipEncodingOverride){s.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"})}if(B.timeout){setTimeout(function(){H=true;D()},B.timeout)}var N=[];try{if(B.extraData){for(var P in B.extraData){N.push(b('').appendTo(v)[0])}}w.appendTo("body");A.attachEvent?A.attachEvent("onload",D):A.addEventListener("load",D,false);v.submit()}finally{v.setAttribute("action",M);if(O){v.setAttribute("target",O)}else{s.removeAttr("target")}b(N).remove()}}if(B.forceSync){G()}else{setTimeout(G,10)}var K,L,J=50;function D(){L=A.contentWindow?A.contentWindow.document:A.contentDocument?A.contentDocument:A.document;if(!L||L.location.href==B.iframeSrc){return}A.detachEvent?A.detachEvent("onload",D):A.removeEventListener("load",D,false);var N=true;try{if(H){throw"timeout"}var R=B.dataType=="xml"||L.XMLDocument||b.isXMLDoc(L);a("isXml="+R);if(!R&&window.opera&&(L.body==null||L.body.innerHTML=="")){if(--J){a("requeing onLoad callback, DOM not available");setTimeout(D,250);return}}x.responseText=L.body?L.body.innerHTML:L.documentElement?L.documentElement.innerHTML:null;x.responseXML=L.XMLDocument?L.XMLDocument:L;x.getResponseHeader=function(T){var S={"content-type":B.dataType};return S[T]};var Q=/(json|script)/.test(B.dataType);if(Q||B.textarea){var M=L.getElementsByTagName("textarea")[0];if(M){x.responseText=M.value}else{if(Q){var P=L.getElementsByTagName("pre")[0];var n=L.getElementsByTagName("body")[0];if(P){x.responseText=P.textContent}else{if(n){x.responseText=n.innerHTML}}}}}else{if(B.dataType=="xml"&&!x.responseXML&&x.responseText!=null){x.responseXML=C(x.responseText)}}K=k(x,B.dataType,B)}catch(O){a("error caught:",O);N=false;x.error=O;B.error.call(B.context,x,"error",O);I&&b.event.trigger("ajaxError",[x,B,O])}if(x.aborted){a("upload aborted");N=false}if(N){B.success.call(B.context,K,"success",x);I&&b.event.trigger("ajaxSuccess",[x,B])}I&&b.event.trigger("ajaxComplete",[x,B]);if(I&&!--b.active){b.event.trigger("ajaxStop")}B.complete&&B.complete.call(B.context,x,N?"success":"error");setTimeout(function(){w.removeData("form-plugin-onload");w.remove();x.responseXML=null},100)}var C=b.parseXML||function(n,M){if(window.ActiveXObject){M=new ActiveXObject("Microsoft.XMLDOM");M.async="false";M.loadXML(n)}else{M=(new DOMParser()).parseFromString(n,"text/xml")}return(M&&M.documentElement&&M.documentElement.nodeName!="parsererror")?M:null};var q=b.parseJSON||function(n){return window["eval"]("("+n+")")};var k=function(Q,O,N){var M=Q.getResponseHeader("content-type")||"",n=O==="xml"||!O&&M.indexOf("xml")>=0,P=n?Q.responseXML:Q.responseText;if(n&&P.documentElement.nodeName==="parsererror"){b.error&&b.error("parsererror")}if(N&&N.dataFilter){P=N.dataFilter(P,O)}if(typeof P==="string"){if(O==="json"||!O&&M.indexOf("json")>=0){P=q(P)}else{if(O==="script"||!O&&M.indexOf("javascript")>=0){b.globalEval(P)}}}return P}}};b.fn.ajaxForm=function(c){if(this.length===0){var d={s:this.selector,c:this.context};if(!b.isReady&&d.s){a("DOM not ready, queuing ajaxForm");b(function(){b(d.s,d.c).ajaxForm(c)});return this}a("terminating; zero elements found by selector"+(b.isReady?"":" (DOM not ready)"));return this}return this.ajaxFormUnbind().bind("submit.form-plugin",function(f){if(!f.isDefaultPrevented()){f.preventDefault();b(this).ajaxSubmit(c)}}).bind("click.form-plugin",function(j){var i=j.target;var g=b(i);if(!(g.is(":submit,input:image"))){var f=g.closest(":submit");if(f.length==0){return}i=f[0]}var h=this;h.clk=i;if(i.type=="image"){if(j.offsetX!=undefined){h.clk_x=j.offsetX;h.clk_y=j.offsetY}else{if(typeof b.fn.offset=="function"){var k=g.offset();h.clk_x=j.pageX-k.left;h.clk_y=j.pageY-k.top}else{h.clk_x=j.pageX-i.offsetLeft;h.clk_y=j.pageY-i.offsetTop}}}setTimeout(function(){h.clk=h.clk_x=h.clk_y=null},100)})};b.fn.ajaxFormUnbind=function(){return this.unbind("submit.form-plugin click.form-plugin")};b.fn.formToArray=function(q){var p=[];if(this.length===0){return p}var d=this[0];var g=q?d.getElementsByTagName("*"):d.elements;if(!g){return p}var k,h,f,r,e,m,c;for(k=0,m=g.length;k=0?"&":"?")+d;p.data=null}else{p.data=d}var r=this,h=[];if(p.resetForm){h.push(function(){r.resetForm()})}if(p.clearForm){h.push(function(){r.clearForm()})}if(!p.dataType&&p.target){var m=p.success||function(){};h.push(function(j){b(p.target).html(j).each(m,arguments)})}else{if(p.success){h.push(p.success)}}p.success=function(q,k){for(var n=0,j=h.length;n');var B=z[0];if(b.browser.msie||b.browser.opera){B.src='javascript:false;document.write("");'}z.css({position:"absolute",top:"-1000px",left:"-1000px"});var C={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(){this.aborted=1;z.attr("src","about:blank")}};var A=q.global;if(A&&!b.active++){b.event.trigger("ajaxStart")}if(A){b.event.trigger("ajaxSend",[C,q])}if(D.beforeSend&&D.beforeSend(C,D)===false){D.global&&jQuery.active--;return}if(C.aborted){return}var k=0;var w=0;var j=u.clk;if(j){var v=j.name;if(v&&!j.disabled){p.extraData=p.extraData||{};p.extraData[v]=j.value;if(j.type=="image"){p.extraData[name+".x"]=u.clk_x;p.extraData[name+".y"]=u.clk_y}}}setTimeout(function(){var G=r.attr("target"),E=r.attr("action");r.attr({target:t,method:"POST",action:q.url});if(!p.skipEncodingOverride){r.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"})}if(q.timeout){setTimeout(function(){w=true;x()},q.timeout)}var F=[];try{if(p.extraData){for(var H in p.extraData){F.push(b('').appendTo(u)[0])}}z.appendTo("body");B.attachEvent?B.attachEvent("onload",x):B.addEventListener("load",x,false);u.submit()}finally{r.attr("action",E);G?r.attr("target",G):r.removeAttr("target");b(F).remove()}},10);function x(){if(k++){return}B.detachEvent?B.detachEvent("onload",x):B.removeEventListener("load",x,false);var E=0;var F=true;try{if(w){throw"timeout"}var G,I;I=B.contentWindow?B.contentWindow.document:B.contentDocument?B.contentDocument:B.document;if(I.body==null&&!E&&b.browser.opera){E=1;k--;setTimeout(x,100);return}C.responseText=I.body?I.body.innerHTML:null;C.responseXML=I.XMLDocument?I.XMLDocument:I;C.getResponseHeader=function(K){var J={"content-type":q.dataType};return J[K]};if(q.dataType=="json"||q.dataType=="script"){var n=I.getElementsByTagName("textarea")[0];C.responseText=n?n.value:C.responseText}else{if(q.dataType=="xml"&&!C.responseXML&&C.responseText!=null){C.responseXML=y(C.responseText)}}G=b.httpData(C,q.dataType)}catch(H){F=false;b.handleError(q,C,"error",H)}if(F){q.success(G,"success");if(A){b.event.trigger("ajaxSuccess",[C,q])}}if(A){b.event.trigger("ajaxComplete",[C,q])}if(A&&!--b.active){b.event.trigger("ajaxStop")}if(q.complete){q.complete(C,F?"success":"error")}setTimeout(function(){z.remove();C.responseXML=null},100)}function y(n,E){if(window.ActiveXObject){E=new ActiveXObject("Microsoft.XMLDOM");E.async="false";E.loadXML(n)}else{E=(new DOMParser()).parseFromString(n,"text/xml")}return(E&&E.documentElement&&E.documentElement.tagName!="parsererror")?E:null}}};b.fn.ajaxForm=function(c){return this.ajaxFormUnbind().bind("submit.form-plugin",function(){b(this).ajaxSubmit(c);return false}).each(function(){b(":submit,input:image",this).bind("click.form-plugin",function(f){var d=this.form;d.clk=this;if(this.type=="image"){if(f.offsetX!=undefined){d.clk_x=f.offsetX;d.clk_y=f.offsetY}else{if(typeof b.fn.offset=="function"){var g=b(this).offset();d.clk_x=f.pageX-g.left;d.clk_y=f.pageY-g.top}else{d.clk_x=f.pageX-this.offsetLeft;d.clk_y=f.pageY-this.offsetTop}}}setTimeout(function(){d.clk=d.clk_x=d.clk_y=null},10)})})};b.fn.ajaxFormUnbind=function(){this.unbind("submit.form-plugin");return this.each(function(){b(":submit,input:image",this).unbind("click.form-plugin")})};b.fn.formToArray=function(q){var p=[];if(this.length==0){return p}var d=this[0];var h=q?d.getElementsByTagName("*"):d.elements;if(!h){return p}for(var k=0,m=h.length;kactor->outputTo($xs, 'author');
+
+ // XXX: Remove ASAP! Author information
+ // has been moved to the author element in the Activity
+ // Streams spec. We're outputting actor only for backward
+ // compatibility with clients that can only parse
+ // activities based on older versions of the spec.
+
+ $depMsg = 'Deprecation warning: activity:actor is present '
+ . 'only for backward compatibility. It will be '
+ . 'removed in the next version of StatusNet.';
+ $xs->comment($depMsg);
+ $this->actor->outputTo($xs, 'activity:actor');
}
if ($this->verb != ActivityVerb::POST || count($this->objects) != 1) {
diff --git a/lib/apiaction.php b/lib/apiaction.php
index 0b539bb397..5d70425720 100644
--- a/lib/apiaction.php
+++ b/lib/apiaction.php
@@ -263,8 +263,7 @@ class ApiAction extends Action
? Design::url($design->backgroundimage) : '';
$twitter_user['profile_background_tile']
- = empty($design->disposition)
- ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
+ = (bool)($design->disposition & BACKGROUND_TILE);
$twitter_user['statuses_count'] = $profile->noticeCount();
@@ -1236,9 +1235,12 @@ class ApiAction extends Action
return;
}
- function clientError($msg, $code = 400, $format = 'xml')
+ function clientError($msg, $code = 400, $format = null)
{
$action = $this->trimmed('action');
+ if ($format === null) {
+ $format = $this->format;
+ }
common_debug("User error '$code' on '$action': $msg", __FILE__);
@@ -1278,9 +1280,12 @@ class ApiAction extends Action
}
}
- function serverError($msg, $code = 500, $content_type = 'xml')
+ function serverError($msg, $code = 500, $content_type = null)
{
$action = $this->trimmed('action');
+ if ($content_type === null) {
+ $content_type = $this->format;
+ }
common_debug("Server error '$code' on '$action': $msg", __FILE__);
diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
index 4e7f992662..817191b64a 100644
--- a/lib/atomgroupnoticefeed.php
+++ b/lib/atomgroupnoticefeed.php
@@ -91,8 +91,16 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed
$ao = ActivityObject::fromGroup($group);
- $this->addAuthorRaw($ao->asString('author').
- $ao->asString('activity:subject'));
+ $this->addAuthorRaw($ao->asString('author'));
+
+ $depMsg = 'Deprecation warning: activity:subject is present '
+ . 'only for backward compatibility. It will be '
+ . 'removed in the next version of StatusNet.';
+
+ $this->addAuthorRaw(
+ "\n"
+ . $ao->asString('activity:subject')
+ );
$this->addLink($group->homeUrl());
}
diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php
index 5ca089b859..3398cc8b4d 100644
--- a/lib/atomusernoticefeed.php
+++ b/lib/atomusernoticefeed.php
@@ -59,9 +59,29 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
parent::__construct($cur, $indent);
$this->user = $user;
if (!empty($user)) {
+
$profile = $user->getProfile();
+
$ao = ActivityObject::fromProfile($profile);
+
+ $ao->extra[] = $profile->profileInfo($cur);
+
+ // XXX: For users, we generate an author _AND_ an
+ // This is for backward compatibility with clients (especially
+ // StatusNet's clients) that assume the Atom will conform to an
+ // older version of the Activity Streams API. Subject should be
+ // removed in future versions of StatusNet.
+
$this->addAuthorRaw($ao->asString('author'));
+
+ $depMsg = 'Deprecation warning: activity:subject is present '
+ . 'only for backward compatibility. It will be '
+ . 'removed in the next version of StatusNet.';
+
+ $this->addAuthorRaw(
+ "\n"
+ . $ao->asString('activity:subject')
+ );
}
// TRANS: Title in atom user notice feed. %s is a user name.
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
index 7e536925bf..c3d3f4d116 100644
--- a/lib/attachmentlist.php
+++ b/lib/attachmentlist.php
@@ -76,8 +76,7 @@ class AttachmentList extends Widget
*/
function show()
{
- $atts = new File;
- $att = $atts->getAttachments($this->notice->id);
+ $att = File::getAttachments($this->notice->id);
if (empty($att)) return 0;
$this->showListStart();
diff --git a/lib/cache.php b/lib/cache.php
index dc667654ab..bf0603c62d 100644
--- a/lib/cache.php
+++ b/lib/cache.php
@@ -164,6 +164,7 @@ class Cache
{
$value = false;
+ common_perf_counter('Cache::get', $key);
if (Event::handle('StartCacheGet', array(&$key, &$value))) {
if (array_key_exists($key, $this->_items)) {
$value = unserialize($this->_items[$key]);
@@ -188,6 +189,7 @@ class Cache
{
$success = false;
+ common_perf_counter('Cache::set', $key);
if (Event::handle('StartCacheSet', array(&$key, &$value, &$flag,
&$expiry, &$success))) {
@@ -214,6 +216,7 @@ class Cache
function increment($key, $step=1)
{
$value = false;
+ common_perf_counter('Cache::increment', $key);
if (Event::handle('StartCacheIncrement', array(&$key, &$step, &$value))) {
// Fallback is not guaranteed to be atomic,
// and may original expiry value.
@@ -239,6 +242,7 @@ class Cache
{
$success = false;
+ common_perf_counter('Cache::delete', $key);
if (Event::handle('StartCacheDelete', array(&$key, &$success))) {
if (array_key_exists($key, $this->_items)) {
unset($this->_items[$key]);
diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php
index c288c2e5f0..f2caf48bdb 100644
--- a/lib/commandinterpreter.php
+++ b/lib/commandinterpreter.php
@@ -25,252 +25,287 @@ class CommandInterpreter
{
function handle_command($user, $text)
{
- # XXX: localise
+ // XXX: localise
$text = preg_replace('/\s+/', ' ', trim($text));
list($cmd, $arg) = $this->split_arg($text);
- # We try to support all the same commands as Twitter, see
- # http://getsatisfaction.com/twitter/topics/what_are_the_twitter_commands
- # There are a few compatibility commands from earlier versions of
- # StatusNet
+ // We try to support all the same commands as Twitter, see
+ // http://getsatisfaction.com/twitter/topics/what_are_the_twitter_commands
+ // There are a few compatibility commands from earlier versions of
+ // StatusNet
- switch(strtolower($cmd)) {
- case 'help':
- if ($arg) {
- return null;
- }
- return new HelpCommand($user);
- case 'login':
- if ($arg) {
- return null;
- } else {
- return new LoginCommand($user);
- }
- case 'lose':
- if ($arg) {
+ $cmd = strtolower($cmd);
+
+ if (Event::handle('StartIntepretCommand', array($cmd, $arg, $user, &$result))) {
+ switch($cmd) {
+ case 'help':
+ if ($arg) {
+ $result = null;
+ }
+ $result = new HelpCommand($user);
+ break;
+ case 'login':
+ if ($arg) {
+ $result = null;
+ } else {
+ $result = new LoginCommand($user);
+ }
+ break;
+ case 'lose':
+ if ($arg) {
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new LoseCommand($user, $other);
+ }
+ } else {
+ $result = null;
+ }
+ break;
+ case 'subscribers':
+ if ($arg) {
+ $result = null;
+ } else {
+ $result = new SubscribersCommand($user);
+ }
+ break;
+ case 'subscriptions':
+ if ($arg) {
+ $result = null;
+ } else {
+ $result = new SubscriptionsCommand($user);
+ }
+ break;
+ case 'groups':
+ if ($arg) {
+ $result = null;
+ } else {
+ $result = new GroupsCommand($user);
+ }
+ break;
+ case 'on':
+ if ($arg) {
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new OnCommand($user, $other);
+ }
+ } else {
+ $result = new OnCommand($user);
+ }
+ break;
+ case 'off':
+ if ($arg) {
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new OffCommand($user, $other);
+ }
+ } else {
+ $result = new OffCommand($user);
+ }
+ break;
+ case 'stop':
+ case 'quit':
+ if ($arg) {
+ $result = null;
+ } else {
+ $result = new OffCommand($user);
+ }
+ break;
+ case 'join':
+ if (!$arg) {
+ $result = null;
+ }
list($other, $extra) = $this->split_arg($arg);
if ($extra) {
- return null;
+ $result = null;
} else {
- return new LoseCommand($user, $other);
+ $result = new JoinCommand($user, $other);
+ }
+ break;
+ case 'drop':
+ if (!$arg) {
+ $result = null;
}
- } else {
- return null;
- }
- case 'subscribers':
- if ($arg) {
- return null;
- } else {
- return new SubscribersCommand($user);
- }
- case 'subscriptions':
- if ($arg) {
- return null;
- } else {
- return new SubscriptionsCommand($user);
- }
- case 'groups':
- if ($arg) {
- return null;
- } else {
- return new GroupsCommand($user);
- }
- case 'on':
- if ($arg) {
list($other, $extra) = $this->split_arg($arg);
if ($extra) {
- return null;
+ $result = null;
} else {
- return new OnCommand($user, $other);
+ $result = new DropCommand($user, $other);
}
- } else {
- return new OnCommand($user);
- }
- case 'off':
- if ($arg) {
+ break;
+ case 'follow':
+ case 'sub':
+ if (!$arg) {
+ $result = null;
+ }
+
list($other, $extra) = $this->split_arg($arg);
if ($extra) {
- return null;
+ $result = null;
} else {
- return new OffCommand($user, $other);
+ $result = new SubCommand($user, $other);
}
- } else {
- return new OffCommand($user);
+ break;
+ case 'leave':
+ case 'unsub':
+ if (!$arg) {
+ $result = null;
+ }
+
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new UnsubCommand($user, $other);
+ }
+ break;
+ case 'get':
+ case 'last':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new GetCommand($user, $other);
+ }
+ break;
+ case 'd':
+ case 'dm':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if (!$extra) {
+ $result = null;
+ } else {
+ $result = new MessageCommand($user, $other, $extra);
+ }
+ break;
+ case 'r':
+ case 'reply':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if (!$extra) {
+ $result = null;
+ } else {
+ $result = new ReplyCommand($user, $other, $extra);
+ }
+ break;
+ case 'repeat':
+ case 'rp':
+ case 'rt':
+ case 'rd':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new RepeatCommand($user, $other);
+ }
+ break;
+ case 'whois':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new WhoisCommand($user, $other);
+ }
+ break;
+ case 'fav':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new FavCommand($user, $other);
+ }
+ break;
+ case 'nudge':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new NudgeCommand($user, $other);
+ }
+ break;
+ case 'stats':
+ if ($arg) {
+ $result = null;
+ }
+ $result = new StatsCommand($user);
+ break;
+ case 'invite':
+ if (!$arg) {
+ $result = null;
+ }
+ list($other, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else {
+ $result = new InviteCommand($user, $other);
+ }
+ break;
+ case 'track':
+ if (!$arg) {
+ $result = null;
+ }
+ list($word, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else if ($word == 'off') {
+ $result = new TrackOffCommand($user);
+ } else {
+ $result = new TrackCommand($user, $word);
+ }
+ break;
+ case 'untrack':
+ if (!$arg) {
+ $result = null;
+ }
+ list($word, $extra) = $this->split_arg($arg);
+ if ($extra) {
+ $result = null;
+ } else if ($word == 'all') {
+ $result = new TrackOffCommand($user);
+ } else {
+ $result = new UntrackCommand($user, $word);
+ }
+ break;
+ case 'tracks':
+ case 'tracking':
+ if ($arg) {
+ $result = null;
+ }
+ $result = new TrackingCommand($user);
+ break;
+ default:
+ $result = false;
}
- case 'stop':
- case 'quit':
- if ($arg) {
- return null;
- } else {
- return new OffCommand($user);
- }
- case 'join':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new JoinCommand($user, $other);
- }
- case 'drop':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new DropCommand($user, $other);
- }
- case 'follow':
- case 'sub':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new SubCommand($user, $other);
- }
- case 'leave':
- case 'unsub':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new UnsubCommand($user, $other);
- }
- case 'get':
- case 'last':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new GetCommand($user, $other);
- }
- case 'd':
- case 'dm':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if (!$extra) {
- return null;
- } else {
- return new MessageCommand($user, $other, $extra);
- }
- case 'r':
- case 'reply':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if (!$extra) {
- return null;
- } else {
- return new ReplyCommand($user, $other, $extra);
- }
- case 'repeat':
- case 'rp':
- case 'rt':
- case 'rd':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new RepeatCommand($user, $other);
- }
- case 'whois':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new WhoisCommand($user, $other);
- }
- case 'fav':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new FavCommand($user, $other);
- }
- case 'nudge':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new NudgeCommand($user, $other);
- }
- case 'stats':
- if ($arg) {
- return null;
- }
- return new StatsCommand($user);
- case 'invite':
- if (!$arg) {
- return null;
- }
- list($other, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else {
- return new InviteCommand($user, $other);
- }
- case 'track':
- if (!$arg) {
- return null;
- }
- list($word, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else if ($word == 'off') {
- return new TrackOffCommand($user);
- } else {
- return new TrackCommand($user, $word);
- }
- case 'untrack':
- if (!$arg) {
- return null;
- }
- list($word, $extra) = $this->split_arg($arg);
- if ($extra) {
- return null;
- } else if ($word == 'all') {
- return new TrackOffCommand($user);
- } else {
- return new UntrackCommand($user, $word);
- }
- case 'tracks':
- case 'tracking':
- if ($arg) {
- return null;
- }
- return new TrackingCommand($user);
- default:
- return false;
+
+ Event::handle('EndInterpretCommand', array($cmd, $arg, $user, $result));
}
+
+ return $result;
}
/**
diff --git a/lib/common.php b/lib/common.php
index 08779fb6fd..35a5391052 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -23,7 +23,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
define('STATUSNET_BASE_VERSION', '0.9.7');
-define('STATUSNET_LIFECYCLE', 'alpha1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
+define('STATUSNET_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE);
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
@@ -36,6 +36,7 @@ define('AVATAR_MINI_SIZE', 24);
define('NOTICES_PER_PAGE', 20);
define('PROFILES_PER_PAGE', 20);
+define('MESSAGES_PER_PAGE', 20);
define('FOREIGN_NOTICE_SEND', 1);
define('FOREIGN_NOTICE_RECV', 2);
diff --git a/lib/default.php b/lib/default.php
index ce61de5ea5..7d8b1fec7a 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -39,6 +39,8 @@ $default =
'logo' => null,
'ssllogo' => null,
'logdebug' => false,
+ 'logperf' => false, // Enable to dump performance counters to syslog
+ 'logperf_detail' => false, // Enable to dump every counter hit
'fancy' => false,
'locale_path' => INSTALLDIR.'/locale',
'language' => 'en',
@@ -269,8 +271,9 @@ $default =
'search' =>
array('type' => 'fulltext'),
'sessions' =>
- array('handle' => false, // whether to handle sessions ourselves
- 'debug' => false), // debugging output for sessions
+ array('handle' => false, // whether to handle sessions ourselves
+ 'debug' => false, // debugging output for sessions
+ 'gc_limit' => 1000), // max sessions to expire at a time
'design' =>
array('backgroundcolor' => null, // null -> 'use theme default'
'contentcolor' => null,
@@ -311,6 +314,9 @@ $default =
'RSSCloud' => null,
'OpenID' => null),
'locale_path' => false, // Set to a path to use *instead of* each plugin's own locale subdirectories
+ 'server' => null,
+ 'sslserver' => null,
+ 'path' => null,
),
'admin' =>
array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'license')),
diff --git a/lib/groupeditform.php b/lib/groupeditform.php
index 8a111b4642..3a2cf6bf4a 100644
--- a/lib/groupeditform.php
+++ b/lib/groupeditform.php
@@ -139,51 +139,54 @@ class GroupEditForm extends Form
}
$this->out->elementStart('ul', 'form_data');
- $this->out->elementStart('li');
- $this->out->hidden('groupid', $id);
- $this->out->input('nickname', _('Nickname'),
- ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname,
- _('1-64 lowercase letters or numbers, no punctuation or spaces.'));
- $this->out->elementEnd('li');
- $this->out->elementStart('li');
- $this->out->input('fullname', _('Full name'),
- ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname);
- $this->out->elementEnd('li');
- $this->out->elementStart('li');
- $this->out->input('homepage', _('Homepage'),
- ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
- _('URL of the homepage or blog of the group or topic.'));
- $this->out->elementEnd('li');
- $this->out->elementStart('li');
- $desclimit = User_group::maxDescription();
- if ($desclimit == 0) {
- $descinstr = _('Describe the group or topic');
- } else {
- $descinstr = sprintf(_m('Describe the group or topic in %d character or less.',
- 'Describe the group or topic in %d characters or less.',
- $desclimit),
- $desclimit);
- }
- $this->out->textarea('description', _('Description'),
- ($this->out->arg('description')) ? $this->out->arg('description') : $description,
- $descinstr);
- $this->out->elementEnd('li');
- $this->out->elementStart('li');
- $this->out->input('location', _('Location'),
- ($this->out->arg('location')) ? $this->out->arg('location') : $location,
- _('Location for the group, if any, like "City, State (or Region), Country".'));
- $this->out->elementEnd('li');
- if (common_config('group', 'maxaliases') > 0) {
- $aliases = (empty($this->group)) ? array() : $this->group->getAliases();
+ if (Event::handle('StartGroupEditFormData', array($this))) {
$this->out->elementStart('li');
- $this->out->input('aliases', _('Aliases'),
- ($this->out->arg('aliases')) ? $this->out->arg('aliases') :
- (!empty($aliases)) ? implode(' ', $aliases) : '',
- sprintf(_m('Extra nicknames for the group, separated with commas or spaces. Maximum %d alias allowed.',
- 'Extra nicknames for the group, separated with commas or spaces. Maximum %d aliases allowed.',
- common_config('group', 'maxaliases')),
- common_config('group', 'maxaliases')));;
+ $this->out->hidden('groupid', $id);
+ $this->out->input('nickname', _('Nickname'),
+ ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname,
+ _('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->out->elementEnd('li');
+ $this->out->elementStart('li');
+ $this->out->input('fullname', _('Full name'),
+ ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname);
+ $this->out->elementEnd('li');
+ $this->out->elementStart('li');
+ $this->out->input('homepage', _('Homepage'),
+ ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
+ _('URL of the homepage or blog of the group or topic.'));
+ $this->out->elementEnd('li');
+ $this->out->elementStart('li');
+ $desclimit = User_group::maxDescription();
+ if ($desclimit == 0) {
+ $descinstr = _('Describe the group or topic');
+ } else {
+ $descinstr = sprintf(_m('Describe the group or topic in %d character or less',
+ 'Describe the group or topic in %d characters or less',
+ $desclimit),
+ $desclimit);
+ }
+ $this->out->textarea('description', _('Description'),
+ ($this->out->arg('description')) ? $this->out->arg('description') : $description,
+ $descinstr);
+ $this->out->elementEnd('li');
+ $this->out->elementStart('li');
+ $this->out->input('location', _('Location'),
+ ($this->out->arg('location')) ? $this->out->arg('location') : $location,
+ _('Location for the group, if any, like "City, State (or Region), Country".'));
+ $this->out->elementEnd('li');
+ if (common_config('group', 'maxaliases') > 0) {
+ $aliases = (empty($this->group)) ? array() : $this->group->getAliases();
+ $this->out->elementStart('li');
+ $this->out->input('aliases', _('Aliases'),
+ ($this->out->arg('aliases')) ? $this->out->arg('aliases') :
+ (!empty($aliases)) ? implode(' ', $aliases) : '',
+ sprintf(_m('Extra nicknames for the group, separated with commas or spaces. Maximum %d alias allowed.',
+ 'Extra nicknames for the group, separated with commas or spaces. Maximum %d aliases allowed.',
+ common_config('group', 'maxaliases')),
+ common_config('group', 'maxaliases')));;
+ $this->out->elementEnd('li');
+ }
+ Event::handle('EndGroupEditFormData', array($this));
}
$this->out->elementEnd('ul');
}
diff --git a/lib/mailbox.php b/lib/mailbox.php
index 2b00f5ffde..7faeb7dba3 100644
--- a/lib/mailbox.php
+++ b/lib/mailbox.php
@@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
-define('MESSAGES_PER_PAGE', 20);
-
/**
* common superclass for direct messages inbox and outbox
*
@@ -111,32 +109,22 @@ class MailboxAction extends CurrentUserDesignAction
$message = $this->getMessages();
if ($message) {
- $cnt = 0;
- $this->elementStart('div', array('id' =>'notices_primary'));
- $this->element('h2', null, _('Notices'));
- $this->elementStart('ul', 'notices');
- while ($message->fetch() && $cnt <= MESSAGES_PER_PAGE) {
- $cnt++;
+ $ml = $this->getMessageList($message);
- if ($cnt > MESSAGES_PER_PAGE) {
- break;
- }
+ $cnt = $ml->show();
- $this->showMessage($message);
- }
-
- $this->elementEnd('ul');
-
- $this->pagination($this->page > 1, $cnt > MESSAGES_PER_PAGE,
- $this->page, $this->trimmed('action'),
+ $this->pagination($this->page > 1,
+ $cnt > MESSAGES_PER_PAGE,
+ $this->page,
+ $this->trimmed('action'),
array('nickname' => $this->user->nickname));
- $this->elementEnd('div');
- $message->free();
- unset($message);
- }
- else {
- $this->element('p', 'guide', _('You have no private messages. You can send private message to engage other users in conversation. People can send you messages for your eyes only.'));
+ } else {
+ $this->element('p',
+ 'guide',
+ _('You have no private messages. '.
+ 'You can send private message to engage other users in conversation. '.
+ 'People can send you messages for your eyes only.'));
}
}
@@ -145,95 +133,11 @@ class MailboxAction extends CurrentUserDesignAction
return null;
}
- /**
- * returns the profile we want to show with the message
- *
- * For inboxes, we show the sender; for outboxes, the recipient.
- *
- * @param Message $message The message to get the profile for
- *
- * @return Profile The profile that matches the message
- */
-
- function getMessageProfile($message)
+ function getMessageList($message)
{
return null;
}
- /**
- * show a single message in the list format
- *
- * XXX: This needs to be extracted out into a MessageList similar
- * to NoticeList.
- *
- * @param Message $message the message to show
- *
- * @return void
- */
-
- function showMessage($message)
- {
- $this->elementStart('li', array('class' => 'hentry notice',
- 'id' => 'message-' . $message->id));
-
- $profile = $this->getMessageProfile($message);
-
- $this->elementStart('div', 'entry-title');
- $this->elementStart('span', 'vcard author');
- $this->elementStart('a', array('href' => $profile->profileurl,
- 'class' => 'url'));
- $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
- $this->element('img', array('src' => ($avatar) ?
- $avatar->displayUrl() :
- Avatar::defaultImage(AVATAR_STREAM_SIZE),
- 'class' => 'photo avatar',
- 'width' => AVATAR_STREAM_SIZE,
- 'height' => AVATAR_STREAM_SIZE,
- 'alt' =>
- ($profile->fullname) ? $profile->fullname :
- $profile->nickname));
- $this->element('span', array('class' => 'nickname fn'),
- $profile->nickname);
- $this->elementEnd('a');
- $this->elementEnd('span');
-
- // FIXME: URL, image, video, audio
- $this->elementStart('p', array('class' => 'entry-content'));
- $this->raw($message->rendered);
- $this->elementEnd('p');
- $this->elementEnd('div');
-
- $messageurl = common_local_url('showmessage',
- array('message' => $message->id));
-
- // XXX: we need to figure this out better. Is this right?
- if (strcmp($message->uri, $messageurl) != 0 &&
- preg_match('/^http/', $message->uri)) {
- $messageurl = $message->uri;
- }
-
- $this->elementStart('div', 'entry-content');
- $this->elementStart('a', array('rel' => 'bookmark',
- 'class' => 'timestamp',
- 'href' => $messageurl));
- $dt = common_date_iso8601($message->created);
- $this->element('abbr', array('class' => 'published',
- 'title' => $dt),
- common_date_string($message->created));
- $this->elementEnd('a');
-
- if ($message->source) {
- $this->elementStart('span', 'source');
- // FIXME: bad i18n. Device should be a parameter (from %s).
- $this->text(_('from'));
- $this->element('span', 'device', $this->showSource($message->source));
- $this->elementEnd('span');
- }
- $this->elementEnd('div');
-
- $this->elementEnd('li');
- }
-
/**
* Show the page notice
*
@@ -252,44 +156,6 @@ class MailboxAction extends CurrentUserDesignAction
$this->elementEnd('div');
}
- /**
- * Show the source of the message
- *
- * Returns either the name (and link) of the API client that posted the notice,
- * or one of other other channels.
- *
- * @param string $source the source of the message
- *
- * @return void
- */
-
- function showSource($source)
- {
- $source_name = _($source);
- switch ($source) {
- case 'web':
- case 'xmpp':
- case 'mail':
- case 'omb':
- case 'api':
- $this->element('span', 'device', $source_name);
- break;
- default:
- $ns = Notice_source::staticGet($source);
- if ($ns) {
- $this->elementStart('span', 'device');
- $this->element('a', array('href' => $ns->url,
- 'rel' => 'external'),
- $ns->name);
- $this->elementEnd('span');
- } else {
- $this->element('span', 'device', $source_name);
- }
- break;
- }
- return;
- }
-
/**
* Mailbox actions are read only
*
@@ -302,5 +168,4 @@ class MailboxAction extends CurrentUserDesignAction
{
return true;
}
-
}
diff --git a/lib/messagelist.php b/lib/messagelist.php
new file mode 100644
index 0000000000..da7e9a6c27
--- /dev/null
+++ b/lib/messagelist.php
@@ -0,0 +1,107 @@
+.
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Message list widget
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+abstract class MessageList extends Widget
+{
+ var $message;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out Output context
+ * @param Message $message Stream of messages to show
+ */
+ function __construct($out, $message)
+ {
+ parent::__construct($out);
+ $this->message = $message;
+ }
+
+ /**
+ * Show the widget
+ *
+ * Uses newItem() to create each new item.
+ *
+ * @return integer count of messages seen.
+ */
+ function show()
+ {
+ $cnt = 0;
+
+ $this->out->elementStart('div', array('id' =>'notices_primary'));
+
+ $this->out->element('h2', null, _('Messages'));
+
+ $this->out->elementStart('ul', 'notices messages');
+
+ while ($this->message->fetch() && $cnt <= MESSAGES_PER_PAGE) {
+
+ $cnt++;
+
+ if ($cnt > MESSAGES_PER_PAGE) {
+ break;
+ }
+
+ $mli = $this->newItem($this->message);
+
+ $mli->show();
+ }
+
+ $this->out->elementEnd('ul');
+
+ $this->out->elementEnd('div');
+ }
+
+ /**
+ * Create a new message item for a message
+ *
+ * @param Message $message The message to show
+ *
+ * @return MessageListItem an item to show
+ */
+ abstract function newItem($message);
+}
diff --git a/lib/messagelistitem.php b/lib/messagelistitem.php
new file mode 100644
index 0000000000..44e6976454
--- /dev/null
+++ b/lib/messagelistitem.php
@@ -0,0 +1,178 @@
+.
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * A single item in a message list
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+abstract class MessageListItem extends Widget
+{
+ var $message;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out Output context
+ * @param Message $message Message to show
+ */
+ function __construct($out, $message)
+ {
+ parent::__construct($out);
+ $this->message = $message;
+ }
+
+ /**
+ * Show the widget
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ $this->out->elementStart('li', array('class' => 'hentry notice',
+ 'id' => 'message-' . $this->message->id));
+
+ $profile = $this->getMessageProfile();
+
+ $this->out->elementStart('div', 'entry-title');
+ $this->out->elementStart('span', 'vcard author');
+ $this->out->elementStart('a', array('href' => $profile->profileurl,
+ 'class' => 'url'));
+ $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
+ $this->out->element('img', array('src' => ($avatar) ?
+ $avatar->displayUrl() :
+ Avatar::defaultImage(AVATAR_STREAM_SIZE),
+ 'class' => 'photo avatar',
+ 'width' => AVATAR_STREAM_SIZE,
+ 'height' => AVATAR_STREAM_SIZE,
+ 'alt' =>
+ ($profile->fullname) ? $profile->fullname :
+ $profile->nickname));
+ $this->out->element('span', array('class' => 'nickname fn'),
+ $profile->nickname);
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('span');
+
+ // FIXME: URL, image, video, audio
+ $this->out->elementStart('p', array('class' => 'entry-content'));
+ $this->out->raw($this->message->rendered);
+ $this->out->elementEnd('p');
+ $this->out->elementEnd('div');
+
+ $messageurl = common_local_url('showmessage',
+ array('message' => $this->message->id));
+
+ // XXX: we need to figure this out better. Is this right?
+ if (strcmp($this->message->uri, $messageurl) != 0 &&
+ preg_match('/^http/', $this->message->uri)) {
+ $messageurl = $this->message->uri;
+ }
+
+ $this->out->elementStart('div', 'entry-content');
+ $this->out->elementStart('a', array('rel' => 'bookmark',
+ 'class' => 'timestamp',
+ 'href' => $messageurl));
+ $dt = common_date_iso8601($this->message->created);
+ $this->out->element('abbr', array('class' => 'published',
+ 'title' => $dt),
+ common_date_string($this->message->created));
+ $this->out->elementEnd('a');
+
+ if ($this->message->source) {
+ $this->out->elementStart('span', 'source');
+ // FIXME: bad i18n. Device should be a parameter (from %s).
+ $this->out->text(_('from'));
+ $this->showSource($this->message->source);
+ $this->out->elementEnd('span');
+ }
+ $this->out->elementEnd('div');
+
+ $this->out->elementEnd('li');
+ }
+
+
+ /**
+ * Show the source of the message
+ *
+ * Returns either the name (and link) of the API client that posted the notice,
+ * or one of other other channels.
+ *
+ * @param string $source the source of the message
+ *
+ * @return void
+ */
+ function showSource($source)
+ {
+ $source_name = _($source);
+ switch ($source) {
+ case 'web':
+ case 'xmpp':
+ case 'mail':
+ case 'omb':
+ case 'api':
+ $this->out->element('span', 'device', $source_name);
+ break;
+ default:
+ $ns = Notice_source::staticGet($source);
+ if ($ns) {
+ $this->out->elementStart('span', 'device');
+ $this->out->element('a', array('href' => $ns->url,
+ 'rel' => 'external'),
+ $ns->name);
+ $this->out->elementEnd('span');
+ } else {
+ $this->out->element('span', 'device', $source_name);
+ }
+ break;
+ }
+ return;
+ }
+
+ /**
+ * Return the profile to show in the message item
+ *
+ * Overridden in sub-classes to show sender, receiver, or whatever
+ *
+ * @return Profile profile to show avatar and name of
+ */
+ abstract function getMessageProfile();
+}
diff --git a/lib/plugin.php b/lib/plugin.php
index 3f84afa27e..115bb10e01 100644
--- a/lib/plugin.php
+++ b/lib/plugin.php
@@ -110,11 +110,16 @@ class Plugin
{
$this->log(LOG_DEBUG, $msg);
}
+
+ function name()
+ {
+ $cls = get_class($this);
+ return mb_substr($cls, 0, -6);
+ }
function onPluginVersion(&$versions)
{
- $cls = get_class($this);
- $name = mb_substr($cls, 0, -6);
+ $name = $this->name();
$versions[] = array('name' => $name,
// TRANS: Displayed as version information for a plugin if no version information was found.
@@ -122,4 +127,39 @@ class Plugin
return true;
}
+
+ function path($relative)
+ {
+ return self::staticPath($this->name(), $relative);
+ }
+
+ static function staticPath($plugin, $relative)
+ {
+ $isHTTPS = StatusNet::isHTTPS();
+
+ if ($isHTTPS) {
+ $server = common_config('plugins', 'sslserver');
+ } else {
+ $server = common_config('plugins', 'server');
+ }
+
+ if (empty($server)) {
+ if ($isHTTPS) {
+ $server = common_config('site', 'sslserver');
+ }
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
+ }
+
+ $path = common_config('plugins', 'path');
+
+ if (empty($path)) {
+ $path = common_config('site', 'path') . '/plugins/';
+ }
+
+ $protocol = ($isHTTPS) ? 'https' : 'http';
+
+ return $protocol.'://'.$server.$path.$plugin.'/'.$relative;
+ }
}
diff --git a/lib/util.php b/lib/util.php
index da36121ffd..85f49e4c59 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -2184,3 +2184,40 @@ function common_nicknamize($str)
$str = preg_replace('/\W/', '', $str);
return strtolower($str);
}
+
+function common_perf_counter($key, $val=null)
+{
+ global $_perfCounters;
+ if (isset($_perfCounters)) {
+ if (common_config('site', 'logperf')) {
+ if (array_key_exists($key, $_perfCounters)) {
+ $_perfCounters[$key][] = $val;
+ } else {
+ $_perfCounters[$key] = array($val);
+ }
+ if (common_config('site', 'logperf_detail')) {
+ common_log(LOG_DEBUG, "PERF COUNTER HIT: $key $val");
+ }
+ }
+ }
+}
+
+function common_log_perf_counters()
+{
+ if (common_config('site', 'logperf')) {
+ global $_startTime, $_perfCounters;
+
+ if (isset($_startTime)) {
+ $endTime = microtime(true);
+ $diff = round(($endTime - $_startTime) * 1000);
+ common_log(LOG_DEBUG, "PERF runtime: ${diff}ms");
+ }
+ $counters = $_perfCounters;
+ ksort($counters);
+ foreach ($counters as $key => $values) {
+ $count = count($values);
+ $unique = count(array_unique($values));
+ common_log(LOG_DEBUG, "PERF COUNTER: $key $count ($unique unique)");
+ }
+ }
+}
diff --git a/plugins/Autocomplete/AutocompletePlugin.php b/plugins/Autocomplete/AutocompletePlugin.php
index 230ba089de..ca495f79f4 100644
--- a/plugins/Autocomplete/AutocompletePlugin.php
+++ b/plugins/Autocomplete/AutocompletePlugin.php
@@ -51,15 +51,15 @@ class AutocompletePlugin extends Plugin
function onEndShowScripts($action){
if (common_logged_in()) {
- $action->script('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js');
- $action->script('plugins/Autocomplete/Autocomplete.js');
+ $action->script($this->path('jquery-autocomplete/jquery.autocomplete.pack.js'));
+ $action->script($this->path('Autocomplete.js'));
}
}
function onEndShowStatusNetStyles($action)
{
if (common_logged_in()) {
- $action->cssLink('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css');
+ $action->cssLink($this->path('jquery-autocomplete/jquery.autocomplete.css'));
}
}
diff --git a/plugins/BlankAd/BlankAdPlugin.php b/plugins/BlankAd/BlankAdPlugin.php
index 49243aeea9..9128e7bcdb 100644
--- a/plugins/BlankAd/BlankAdPlugin.php
+++ b/plugins/BlankAd/BlankAdPlugin.php
@@ -65,7 +65,7 @@ class BlankAdPlugin extends UAPPlugin
$action->element('img',
array('width' => 300,
'height' => 250,
- 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ 'src' => $this->path('redpixel.png')),
'');
}
@@ -81,7 +81,7 @@ class BlankAdPlugin extends UAPPlugin
$action->element('img',
array('width' => 180,
'height' => 150,
- 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ 'src' => $this->path('redpixel.png')),
'');
}
@@ -97,7 +97,7 @@ class BlankAdPlugin extends UAPPlugin
$action->element('img',
array('width' => 160,
'height' => 600,
- 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ 'src' => $this->path('redpixel.png')),
'');
}
@@ -113,7 +113,7 @@ class BlankAdPlugin extends UAPPlugin
$action->element('img',
array('width' => 728,
'height' => 90,
- 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ 'src' => $this->path('redpixel.png')),
'');
}
diff --git a/plugins/Bookmark/BookmarkPlugin.php b/plugins/Bookmark/BookmarkPlugin.php
index 800a26e795..2e6baf5e4e 100644
--- a/plugins/Bookmark/BookmarkPlugin.php
+++ b/plugins/Bookmark/BookmarkPlugin.php
@@ -149,7 +149,7 @@ class BookmarkPlugin extends Plugin
function onEndShowStyles($action)
{
- $action->cssLink('plugins/Bookmark/bookmark.css');
+ $action->cssLink($this->path('bookmark.css'));
return true;
}
diff --git a/plugins/Bookmark/bookmarkpopup.php b/plugins/Bookmark/bookmarkpopup.php
index 24ed79612b..33f983a93a 100644
--- a/plugins/Bookmark/bookmarkpopup.php
+++ b/plugins/Bookmark/bookmarkpopup.php
@@ -107,6 +107,6 @@ class BookmarkpopupAction extends NewbookmarkAction
function showScripts()
{
parent::showScripts();
- $this->script(common_path('plugins/Bookmark/bookmarkpopup.js'));
+ $this->script(Plugin::staticPath('Bookmark', 'bookmarkpopup.js'));
}
}
diff --git a/plugins/ClientSideShorten/ClientSideShortenPlugin.php b/plugins/ClientSideShorten/ClientSideShortenPlugin.php
index 27a3a56f72..443d2fffe0 100644
--- a/plugins/ClientSideShorten/ClientSideShortenPlugin.php
+++ b/plugins/ClientSideShorten/ClientSideShortenPlugin.php
@@ -53,7 +53,7 @@ class ClientSideShortenPlugin extends Plugin
function onEndShowScripts($action){
$action->inlineScript('var Notice_maxContent = ' . Notice::maxContent());
if (common_logged_in()) {
- $action->script('plugins/ClientSideShorten/shorten.js');
+ $action->script($this->path('shorten.js'));
}
}
diff --git a/plugins/DirectionDetector/DirectionDetectorPlugin.php b/plugins/DirectionDetector/DirectionDetectorPlugin.php
index 4a38f390f1..ec206dd75c 100644
--- a/plugins/DirectionDetector/DirectionDetectorPlugin.php
+++ b/plugins/DirectionDetector/DirectionDetectorPlugin.php
@@ -129,7 +129,7 @@ class DirectionDetectorPlugin extends Plugin {
*/
function onEndShowScripts($action){
if (common_logged_in()) {
- $action->script('plugins/DirectionDetector/jquery.DirectionDetector.js');
+ $action->script($this->path('jquery.DirectionDetector.js'));
}
}
diff --git a/plugins/FacebookBridge/FacebookBridgePlugin.php b/plugins/FacebookBridge/FacebookBridgePlugin.php
index 000b214ff4..7e17c2d7ec 100644
--- a/plugins/FacebookBridge/FacebookBridgePlugin.php
+++ b/plugins/FacebookBridge/FacebookBridgePlugin.php
@@ -24,7 +24,7 @@
* @category Pugin
* @package StatusNet
* @author Zach Copley
- * @copyright 2010 StatusNet, Inc.
+ * @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
@@ -47,8 +47,9 @@ define("FACEBOOK_SERVICE", 2);
*/
class FacebookBridgePlugin extends Plugin
{
- public $appId = null; // Facebook application ID
- public $secret = null; // Facebook application secret
+ public $appId; // Facebook application ID
+ public $secret; // Facebook application secret
+
public $facebook = null; // Facebook application instance
public $dir = null; // Facebook plugin dir
@@ -61,6 +62,28 @@ class FacebookBridgePlugin extends Plugin
*/
function initialize()
{
+
+ // Allow the id and key to be passed in
+ // Control panel will override
+
+ if (isset($this->appId)) {
+ $appId = common_config('facebook', 'appid');
+ if (empty($appId)) {
+ Config::save(
+ 'facebook',
+ 'appid',
+ $this->appId
+ );
+ }
+ }
+
+ if (isset($this->secret)) {
+ $secret = common_config('facebook', 'secret');
+ if (empty($secret)) {
+ Config::save('facebook', 'secret', $this->secret);
+ }
+ }
+
$this->facebook = Facebookclient::getFacebook(
$this->appId,
$this->secret
diff --git a/plugins/FacebookBridge/actions/facebooklogin.php b/plugins/FacebookBridge/actions/facebooklogin.php
index f8a45c41b2..924dd46565 100644
--- a/plugins/FacebookBridge/actions/facebooklogin.php
+++ b/plugins/FacebookBridge/actions/facebooklogin.php
@@ -89,10 +89,7 @@ class FacebookloginAction extends Action
);
$attrs = array(
- 'src' => common_path(
- 'plugins/FacebookBridge/images/login-button.png',
- true
- ),
+ 'src' => Plugin::staticPath('FacebookBridge', 'images/login-button.png'),
'alt' => 'Login with Facebook',
'title' => 'Login with Facebook'
);
diff --git a/plugins/FacebookBridge/lib/facebookclient.php b/plugins/FacebookBridge/lib/facebookclient.php
index d5ecd11a9c..030a75caed 100644
--- a/plugins/FacebookBridge/lib/facebookclient.php
+++ b/plugins/FacebookBridge/lib/facebookclient.php
@@ -115,14 +115,7 @@ class Facebookclient
function isFacebookBound() {
if (empty($this->flink)) {
- common_log(
- LOG_WARN,
- sprintf(
- "No Foreign_link to Facebook for the author of notice %d.",
- $this->notice->id
- ),
- __FILE__
- );
+ // User hasn't setup bridging
return false;
}
@@ -180,15 +173,6 @@ class Facebookclient
// Otherwise we most likely have an access token
return $this->sendGraph();
}
-
- } else {
- common_debug(
- sprintf(
- "Skipping notice %d - not bound for Facebook",
- $this->notice->id,
- __FILE__
- )
- );
}
}
diff --git a/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php b/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php
new file mode 100644
index 0000000000..09fd1d7bfa
--- /dev/null
+++ b/plugins/GroupPrivateMessage/GroupPrivateMessagePlugin.php
@@ -0,0 +1,505 @@
+.
+ *
+ * @category Privacy
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Private groups
+ *
+ * This plugin allows users to send private messages to a group.
+ *
+ * @category Privacy
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class GroupPrivateMessagePlugin extends Plugin
+{
+ /**
+ * Database schema setup
+ *
+ * @see Schema
+ * @see ColumnDef
+ *
+ * @return boolean hook value
+ */
+
+ function onCheckSchema()
+ {
+ $schema = Schema::get();
+
+ // For storing user-submitted flags on profiles
+
+ $schema->ensureTable('group_privacy_settings',
+ array(new ColumnDef('group_id',
+ 'integer',
+ null,
+ false,
+ 'PRI'),
+ new ColumnDef('allow_privacy',
+ 'integer'),
+ new ColumnDef('allow_sender',
+ 'integer'),
+ new ColumnDef('created',
+ 'datetime'),
+ new ColumnDef('modified',
+ 'timestamp')));
+
+ $schema->ensureTable('group_message',
+ array(new ColumnDef('id',
+ 'char',
+ 36,
+ false,
+ 'PRI'),
+ new ColumnDef('uri',
+ 'varchar',
+ 255,
+ false,
+ 'UNI'),
+ new ColumnDef('from_profile',
+ 'integer',
+ null,
+ false,
+ 'MUL'),
+ new ColumnDef('to_group',
+ 'integer',
+ null,
+ false,
+ 'MUL'),
+ new ColumnDef('content',
+ 'text'),
+ new ColumnDef('rendered',
+ 'text'),
+ new ColumnDef('url',
+ 'varchar',
+ 255,
+ false,
+ 'UNI'),
+ new ColumnDef('created',
+ 'datetime')));
+
+ $schema->ensureTable('group_message_profile',
+ array(new ColumnDef('to_profile',
+ 'integer',
+ null,
+ false,
+ 'PRI'),
+ new ColumnDef('group_message_id',
+ 'char',
+ 36,
+ false,
+ 'PRI'),
+ new ColumnDef('created',
+ 'datetime')));
+
+ return true;
+ }
+
+ /**
+ * Load related modules when needed
+ *
+ * @param string $cls Name of the class to be loaded
+ *
+ * @return boolean hook value
+ */
+
+ function onAutoload($cls)
+ {
+ $dir = dirname(__FILE__);
+
+ switch ($cls)
+ {
+ case 'GroupinboxAction':
+ case 'ShowgroupmessageAction':
+ case 'NewgroupmessageAction':
+ include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
+ return false;
+ case 'Group_privacy_settings':
+ case 'Group_message':
+ case 'Group_message_profile':
+ include_once $dir . '/'.$cls.'.php';
+ return false;
+ case 'GroupMessageCommand':
+ case 'GroupMessageList':
+ case 'GroupMessageListItem':
+ case 'GroupMessageForm':
+ include_once $dir . '/'.strtolower($cls).'.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Map URLs to actions
+ *
+ * @param Net_URL_Mapper $m path-to-action mapper
+ *
+ * @return boolean hook value
+ */
+
+ function onRouterInitialized($m)
+ {
+ $m->connect('group/:nickname/inbox',
+ array('action' => 'groupinbox'),
+ array('nickname' => Nickname::DISPLAY_FMT));
+
+ $m->connect('group/message/:id',
+ array('action' => 'showgroupmessage'),
+ array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
+
+ $m->connect('group/:nickname/message/new',
+ array('action' => 'newgroupmessage'),
+ array('nickname' => Nickname::DISPLAY_FMT));
+
+ return true;
+ }
+
+ /**
+ * Add group inbox to the menu
+ *
+ * @param Action $action The current action handler. Use this to
+ * do any output.
+ *
+ * @return boolean hook value; true means continue processing, false means stop.
+ *
+ * @see Action
+ */
+
+ function onEndGroupGroupNav($groupnav)
+ {
+ $action = $groupnav->action;
+ $group = $groupnav->group;
+
+ $action->menuItem(common_local_url('groupinbox',
+ array('nickname' => $group->nickname)),
+ _m('Inbox'),
+ _m('Private messages for this group'),
+ $action->trimmed('action') == 'groupinbox',
+ 'nav_group_inbox');
+ return true;
+ }
+
+ /**
+ * Create default group privacy settings at group create time
+ *
+ * @param User_group $group Group that was just created
+ *
+ * @result boolean hook value
+ */
+
+ function onEndGroupSave($group)
+ {
+ $gps = new Group_privacy_settings();
+
+ $gps->group_id = $group->id;
+ $gps->allow_privacy = Group_privacy_settings::SOMETIMES;
+ $gps->allow_sender = Group_privacy_settings::MEMBER;
+ $gps->created = common_sql_now();
+ $gps->modified = $gps->created;
+
+ // This will throw an exception on error
+
+ $gps->insert();
+
+ return true;
+ }
+
+ /**
+ * Show group privacy controls on group edit form
+ *
+ * @param GroupEditForm $form form being shown
+ */
+
+ function onEndGroupEditFormData($form)
+ {
+ $gps = null;
+
+ if (!empty($form->group)) {
+ $gps = Group_privacy_settings::staticGet('group_id', $form->group->id);
+ }
+
+ $form->out->elementStart('li');
+ $form->out->dropdown('allow_privacy',
+ _('Private messages'),
+ array(Group_privacy_settings::SOMETIMES => _('Sometimes'),
+ Group_privacy_settings::ALWAYS => _('Always'),
+ Group_privacy_settings::NEVER => _('Never')),
+ _('Whether to allow private messages to this group'),
+ false,
+ (empty($gps)) ? Group_privacy_settings::SOMETIMES : $gps->allow_privacy);
+ $form->out->elementEnd('li');
+ $form->out->elementStart('li');
+ $form->out->dropdown('allow_sender',
+ _('Private sender'),
+ array(Group_privacy_settings::EVERYONE => _('Everyone'),
+ Group_privacy_settings::MEMBER => _('Member'),
+ Group_privacy_settings::ADMIN => _('Admin')),
+ _('Who can send private messages to the group'),
+ false,
+ (empty($gps)) ? Group_privacy_settings::MEMBER : $gps->allow_sender);
+ $form->out->elementEnd('li');
+ return true;
+ }
+
+ function onEndGroupSaveForm($action)
+ {
+ $gps = null;
+
+ if (!empty($action->group)) {
+ $gps = Group_privacy_settings::staticGet('group_id', $action->group->id);
+ }
+
+ $orig = null;
+
+ if (empty($gps)) {
+ $gps = new Group_privacy_settings();
+ $gps->group_id = $action->group->id;
+ } else {
+ $orig = clone($gps);
+ }
+
+ $gps->allow_privacy = $action->trimmed('allow_privacy');
+ $gps->allow_sender = $action->trimmed('allow_sender');
+
+ if (empty($orig)) {
+ $gps->created = common_sql_now();
+ $gps->insert();
+ } else {
+ $gps->update($orig);
+ }
+
+ return true;
+ }
+
+ /**
+ * Overload 'd' command to send private messages to groups.
+ *
+ * 'd !group word word word' will send the private message
+ * 'word word word' to the group 'group'.
+ *
+ * @param string $cmd Command being run
+ * @param string $arg Rest of the message (including address)
+ * @param User $user User sending the message
+ * @param Command &$result The resulting command object to be run.
+ *
+ * @return boolean hook value
+ */
+ function onStartIntepretCommand($cmd, $arg, $user, &$result)
+ {
+ if ($cmd == 'd' || $cmd == 'dm') {
+
+ $this->debug('Got a d command');
+
+ // Break off the first word as the address
+
+ $pieces = explode(' ', $arg, 2);
+
+ if (count($pieces) == 1) {
+ $pieces[] = null;
+ }
+
+ list($addr, $msg) = $pieces;
+
+ if (!empty($addr) && $addr[0] == '!') {
+ $result = new GroupMessageCommand($user, substr($addr, 1), $msg);
+ Event::handle('EndInterpretCommand', array($cmd, $arg, $user, $result));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * To add a "Message" button to the group profile page
+ *
+ * @param Action $action The showgroup action being shown
+ * @param User_group $group The current group
+ *
+ * @return boolean hook value
+ */
+ function onEndGroupActionsList($action, $group)
+ {
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ return true;
+ }
+
+ try {
+ Group_privacy_settings::ensurePost($cur, $group);
+ } catch (Exception $e) {
+ return true;
+ }
+
+ $action->elementStart('li', 'entity_send-a-message');
+ $action->element('a', array('href' => common_local_url('newgroupmessage', array('nickname' => $group->nickname)),
+ 'title' => _('Send a direct message to this group')),
+ _('Message'));
+ // $form = new GroupMessageForm($action, $group);
+ // $form->hidden = true;
+ // $form->show();
+ $action->elementEnd('li');
+ return true;
+ }
+
+ /**
+ * When saving a notice, check its groups. If any of them has
+ * privacy == always, force a group private message to all mentioned groups.
+ * If any of the groups disallows private messages, skip it.
+ *
+ * @param
+ *
+ */
+
+ function onStartNoticeSave(&$notice) {
+
+ // Look for group tags
+ // FIXME: won't work for remote groups
+ // @fixme if Notice::saveNew is refactored so we can just pull its list
+ // of groups between processing and saving, make use of it
+
+ $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/',
+ strtolower($notice->content),
+ $match);
+
+ $groups = array();
+ $ignored = array();
+
+ $forcePrivate = false;
+
+ if ($count > 0) {
+
+ /* Add them to the database */
+
+ foreach (array_unique($match[1]) as $nickname) {
+
+ $group = User_group::getForNickname($nickname, $profile);
+
+ if (empty($group)) {
+ continue;
+ }
+
+ $gps = Group_privacy_settings::forGroup($group);
+
+ switch ($gps->allow_privacy) {
+ case Group_privacy_settings::ALWAYS:
+ $forcePrivate = true;
+ // fall through
+ case Group_privacy_settings::SOMETIMES:
+ $groups[] = $group;
+ break;
+ case Group_privacy_settings::NEVER:
+ $ignored[] = $group;
+ break;
+ }
+ }
+
+ if ($forcePrivate) {
+
+ foreach ($ignored as $group) {
+ common_log(LOG_NOTICE,
+ "Notice forced to group direct message ".
+ "but group ".$group->nickname." does not allow them.");
+ }
+
+ $user = User::staticGet('id', $notice->profile_id);
+
+ if (empty($user)) {
+ common_log(LOG_WARNING,
+ "Notice forced to group direct message ".
+ "but profile ".$notice->profile_id." is not a local user.");
+ } else {
+ foreach ($groups as $group) {
+ Group_message::send($user, $group, $notice->content);
+ }
+ }
+
+ // Don't save the notice!
+ // FIXME: this is probably cheating.
+ throw new ClientException(sprintf(_('Forced notice to private group message.')),
+ 200);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Show an indicator that the group is (essentially) private on the group page
+ *
+ * @param Action $action The action being shown
+ * @param User_group $group The group being shown
+ *
+ * @return boolean hook value
+ */
+
+ function onEndGroupProfileElements($action, $group)
+ {
+ $gps = Group_privacy_settings::forGroup($group);
+
+ if ($gps->allow_privacy == Group_privacy_settings::ALWAYS) {
+ $action->element('p', 'privategroupindicator', _('Private'));
+ }
+
+ return true;
+ }
+
+ function onStartShowExportData($action)
+ {
+ if ($action instanceof ShowgroupAction) {
+ $gps = Group_privacy_settings::forGroup($action->group);
+
+ if ($gps->allow_privacy == Group_privacy_settings::ALWAYS) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function onPluginVersion(&$versions)
+ {
+ $versions[] = array('name' => 'GroupPrivateMessage',
+ 'version' => STATUSNET_VERSION,
+ 'author' => 'Evan Prodromou',
+ 'homepage' => 'http://status.net/wiki/Plugin:GroupPrivateMessage',
+ 'rawdescription' =>
+ _m('Allow posting DMs to a group.'));
+ return true;
+ }
+}
diff --git a/plugins/GroupPrivateMessage/Group_message.php b/plugins/GroupPrivateMessage/Group_message.php
new file mode 100644
index 0000000000..f8c0c707c3
--- /dev/null
+++ b/plugins/GroupPrivateMessage/Group_message.php
@@ -0,0 +1,208 @@
+
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+/**
+ * Data class for group direct messages
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * @see DB_DataObject
+ */
+
+class Group_message extends Memcached_DataObject
+{
+ public $__table = 'group_message'; // table name
+ public $id; // char(36) primary_key not_null
+ public $uri; // varchar(255)
+ public $from_profile; // int
+ public $to_group; // int
+ public $content;
+ public $rendered;
+ public $url;
+ public $created;
+
+ /**
+ * Get an instance by key
+ *
+ * This is a utility method to get a single instance with a given key value.
+ *
+ * @param string $k Key to use to lookup (usually 'user_id' for this class)
+ * @param mixed $v Value to lookup
+ *
+ * @return Group_message object found, or null for no hits
+ *
+ */
+ function staticGet($k, $v=null)
+ {
+ return Memcached_DataObject::staticGet('Group_message', $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+ function table()
+ {
+ return array('id' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'from_profile' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'to_group' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'content' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'rendered' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'url' => DB_DATAOBJECT_STR,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has, since it
+ * won't appear in StatusNet's own keys list. In most cases, this will
+ * simply reference your keyTypes() function.
+ *
+ * @return array list of key field names
+ */
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * @return array associative array of key definitions, field name to type:
+ * 'K' for primary key: for compound keys, add an entry for each component;
+ * 'U' for unique keys: compound keys are not well supported here.
+ */
+ function keyTypes()
+ {
+ return array('id' => 'K', 'uri' => 'U');
+ }
+
+ static function send($user, $group, $text)
+ {
+ if (!$user->hasRight(Right::NEWMESSAGE)) {
+ // XXX: maybe break this out into a separate right
+ throw new Exception(sprintf(_('User %s not allowed to send private messages.'),
+ $user->nickname));
+ }
+
+ Group_privacy_settings::ensurePost($user, $group);
+
+ $text = $user->shortenLinks($text);
+
+ // We use the same limits as for 'regular' private messages.
+
+ if (Message::contentTooLong($text)) {
+ throw new Exception(sprintf(_m('That\'s too long. Maximum message size is %d character.',
+ 'That\'s too long. Maximum message size is %d characters.',
+ Message::maxContent()),
+ Message::maxContent()));
+ }
+
+ // Valid! Let's do this thing!
+
+ $gm = new Group_message();
+
+ $gm->id = UUID::gen();
+ $gm->uri = common_local_url('showgroupmessage', array('id' => $gm->id));
+ $gm->from_profile = $user->id;
+ $gm->to_group = $group->id;
+ $gm->content = $text; // XXX: is this cool?!
+ $gm->rendered = common_render_text($text);
+ $gm->url = $gm->uri;
+ $gm->created = common_sql_now();
+
+ // This throws a conniption if there's a problem
+
+ $gm->insert();
+
+ $gm->distribute();
+
+ return $gm;
+ }
+
+ function distribute()
+ {
+ $group = User_group::staticGet('id', $this->to_group);
+
+ $member = $group->getMembers();
+
+ while ($member->fetch()) {
+ Group_message_profile::send($this, $member);
+ }
+ }
+
+ function getGroup()
+ {
+ $group = User_group::staticGet('id', $this->to_group);
+ if (empty($group)) {
+ throw new ServerException(_('No group for group message'));
+ }
+ return $group;
+ }
+
+ function getSender()
+ {
+ $sender = Profile::staticGet('id', $this->from_profile);
+ if (empty($sender)) {
+ throw new ServerException(_('No sender for group message'));
+ }
+ return $sender;
+ }
+
+ static function forGroup($group, $offset, $limit)
+ {
+ // XXX: cache
+ $gm = new Group_message();
+
+ $gm->to_group = $group->id;
+ $gm->orderBy('created DESC');
+ $gm->limit($offset, $limit);
+
+ $gm->find();
+
+ return $gm;
+ }
+
+}
diff --git a/plugins/GroupPrivateMessage/Group_message_profile.php b/plugins/GroupPrivateMessage/Group_message_profile.php
new file mode 100644
index 0000000000..bd778b815a
--- /dev/null
+++ b/plugins/GroupPrivateMessage/Group_message_profile.php
@@ -0,0 +1,189 @@
+
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+/**
+ * Data class for group direct messages for users
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * @see DB_DataObject
+ */
+
+class Group_message_profile extends Memcached_DataObject
+{
+ public $__table = 'group_message_profile'; // table name
+ public $to_profile; // int
+ public $group_message_id; // char(36) primary_key not_null
+ public $created;
+
+ /**
+ * Get an instance by key
+ *
+ * This is a utility method to get a single instance with a given key value.
+ *
+ * @param string $k Key to use to lookup (usually 'user_id' for this class)
+ * @param mixed $v Value to lookup
+ *
+ * @return Group_message object found, or null for no hits
+ *
+ */
+ function staticGet($k, $v=null)
+ {
+ return Memcached_DataObject::staticGet('Group_message_profile', $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+ function table()
+ {
+ return array('to_profile' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'group_message_id' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has, since it
+ * won't appear in StatusNet's own keys list. In most cases, this will
+ * simply reference your keyTypes() function.
+ *
+ * @return array list of key field names
+ */
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * @return array associative array of key definitions, field name to type:
+ * 'K' for primary key: for compound keys, add an entry for each component;
+ * 'U' for unique keys: compound keys are not well supported here.
+ */
+ function keyTypes()
+ {
+ return array('to_profile' => 'K', 'group_message_id' => 'K');
+ }
+
+ /**
+ * No sequence keys in this table.
+ */
+ function sequenceKey()
+ {
+ return array(false, false, false);
+ }
+
+ function send($gm, $profile)
+ {
+ $gmp = new Group_message_profile();
+
+ $gmp->group_message_id = $gm->id;
+ $gmp->to_profile = $profile->id;
+ $gmp->created = common_sql_now();
+
+ $gmp->insert();
+
+ $gmp->notify();
+
+ return $gmp;
+ }
+
+ function notify()
+ {
+ // XXX: add more here
+ $this->notifyByMail();
+ }
+
+ function notifyByMail()
+ {
+ $to = User::staticGet('id', $this->to_profile);
+
+ if (empty($to) || is_null($to->email) || !$to->emailnotifymsg) {
+ return true;
+ }
+
+ $gm = Group_message::staticGet('id', $this->group_message_id);
+
+ $from_profile = Profile::staticGet('id', $gm->from_profile);
+
+ $group = $gm->getGroup();
+
+ common_switch_locale($to->language);
+
+ // TRANS: Subject for direct-message notification email.
+ // TRANS: %s is the sending user's nickname.
+ $subject = sprintf(_('New private message from %s to group %s'), $from->nickname, $group->nickname);
+
+ $from_profile = $from->getProfile();
+
+ // TRANS: Body for direct-message notification email.
+ // TRANS: %1$s is the sending user's long name, %2$s is the sending user's nickname,
+ // TRANS: %3$s is the message content, %4$s a URL to the message,
+ // TRANS: %5$s is the StatusNet sitename.
+ $body = sprintf(_("%1\$s (%2\$s) sent a private message to group %3\$s:\n\n".
+ "------------------------------------------------------\n".
+ "%4\$s\n".
+ "------------------------------------------------------\n\n".
+ "You can reply to their message here:\n\n".
+ "%5\$s\n\n".
+ "Don't reply to this email; it won't get to them.\n\n".
+ "With kind regards,\n".
+ "%6\$s\n"),
+ $from_profile->getBestName(),
+ $from->nickname,
+ $group->nickname,
+ $this->content,
+ common_local_url('newmessage', array('to' => $from->id)),
+ common_config('site', 'name'));
+
+ $headers = _mail_prepare_headers('message', $to->nickname, $from->nickname);
+
+ common_switch_locale();
+
+ return mail_to_user($to, $subject, $body, $headers);
+ }
+}
diff --git a/plugins/GroupPrivateMessage/Group_privacy_settings.php b/plugins/GroupPrivateMessage/Group_privacy_settings.php
new file mode 100644
index 0000000000..0176d3bd6e
--- /dev/null
+++ b/plugins/GroupPrivateMessage/Group_privacy_settings.php
@@ -0,0 +1,201 @@
+
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Data class for group privacy
+ *
+ * Stores admin preferences about the group.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * @see DB_DataObject
+ */
+
+class Group_privacy_settings extends Memcached_DataObject
+{
+ public $__table = 'group_privacy_settings';
+ /** ID of the group. */
+ public $group_id;
+ /** When to allow privacy: always, sometimes, or never. */
+ public $allow_privacy;
+ /** Who can send private messages: everyone, member, admin */
+ public $allow_sender;
+ /** row creation timestamp */
+ public $created;
+ /** Last-modified timestamp */
+ public $modified;
+
+ /** NEVER is */
+
+ const SOMETIMES = -1;
+ const NEVER = 0;
+ const ALWAYS = 1;
+
+ /** These are bit-mappy, as a hedge against the future. */
+
+ const EVERYONE = 1;
+ const MEMBER = 2;
+ const ADMIN = 4;
+
+ /**
+ * Get an instance by key
+ *
+ * This is a utility method to get a single instance with a given key value.
+ *
+ * @param string $k Key to use to lookup (usually 'user_id' for this class)
+ * @param mixed $v Value to lookup
+ *
+ * @return User_greeting_count object found, or null for no hits
+ */
+
+ function staticGet($k, $v=null)
+ {
+ return Memcached_DataObject::staticGet('Group_privacy_settings', $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('group_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'allow_privacy' => DB_DATAOBJECT_INT,
+ 'allow_sender' => DB_DATAOBJECT_INT,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has, since it
+ * won't appear in StatusNet's own keys list. In most cases, this will
+ * simply reference your keyTypes() function.
+ *
+ * @return array list of key field names
+ */
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * @return array associative array of key definitions, field name to type:
+ * 'K' for primary key: for compound keys, add an entry for each component;
+ * 'U' for unique keys: compound keys are not well supported here.
+ */
+
+ function keyTypes()
+ {
+ return array('group_id' => 'K');
+ }
+
+ /**
+ * Magic formula for non-autoincrementing integer primary keys
+ *
+ * @return array magic three-false array that stops auto-incrementing.
+ */
+
+ function sequenceKey()
+ {
+ return array(false, false, false);
+ }
+
+ function forGroup($group)
+ {
+ $gps = Group_privacy_settings::staticGet('group_id', $group->id);
+
+ if (empty($gps)) {
+ // make a fake one with defaults
+ $gps = new Group_privacy_settings();
+ $gps->allow_privacy = Group_privacy_settings::SOMETIMES;
+ $gps->allow_sender = Group_privacy_settings::MEMBER;
+ }
+
+ return $gps;
+ }
+
+ function ensurePost($user, $group)
+ {
+ $gps = self::forGroup($group);
+
+ if ($gps->allow_privacy == Group_privacy_settings::NEVER) {
+ throw new Exception(sprintf(_('Group %s does not allow private messages.'),
+ $group->nickname));
+ }
+
+ switch ($gps->allow_sender) {
+ case Group_privacy_settings::EVERYONE:
+ $profile = $user->getProfile();
+ if (Group_block::isBlocked($group, $profile)) {
+ throw new Exception(sprintf(_('User %s is blocked from group %s.'),
+ $user->nickname,
+ $group->nickname));
+ }
+ break;
+ case Group_privacy_settings::MEMBER:
+ if (!$user->isMember($group)) {
+ throw new Exception(sprintf(_('User %s is not a member of group %s.'),
+ $user->nickname,
+ $group->nickname));
+ }
+ break;
+ case Group_privacy_settings::ADMIN:
+ if (!$user->isAdmin($group)) {
+ throw new Exception(sprintf(_('User %s is not an administrator of group %s.'),
+ $user->nickname,
+ $group->nickname));
+ }
+ break;
+ default:
+ throw new Exception(sprintf(_('Unknown privacy settings for group %s.'),
+ $group->nickname));
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/GroupPrivateMessage/groupinbox.php b/plugins/GroupPrivateMessage/groupinbox.php
new file mode 100644
index 0000000000..39789cc9af
--- /dev/null
+++ b/plugins/GroupPrivateMessage/groupinbox.php
@@ -0,0 +1,208 @@
+.
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Show a list of private messages to this group
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class GroupinboxAction extends GroupDesignAction
+{
+ var $gm;
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ throw new ClientException(_('Only for logged-in users'), 403);
+ }
+
+ $nicknameArg = $this->trimmed('nickname');
+
+ $nickname = common_canonical_nickname($nicknameArg);
+
+ if ($nickname != $nicknameArg) {
+ $url = common_local_url('groupinbox', array('nickname' => $nickname));
+ common_redirect($url);
+ return false;
+ }
+
+ $localGroup = Local_group::staticGet('nickname', $nickname);
+
+ if (empty($localGroup)) {
+ throw new ClientException(_('No such group'), 404);
+ }
+
+ $this->group = User_group::staticGet('id', $localGroup->group_id);
+
+ if (empty($this->group)) {
+ throw new ClientException(_('No such group'), 404);
+ }
+
+ if (!$cur->isMember($this->group)) {
+ throw new ClientException(_('Only for members'), 403);
+ }
+
+ $this->page = $this->trimmed('page');
+
+ if (!$this->page) {
+ $this->page = 1;
+ }
+
+ $this->gm = Group_message::forGroup($this->group,
+ ($this->page - 1) * MESSAGES_PER_PAGE,
+ MESSAGES_PER_PAGE + 1);
+ return true;
+ }
+
+ function showLocalNav()
+ {
+ $nav = new GroupNav($this, $this->group);
+ $nav->show();
+ }
+
+ function showNoticeForm()
+ {
+ $form = new GroupMessageForm($this, $this->group);
+ $form->show();
+ }
+
+ function showContent()
+ {
+ $gml = new GroupMessageList($this, $this->gm);
+ $cnt = $gml->show();
+
+ if ($cnt == 0) {
+ $this->element('p', 'guide', _m('This group has not received any private messages.'));
+ }
+ $this->pagination($this->page > 1,
+ $cnt > MESSAGES_PER_PAGE,
+ $this->page,
+ 'groupinbox',
+ array('nickname' => $this->group->nickname));
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+ function handle($argarray=null)
+ {
+ $this->showPage();
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string page title, with page number
+ */
+ function title()
+ {
+ $base = $this->group->getFancyName();
+
+ if ($this->page == 1) {
+ return sprintf(_('%s group inbox'), $base);
+ } else {
+ // TRANS: Page title for any but first group page.
+ // TRANS: %1$s is a group name, $2$s is a page number.
+ return sprintf(_('%1$s group inbox, page %2$d'),
+ $base,
+ $this->page);
+ }
+ }
+
+ /**
+ * Show the page notice
+ *
+ * Shows instructions for the page
+ *
+ * @return void
+ */
+
+ function showPageNotice()
+ {
+ $instr = $this->getInstructions();
+ $output = common_markup_to_html($instr);
+
+ $this->elementStart('div', 'instructions');
+ $this->raw($output);
+ $this->elementEnd('div');
+ }
+
+ /**
+ * Instructions for using this page
+ *
+ * @return string localised instructions for using the page
+ */
+ function getInstructions()
+ {
+ // TRANS: Instructions for user inbox page.
+ return _m('This is the group inbox, which lists all incoming private messages for this group.');
+ }
+}
diff --git a/plugins/GroupPrivateMessage/groupmessagecommand.php b/plugins/GroupPrivateMessage/groupmessagecommand.php
new file mode 100644
index 0000000000..3b3cf4cfea
--- /dev/null
+++ b/plugins/GroupPrivateMessage/groupmessagecommand.php
@@ -0,0 +1,85 @@
+.
+ *
+ * @category Command
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Command object for messages to groups
+ *
+ * @category General
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class GroupMessageCommand extends Command
+{
+ /** User sending the message. */
+ var $user;
+ /** Nickname of the group they're sending to. */
+ var $nickname;
+ /** Text of the message. */
+ var $text;
+
+ /**
+ * Constructor
+ *
+ * @param User $user User sending the message
+ * @param string $nickname Nickname of the group
+ * @param string $text Text of message
+ */
+
+ function __construct($user, $nickname, $text)
+ {
+ $this->user = $user;
+ $this->nickname = $nickname;
+ $this->text = $text;
+ }
+
+ function handle($channel)
+ {
+ // Throws a command exception if group not found
+ $group = $this->getGroup($this->nickname);
+
+ $gm = Group_message::send($this->user, $group, $this->text);
+
+ $channel->output($this->user,
+ sprintf(_('Direct message to group %s sent.'),
+ $group->nickname));
+
+ return true;
+ }
+}
diff --git a/plugins/GroupPrivateMessage/groupmessageform.php b/plugins/GroupPrivateMessage/groupmessageform.php
new file mode 100644
index 0000000000..7205d3f0b9
--- /dev/null
+++ b/plugins/GroupPrivateMessage/groupmessageform.php
@@ -0,0 +1,166 @@
+.
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Form for posting a group message
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class GroupMessageForm extends Form
+{
+ var $group;
+ var $content;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out Output context
+ * @param User_group $group Group to post to
+ *
+ * @todo add a drop-down list to post to any group
+ */
+
+ function __construct($out, $group, $content=null)
+ {
+ parent::__construct($out);
+
+ $this->group = $group;
+ $this->content = $content;
+ }
+
+ /**
+ * Action for the form
+ */
+ function action()
+ {
+ return common_local_url('newgroupmessage',
+ array('nickname' => $this->group->nickname));
+ }
+
+ /**
+ * Legend for the form
+ *
+ * @param
+ *
+ * @return
+ */
+ function formLegend()
+ {
+ $this->out->element('legend',
+ null,
+ sprintf(_('Message to %s'), $this->group->nickname));
+ }
+
+ /**
+ * id for the form
+ *
+ * @param
+ *
+ * @return
+ */
+
+ function id()
+ {
+ return 'form_notice-group-message';
+ }
+
+ /**
+ * class for the form
+ *
+ * @param
+ *
+ * @return
+ */
+
+ function formClass()
+ {
+ return 'form_notice';
+ }
+
+ /**
+ * Entry data
+ *
+ * @param
+ *
+ * @return
+ */
+
+ function formData()
+ {
+ $this->out->element('label', array('for' => 'notice_data-text',
+ 'id' => 'notice_data-text-label'),
+ sprintf(_('Direct message to %s'), $this->group->nickname));
+
+ $this->out->element('textarea', array('id' => 'notice_data-text',
+ 'cols' => 35,
+ 'rows' => 4,
+ 'name' => 'content'),
+ ($this->content) ? $this->content : '');
+
+ $contentLimit = Message::maxContent();
+
+ if ($contentLimit > 0) {
+ $this->out->elementStart('dl', 'form_note');
+ $this->out->element('dt', null, _('Available characters'));
+ $this->out->element('dd', array('id' => 'notice_text-count'),
+ $contentLimit);
+ $this->out->elementEnd('dl');
+ }
+ }
+
+ /**
+ * Legend for the form
+ *
+ * @param
+ *
+ * @return
+ */
+
+ function formActions()
+ {
+ $this->out->element('input', array('id' => 'notice_action-submit',
+ 'class' => 'submit',
+ 'name' => 'message_send',
+ 'type' => 'submit',
+ 'value' => _m('Send button for sending notice', 'Send')));
+ }
+}
diff --git a/plugins/GroupPrivateMessage/groupmessagelist.php b/plugins/GroupPrivateMessage/groupmessagelist.php
new file mode 100644
index 0000000000..07f8ed5e4c
--- /dev/null
+++ b/plugins/GroupPrivateMessage/groupmessagelist.php
@@ -0,0 +1,90 @@
+.
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Widget for showing list of group messages
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class GroupMessageList extends Widget
+{
+ var $gm;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output context
+ * @param Group_message $gm Group message stream
+ */
+ function __construct($out, $gm)
+ {
+ parent::__construct($out);
+ $this->gm = $gm;
+ }
+
+ /**
+ * Show the list
+ *
+ * @return void
+ */
+ function show()
+ {
+ $this->out->elementStart('ul', 'notices messages group-messages');
+
+ $cnt = 0;
+
+ while ($this->gm->fetch() && $cnt <= MESSAGES_PER_PAGE) {
+
+ $cnt++;
+
+ if ($cnt > MESSAGES_PER_PAGE) {
+ break;
+ }
+
+ $gmli = new GroupMessageListItem($this->out, $this->gm);
+ $gmli->show();
+ }
+
+ $this->out->elementEnd('ul');
+
+ return $cnt;
+ }
+}
diff --git a/plugins/GroupPrivateMessage/groupmessagelistitem.php b/plugins/GroupPrivateMessage/groupmessagelistitem.php
new file mode 100644
index 0000000000..4c0fd2ce64
--- /dev/null
+++ b/plugins/GroupPrivateMessage/groupmessagelistitem.php
@@ -0,0 +1,113 @@
+.
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Widget for showing a single group message
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class GroupMessageListItem extends Widget
+{
+ var $gm;
+
+ /**
+ * Constructor
+ *
+ * @param HTMLOutputter $out output context
+ * @param Group_message $gm Group message
+ */
+ function __construct($out, $gm)
+ {
+ parent::__construct($out);
+ $this->gm = $gm;
+ }
+
+ /**
+ * Show the item
+ *
+ * @return void
+ */
+ function show()
+ {
+ $group = $this->gm->getGroup();
+ $sender = $this->gm->getSender();
+
+ $this->out->elementStart('li', array('class' => 'hentry notice message group-message',
+ 'id' => 'message-' . $this->gm->id));
+
+ $this->out->elementStart('div', 'entry-title');
+ $this->out->elementStart('span', 'vcard author');
+ $this->out->elementStart('a',
+ array('href' => $sender->profileurl,
+ 'class' => 'url'));
+ $avatar = $sender->getAvatar(AVATAR_STREAM_SIZE);
+ $this->out->element('img', array('src' => ($avatar) ?
+ $avatar->displayUrl() :
+ Avatar::defaultImage(AVATAR_STREAM_SIZE),
+ 'width' => AVATAR_STREAM_SIZE,
+ 'height' => AVATAR_STREAM_SIZE,
+ 'class' => 'photo avatar',
+ 'alt' => $sender->getBestName()));
+ $this->out->element('span',
+ array('class' => 'nickname fn'),
+ $sender->nickname);
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('span');
+
+ $this->out->elementStart('p', array('class' => 'entry-content message-content'));
+ $this->out->raw($this->gm->rendered);
+ $this->out->elementEnd('p');
+ $this->out->elementEnd('div');
+
+ $this->out->elementStart('div', 'entry-content');
+ $this->out->elementStart('a', array('rel' => 'bookmark',
+ 'class' => 'timestamp',
+ 'href' => $this->gm->url));
+ $dt = common_date_iso8601($this->gm->created);
+ $this->out->element('abbr', array('class' => 'published',
+ 'title' => $dt),
+ common_date_string($this->gm->created));
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('div');
+
+ $this->out->elementEnd('li');
+ }
+}
diff --git a/plugins/GroupPrivateMessage/newgroupmessage.php b/plugins/GroupPrivateMessage/newgroupmessage.php
new file mode 100644
index 0000000000..1ad24c4a0a
--- /dev/null
+++ b/plugins/GroupPrivateMessage/newgroupmessage.php
@@ -0,0 +1,161 @@
+.
+ *
+ * @category Cache
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Action for adding a new group message
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class NewgroupmessageAction extends Action
+{
+ var $group;
+ var $user;
+ var $text;
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $this->user = common_current_user();
+
+ if (empty($this->user)) {
+ throw new ClientException(_('Must be logged in.'), 403);
+ }
+
+ if (!$this->user->hasRight(Right::NEWMESSAGE)) {
+ throw new Exception(sprintf(_('User %s not allowed to send private messages.'),
+ $this->user->nickname));
+ }
+
+ $nicknameArg = $this->trimmed('nickname');
+
+ $nickname = common_canonical_nickname($nicknameArg);
+
+ if ($nickname != $nicknameArg) {
+ $url = common_local_url('newgroupmessage', array('nickname' => $nickname));
+ common_redirect($url, 301);
+ return false;
+ }
+
+ $localGroup = Local_group::staticGet('nickname', $nickname);
+
+ if (empty($localGroup)) {
+ throw new ClientException(_('No such group'), 404);
+ }
+
+ $this->group = User_group::staticGet('id', $localGroup->group_id);
+
+ if (empty($this->group)) {
+ throw new ClientException(_('No such group'), 404);
+ }
+
+ // This throws an exception on error
+
+ Group_privacy_settings::ensurePost($this->user, $this->group);
+
+ // If we're posted to, check session token and get text
+
+ if ($this->isPost()) {
+ $this->checkSessionToken();
+ $this->text = $this->trimmed('content');
+ }
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+
+ function handle($argarray=null)
+ {
+ if ($this->isPost()) {
+ $this->sendNewMessage();
+ } else {
+ $this->showPage();
+ }
+ }
+
+ function showNoticeForm()
+ {
+ $form = new GroupMessageForm($this, $this->group);
+ $form->show();
+ }
+
+ function sendNewMessage()
+ {
+ $gm = Group_message::send($this->user, $this->group, $this->text);
+
+ if ($this->boolean('ajax')) {
+ $this->startHTML('text/xml;charset=utf-8');
+ $this->elementStart('head');
+ $this->element('title', null, _('Message sent'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->element('p',
+ array('id' => 'command_result'),
+ sprintf(_('Direct message to %s sent.'),
+ $this->group->nickname));
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ common_redirect($gm->url, 303);
+ }
+ }
+
+ function title()
+ {
+ return sprintf(_('New message to group %s'), $this->group->nickname);
+ }
+}
diff --git a/plugins/GroupPrivateMessage/showgroupmessage.php b/plugins/GroupPrivateMessage/showgroupmessage.php
new file mode 100644
index 0000000000..73293255cf
--- /dev/null
+++ b/plugins/GroupPrivateMessage/showgroupmessage.php
@@ -0,0 +1,188 @@
+.
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Show a single private group message
+ *
+ * @category GroupPrivateMessage
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class ShowgroupmessageAction extends Action
+{
+ var $gm;
+ var $group;
+ var $sender;
+ var $user;
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $this->user = common_current_user();
+
+ if (empty($this->user)) {
+ throw new ClientException(_('Only logged-in users can view private messages.'),
+ 403);
+ }
+
+ $id = $this->trimmed('id');
+
+ $this->gm = Group_message::staticGet('id', $id);
+
+ if (empty($this->gm)) {
+ throw new ClientException(_('No such message'), 404);
+ }
+
+ $this->group = User_group::staticGet('id', $this->gm->to_group);
+
+ if (empty($this->group)) {
+ throw new ServerException(_('Group not found.'));
+ }
+
+ if (!$this->user->isMember($this->group)) {
+ throw new ClientException(_('Cannot read message.'), 403);
+ }
+
+ $this->sender = Profile::staticGet('id', $this->gm->from_profile);
+
+ if (empty($this->sender)) {
+ throw new ServerException(_('No sender found.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+
+ function handle($argarray=null)
+ {
+ $this->showPage();
+ }
+
+ /**
+ * Title of the page
+ */
+
+ function title()
+ {
+ return sprintf(_('Message from %1$s to group %2$s on %3$s'),
+ $this->sender->nickname,
+ $this->group->nickname,
+ common_exact_date($this->gm->created));
+ }
+
+ /**
+ * Show the content area.
+ */
+
+ function showContent()
+ {
+ $this->elementStart('ul', 'notices messages');
+ $gmli = new GroupMessageListItem($this, $this->gm);
+ $gmli->show();
+ $this->elementEnd('ul');
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Return last modified, if applicable.
+ *
+ * MAY override
+ *
+ * @return string last modified http header
+ */
+ function lastModified()
+ {
+ return max(strtotime($this->group->modified),
+ strtotime($this->sender->modified),
+ strtotime($this->gm->modified));
+ }
+
+ /**
+ * Return etag, if applicable.
+ *
+ * MAY override
+ *
+ * @return string etag http header
+ */
+ function etag()
+ {
+ $avatar = $this->sender->getAvatar(AVATAR_STREAM_SIZE);
+
+ $avtime = ($avatar) ? strtotime($avatar->modified) : 0;
+
+ return 'W/"' . implode(':', array($this->arg('action'),
+ common_user_cache_hash(),
+ common_language(),
+ $this->gm->id,
+ strtotime($this->sender->modified),
+ strtotime($this->group->modified),
+ $avtime)) . '"';
+ }
+}
diff --git a/plugins/InfiniteScroll/InfiniteScrollPlugin.php b/plugins/InfiniteScroll/InfiniteScrollPlugin.php
index 50c1b5a208..b6c4fabba0 100644
--- a/plugins/InfiniteScroll/InfiniteScrollPlugin.php
+++ b/plugins/InfiniteScroll/InfiniteScrollPlugin.php
@@ -40,8 +40,8 @@ class InfiniteScrollPlugin extends Plugin
function onEndShowScripts($action)
{
- $action->script('plugins/InfiniteScroll/jquery.infinitescroll.js');
- $action->script('plugins/InfiniteScroll/infinitescroll.js');
+ $action->script($this->path('jquery.infinitescroll.js'));
+ $action->script($this->path('infinitescroll.js'));
}
function onPluginVersion(&$versions)
diff --git a/plugins/LinkPreview/LinkPreviewPlugin.php b/plugins/LinkPreview/LinkPreviewPlugin.php
index 65f896ca27..8bc726413d 100644
--- a/plugins/LinkPreview/LinkPreviewPlugin.php
+++ b/plugins/LinkPreview/LinkPreviewPlugin.php
@@ -51,7 +51,7 @@ class LinkPreviewPlugin extends Plugin
{
$user = common_current_user();
if ($user && common_config('attachments', 'process_links')) {
- $action->script('plugins/LinkPreview/linkpreview.min.js');
+ $action->script($this->path('linkpreview.min.js'));
$data = json_encode(array(
'api' => common_local_url('oembedproxy'),
'width' => common_config('attachments', 'thumbwidth'),
diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php
index 020c0818ad..13c5e22057 100644
--- a/plugins/Mapstraction/MapstractionPlugin.php
+++ b/plugins/Mapstraction/MapstractionPlugin.php
@@ -129,7 +129,7 @@ class MapstractionPlugin extends Plugin
break;
case 'openlayers':
// Use our included stripped & minified OpenLayers.
- $action->script(common_path('plugins/Mapstraction/OpenLayers/OpenLayers.js'));
+ $action->script($this->path('OpenLayers/OpenLayers.js'));
break;
case 'yahoo':
$action->script(sprintf('http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=%s',
@@ -145,13 +145,13 @@ class MapstractionPlugin extends Plugin
//
// Note that OpenLayers.js needs to be separate, or it won't
// be able to find its UI images and styles.
- $action->script(common_path('plugins/Mapstraction/usermap-mxn-openlayers.min.js'));
+ $action->script($this->path('usermap-mxn-openlayers.min.js'));
} else {
$action->script(sprintf('%s?(%s)',
- common_path('plugins/Mapstraction/js/mxn.js'),
+ $this->path('js/mxn.js'),
$this->provider));
- $action->script(common_path('plugins/Mapstraction/usermap.js'));
+ $action->script($this->path('usermap.js'));
}
$action->inlineScript(sprintf('var _provider = "%s";', $this->provider));
diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php
index 1bdccae7a8..6e93e364f7 100644
--- a/plugins/Meteor/MeteorPlugin.php
+++ b/plugins/Meteor/MeteorPlugin.php
@@ -89,7 +89,7 @@ class MeteorPlugin extends RealtimePlugin
{
$scripts = parent::_getScripts();
$scripts[] = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport).'/meteor.js';
- $scripts[] = common_path('plugins/Meteor/meteorupdater.min.js');
+ $scripts[] = $this->path('meteorupdater.min.js');
return $scripts;
}
diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php
index b50440682f..b9c25ab998 100644
--- a/plugins/MobileProfile/MobileProfilePlugin.php
+++ b/plugins/MobileProfile/MobileProfilePlugin.php
@@ -241,13 +241,13 @@ class MobileProfilePlugin extends WAP20Plugin
if (file_exists(Theme::file('css/mp-screen.css'))) {
$action->cssLink('css/mp-screen.css', null, 'screen');
} else {
- $action->cssLink('plugins/MobileProfile/mp-screen.css',null,'screen');
+ $action->cssLink($this->path('mp-screen.css'),null,'screen');
}
if (file_exists(Theme::file('css/mp-handheld.css'))) {
$action->cssLink('css/mp-handheld.css', null, 'handheld');
} else {
- $action->cssLink('plugins/MobileProfile/mp-handheld.css',null,'handheld');
+ $action->cssLink($this->path('mp-handheld.css'),null,'handheld');
}
// Allow other plugins to load their styles.
diff --git a/plugins/ModPlus/ModPlusPlugin.php b/plugins/ModPlus/ModPlusPlugin.php
index 3e7a8c7455..d2b7c09346 100644
--- a/plugins/ModPlus/ModPlusPlugin.php
+++ b/plugins/ModPlus/ModPlusPlugin.php
@@ -51,13 +51,13 @@ class ModPlusPlugin extends Plugin
{
$user = common_current_user();
if ($user) {
- $action->script('plugins/ModPlus/modplus.js');
+ $action->script($this->path('modplus.js'));
}
return true;
}
function onEndShowStatusNetStyles($action) {
- $action->cssLink('plugins/ModPlus/modplus.css');
+ $action->cssLink($this->path('modplus.css'));
return true;
}
diff --git a/plugins/NewMenu/NewMenuPlugin.php b/plugins/NewMenu/NewMenuPlugin.php
index 7aad0672bf..bc8132f0da 100644
--- a/plugins/NewMenu/NewMenuPlugin.php
+++ b/plugins/NewMenu/NewMenuPlugin.php
@@ -326,12 +326,12 @@ class NewMenuPlugin extends Plugin
function onEndShowStyles($action)
{
- if (($this->showCSS ||
+ if (($this->loadCSS ||
in_array(common_config('site', 'theme'),
array('default', 'identica', 'h4ck3r'))) &&
($action instanceof AccountSettingsAction ||
$action instanceof ConnectSettingsAction)) {
- $action->cssLink(common_path('plugins/NewMenu/newmenu.css'));
+ $action->cssLink($this->path('newmenu.css'));
}
return true;
}
diff --git a/plugins/NewMenu/newmenu.css b/plugins/NewMenu/newmenu.css
index f700fdcc45..67fe5826c0 100644
--- a/plugins/NewMenu/newmenu.css
+++ b/plugins/NewMenu/newmenu.css
@@ -41,7 +41,12 @@ border-radius-topright:7px;
border-radius-topright:0;
-moz-border-radius-topright:0;
-webkit-border-top-right-radius:0;
+min-height: 360px;
}
body[id$=settings] #aside_primary {
-display:none;
+float: right;
+width: 17.25%;
+margin-right: 10.45%;
+margin-top: 6px;
+min-height: 0px;
}
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 59c18746f0..8e6e2d2891 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -419,12 +419,12 @@ class OStatusPlugin extends Plugin
}
function onEndShowStatusNetStyles($action) {
- $action->cssLink('plugins/OStatus/theme/base/css/ostatus.css');
+ $action->cssLink($this->path('theme/base/css/ostatus.css'));
return true;
}
function onEndShowStatusNetScripts($action) {
- $action->script('plugins/OStatus/js/ostatus.js');
+ $action->script($this->path('js/ostatus.js'));
return true;
}
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
index 303e177a57..28f2dc0a97 100644
--- a/plugins/OStatus/classes/Ostatus_profile.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -1112,7 +1112,8 @@ class Ostatus_profile extends Memcached_DataObject
return $url;
}
}
- return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png');
+
+ return Plugin::staticPath('OStatus', 'images/96px-Feed-icon.svg.png');
}
/**
@@ -1781,12 +1782,14 @@ class Ostatus_profile extends Memcached_DataObject
$oprofile = Ostatus_profile::ensureWebfinger($rest);
break;
default:
- common_log(LOG_WARNING,
- "Unrecognized URI protocol for profile: $protocol ($uri)");
+ throw new ServerException("Unrecognized URI protocol for profile: $protocol ($uri)");
break;
}
+ } else {
+ throw new ServerException("No URI protocol for profile: ($uri)");
}
}
+
return $oprofile;
}
diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php
index 113187e1e3..246b1f9735 100644
--- a/plugins/Realtime/RealtimePlugin.php
+++ b/plugins/Realtime/RealtimePlugin.php
@@ -116,8 +116,9 @@ class RealtimePlugin extends Plugin
function onEndShowStatusNetStyles($action)
{
- $action->cssLink('plugins/Realtime/realtimeupdate.css',
- null, 'screen, projection, tv');
+ $action->cssLink(Plugin::staticPath('Realtime', 'realtimeupdate.css'),
+ null,
+ 'screen, projection, tv');
return true;
}
@@ -322,7 +323,7 @@ class RealtimePlugin extends Plugin
function _getScripts()
{
- return array('plugins/Realtime/realtimeupdate.min.js');
+ return array(Plugin::staticPath('Realtime', 'realtimeupdate.min.js'));
}
/**
diff --git a/plugins/ShareNotice/ShareNoticePlugin.php b/plugins/ShareNotice/ShareNoticePlugin.php
index 8b94f83c8f..0cd248e213 100644
--- a/plugins/ShareNotice/ShareNoticePlugin.php
+++ b/plugins/ShareNotice/ShareNoticePlugin.php
@@ -33,7 +33,7 @@ class ShareNoticePlugin extends Plugin
);
function onEndShowStatusNetStyles($action) {
- $action->cssLink('plugins/ShareNotice/css/sharenotice.css');
+ $action->cssLink($this->path('css/sharenotice.css'));
return true;
}
diff --git a/plugins/TabFocus/TabFocusPlugin.php b/plugins/TabFocus/TabFocusPlugin.php
index dd8a972767..1b1d1c2b0a 100644
--- a/plugins/TabFocus/TabFocusPlugin.php
+++ b/plugins/TabFocus/TabFocusPlugin.php
@@ -41,7 +41,7 @@ class TabFocusPlugin extends Plugin
function onEndShowScripts($action)
{
- $action->script('plugins/TabFocus/tabfocus.js');
+ $action->script($this->path('tabfocus.js'));
}
function onPluginVersion(&$versions)
diff --git a/plugins/TinyMCE/TinyMCEPlugin.php b/plugins/TinyMCE/TinyMCEPlugin.php
index e0640ebdf3..49bbdf90db 100644
--- a/plugins/TinyMCE/TinyMCEPlugin.php
+++ b/plugins/TinyMCE/TinyMCEPlugin.php
@@ -38,6 +38,10 @@ if (!defined('STATUSNET')) {
* Use TinyMCE library to allow rich text editing in the browser
*
* Converts the notice form in browser to a rich-text editor.
+ *
+ * FIXME: this plugin DOES NOT load its static files from the configured
+ * plugin server if one exists. There are cross-server permissions errors
+ * if you try to do that (something about window.tinymce).
*
* @category WYSIWYG
* @package StatusNet
diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php
index a993f8ff8f..f5a0b62588 100644
--- a/plugins/TwitterBridge/twitter.php
+++ b/plugins/TwitterBridge/twitter.php
@@ -45,7 +45,7 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser = new Foreign_user();
$fuser->nickname = $screen_name;
- $fuser->uri = 'http://twitter.com/#!/' . $screen_name;
+ $fuser->uri = 'http://twitter.com/' . $screen_name;
$fuser->id = $twitter_id;
$fuser->service = TWITTER_SERVICE;
$fuser->created = common_sql_now();
@@ -173,18 +173,20 @@ function broadcast_twitter($notice)
// Don't bother with basic auth, since it's no longer allowed
if (!empty($flink) && TwitterOAuthClient::isPackedToken($flink->credentials)) {
- if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) {
- $retweet = retweet_notice($flink, Notice::staticGet('id', $notice->repeat_of));
- if (is_object($retweet)) {
- Notice_to_status::saveNew($notice->id, twitter_id($retweet));
- return true;
+ if (is_twitter_bound($notice, $flink)) {
+ if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) {
+ $retweet = retweet_notice($flink, Notice::staticGet('id', $notice->repeat_of));
+ if (is_object($retweet)) {
+ Notice_to_status::saveNew($notice->id, twitter_id($retweet));
+ return true;
+ } else {
+ // Our error processing will have decided if we need to requeue
+ // this or can discard safely.
+ return $retweet;
+ }
} else {
- // Our error processing will have decided if we need to requeue
- // this or can discard safely.
- return $retweet;
+ return broadcast_oauth($notice, $flink);
}
- } else if (is_twitter_bound($notice, $flink)) {
- return broadcast_oauth($notice, $flink);
}
}
diff --git a/plugins/TwitterBridge/twitterimport.php b/plugins/TwitterBridge/twitterimport.php
index 3a4c0c43c0..475a99efac 100644
--- a/plugins/TwitterBridge/twitterimport.php
+++ b/plugins/TwitterBridge/twitterimport.php
@@ -264,7 +264,7 @@ class TwitterImport
function ensureProfile($user)
{
// check to see if there's already a profile for this user
- $profileurl = 'http://twitter.com/#!/' . $user->screen_name;
+ $profileurl = 'http://twitter.com/' . $user->screen_name;
$profile = $this->getProfileByUrl($user->screen_name, $profileurl);
if (!empty($profile)) {
diff --git a/plugins/TwitterBridge/twitterlogin.php b/plugins/TwitterBridge/twitterlogin.php
index 5b5bfae191..062c781f77 100644
--- a/plugins/TwitterBridge/twitterlogin.php
+++ b/plugins/TwitterBridge/twitterlogin.php
@@ -83,7 +83,7 @@ class TwitterloginAction extends Action
$this->elementStart('a', array('href' => common_local_url('twitterauthorization',
null,
array('signin' => true))));
- $this->element('img', array('src' => common_path('plugins/TwitterBridge/Sign-in-with-Twitter-lighter.png'),
+ $this->element('img', array('src' => Plugin::staticPath('TwitterBridge', 'Sign-in-with-Twitter-lighter.png'),
'alt' => _m('Sign in with Twitter')));
$this->elementEnd('a');
}
diff --git a/plugins/YammerImport/actions/yammeradminpanel.php b/plugins/YammerImport/actions/yammeradminpanel.php
index 3faf390ac1..4714154290 100644
--- a/plugins/YammerImport/actions/yammeradminpanel.php
+++ b/plugins/YammerImport/actions/yammeradminpanel.php
@@ -176,12 +176,12 @@ class YammeradminpanelAction extends AdminPanelAction
function showStylesheets()
{
parent::showStylesheets();
- $this->cssLink('plugins/YammerImport/css/admin.css', null, 'screen, projection, tv');
+ $this->cssLink(Plugin::staticPath('YammerImport', 'css/admin.css'), null, 'screen, projection, tv');
}
function showScripts()
{
parent::showScripts();
- $this->script('plugins/YammerImport/js/yammer-admin.js');
+ $this->script(Plugin::staticPath('YammerImport', 'js/yammer-admin.js'));
}
}