diff options
65 files changed, 2415 insertions, 763 deletions
diff --git a/EVENTS.txt b/EVENTS.txt index bb4936b35..2da6f3da6 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -363,6 +363,14 @@ EndProfileRemoteSubscribe: After showing the link to remote subscription - $userprofile: UserProfile widget - &$profile: the profile being shown +StartGroupSubscribe: Before showing the link to remote subscription +- $action: the current action +- $group: the group being shown + +EndGroupSubscribe: After showing the link to remote subscription +- $action: the current action +- $group: the group being shown + StartProfilePageProfileSection: Starting to show the section of the profile page with the actual profile data; hook to prevent showing the profile (e.g.) @@ -770,12 +778,30 @@ StartShowSubscriptionsContent: before showing the subscriptions content EndShowSubscriptionsContent: after showing the subscriptions content - $action: the current action +StartShowUserGroupsContent: before showing the user groups content +- $action: the current action + +EndShowUserGroupsContent: after showing the user groups content +- $action: the current action + StartShowAllContent: before showing the all (you and friends) content - $action: the current action EndShowAllContent: after showing the all (you and friends) content - $action: the current action +StartShowSubscriptionsMiniList: at the start of subscriptions mini list +- $action: the current action + +EndShowSubscriptionsMiniList: at the end of subscriptions mini list +- $action: the current action + +StartShowGroupsMiniList: at the start of groups mini list +- $action: the current action + +EndShowGroupsMiniList: at the end of groups mini list +- $action: the current action + StartDeleteUserForm: starting the data in the form for deleting a user - $action: action being shown - $user: user being deleted diff --git a/actions/allrss.php b/actions/allrss.php index 28b1be27d..01e737ad7 100644 --- a/actions/allrss.php +++ b/actions/allrss.php @@ -83,6 +83,7 @@ class AllrssAction extends Rss10Action function getNotices($limit=0) { $cur = common_current_user(); + $user = $this->user; if (!empty($cur) && $cur->id == $user->id) { $notice = $this->user->noticeInbox(0, $limit); @@ -90,7 +91,6 @@ class AllrssAction extends Rss10Action $notice = $this->user->noticesWithFriends(0, $limit); } - $user = $this->user; $notice = $user->noticesWithFriends(0, $limit); $notices = array(); diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index d0af49844..8f971392b 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -104,30 +104,21 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction function showTimeline() { - $sitename = common_config('site', 'name'); - $avatar = $this->group->homepage_logo; - $title = sprintf(_("%s timeline"), $this->group->nickname); - - $subtitle = sprintf( - _('Updates from %1$s on %2$s!'), - $this->group->nickname, - $sitename - ); - - $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); + // We'll pull common formatting out of this for other formats + $atom = new AtomGroupNoticeFeed($this->group); switch($this->format) { case 'xml': $this->showXmlTimeline($this->notices); break; case 'rss': - $this->showRssTimeline( + $this->showRssTimeline( $this->notices, - $title, + $atom->title, $this->group->homeUrl(), - $subtitle, + $atom->subtitle, null, - $logo + $atom->logo ); break; case 'atom': @@ -136,38 +127,22 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction try { - $atom = new AtomGroupNoticeFeed($this->group); - - // @todo set all this Atom junk up inside the feed class - - $atom->setId($id); - $atom->setTitle($title); - $atom->setSubtitle($subtitle); - $atom->setLogo($logo); - $atom->setUpdated('now'); - $atom->addAuthorRaw($this->group->asAtomAuthor()); $atom->setActivitySubject($this->group->asActivitySubject()); - $atom->addLink($this->group->homeUrl()); - $id = $this->arg('id'); $aargs = array('format' => 'atom'); if (!empty($id)) { $aargs['id'] = $id; } + $self = $this->getSelfUri('ApiTimelineGroup', $aargs); - $atom->setId($this->getSelfUri('ApiTimelineGroup', $aargs)); - - $atom->addLink( - $this->getSelfUri('ApiTimelineGroup', $aargs), - array('rel' => 'self', 'type' => 'application/atom+xml') - ); + $atom->setId($self); + $atom->setSelfLink($self); $atom->addEntryFromNotices($this->notices); - //$this->raw($atom->getString()); - print $atom->getString(); // temp hack until PuSH feeds are redone cleanly + $this->raw($atom->getString()); } catch (Atom10FeedException $e) { $this->serverError( diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 94491946c..2d0047c04 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -112,19 +112,17 @@ class ApiTimelineUserAction extends ApiBareAuthAction function showTimeline() { $profile = $this->user->getProfile(); - $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); - $sitename = common_config('site', 'name'); - $title = sprintf(_("%s timeline"), $this->user->nickname); + // We'll use the shared params from the Atom stub + // for other feed types. + $atom = new AtomUserNoticeFeed($this->user); + $title = $atom->title; $link = common_local_url( 'showstream', array('nickname' => $this->user->nickname) ); - $subtitle = sprintf( - _('Updates from %1$s on %2$s!'), - $this->user->nickname, $sitename - ); - $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $subtitle = $atom->subtitle; + $logo = $atom->logo; // FriendFeed's SUP protocol // Also added RSS and Atom feeds @@ -146,47 +144,18 @@ class ApiTimelineUserAction extends ApiBareAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); - // @todo set all this Atom junk up inside the feed class - - $atom = new AtomUserNoticeFeed($this->user); - - $atom->setTitle($title); - $atom->setSubtitle($subtitle); - $atom->setLogo($logo); - $atom->setUpdated('now'); - - $atom->addLink( - common_local_url( - 'showstream', - array('nickname' => $this->user->nickname) - ) - ); - $id = $this->arg('id'); $aargs = array('format' => 'atom'); if (!empty($id)) { $aargs['id'] = $id; } - - $atom->setId($this->getSelfUri('ApiTimelineUser', $aargs)); - - $atom->addLink( - $this->getSelfUri('ApiTimelineUser', $aargs), - array('rel' => 'self', 'type' => 'application/atom+xml') - ); - - $atom->addLink( - $suplink, - array( - 'rel' => 'http://api.friendfeed.com/2008/03#sup', - 'type' => 'application/json' - ) - ); + $self = $this->getSelfUri('ApiTimelineUser', $aargs); + $atom->setId($self); + $atom->setSelfLink($self); $atom->addEntryFromNotices($this->notices); - #$this->raw($atom->getString()); - print $atom->getString(); // temporary for output buffering + $this->raw($atom->getString()); break; case 'json': diff --git a/actions/grantrole.php b/actions/grantrole.php new file mode 100644 index 000000000..cd6bd4d79 --- /dev/null +++ b/actions/grantrole.php @@ -0,0 +1,99 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Action class to sandbox an abusive user + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Sandbox a user. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class GrantRoleAction extends ProfileFormAction +{ + /** + * Check parameters + * + * @param array $args action arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + if (!parent::prepare($args)) { + return false; + } + + $this->role = $this->arg('role'); + if (!Profile_role::isValid($this->role)) { + $this->clientError(_("Invalid role.")); + return false; + } + if (!Profile_role::isSettable($this->role)) { + $this->clientError(_("This role is reserved and cannot be set.")); + return false; + } + + $cur = common_current_user(); + + assert(!empty($cur)); // checked by parent + + if (!$cur->hasRight(Right::GRANTROLE)) { + $this->clientError(_("You cannot grant user roles on this site.")); + return false; + } + + assert(!empty($this->profile)); // checked by parent + + if ($this->profile->hasRole($this->role)) { + $this->clientError(_("User already has this role.")); + return false; + } + + return true; + } + + /** + * Sandbox a user. + * + * @return void + */ + + function handlePost() + { + $this->profile->grantRole($this->role); + } +} diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index b1467f0d0..f125f4c63 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -99,7 +99,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $application = $profile->getApplications($offset, $limit); - $cnt == 0; + $cnt = 0; if (!empty($application)) { $al = new ApplicationList($application, $user, $this, true); @@ -112,7 +112,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $this->pagination($this->page > 1, $cnt > APPS_PER_PAGE, $this->page, 'connectionssettings', - array('nickname' => $this->user->nickname)); + array('nickname' => $user->nickname)); } /** diff --git a/actions/revokerole.php b/actions/revokerole.php new file mode 100644 index 000000000..b78c1c25a --- /dev/null +++ b/actions/revokerole.php @@ -0,0 +1,99 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Action class to sandbox an abusive user + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Sandbox a user. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class RevokeRoleAction extends ProfileFormAction +{ + /** + * Check parameters + * + * @param array $args action arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + if (!parent::prepare($args)) { + return false; + } + + $this->role = $this->arg('role'); + if (!Profile_role::isValid($this->role)) { + $this->clientError(_("Invalid role.")); + return false; + } + if (!Profile_role::isSettable($this->role)) { + $this->clientError(_("This role is reserved and cannot be set.")); + return false; + } + + $cur = common_current_user(); + + assert(!empty($cur)); // checked by parent + + if (!$cur->hasRight(Right::REVOKEROLE)) { + $this->clientError(_("You cannot revoke user roles on this site.")); + return false; + } + + assert(!empty($this->profile)); // checked by parent + + if (!$this->profile->hasRole($this->role)) { + $this->clientError(_("User doesn't have this role.")); + return false; + } + + return true; + } + + /** + * Sandbox a user. + * + * @return void + */ + + function handlePost() + { + $this->profile->revokeRole($this->role); + } +} diff --git a/actions/showgroup.php b/actions/showgroup.php index 4e1fcb6c7..5704b13d1 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -301,19 +301,20 @@ class ShowgroupAction extends GroupDesignAction $this->element('h2', null, _('Group actions')); $this->elementStart('ul'); $this->elementStart('li', 'entity_subscribe'); - $cur = common_current_user(); - 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('StartGroupSubscribe', array($this, $this->group))) { + $cur = common_current_user(); + 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)); } - $this->elementEnd('li'); - $this->elementEnd('ul'); $this->elementEnd('div'); } diff --git a/actions/siteadminpanel.php b/actions/siteadminpanel.php index 8c8f8b374..cb3c2e8fd 100644 --- a/actions/siteadminpanel.php +++ b/actions/siteadminpanel.php @@ -66,7 +66,7 @@ class SiteadminpanelAction extends AdminPanelAction function getInstructions() { - return _('Basic settings for this StatusNet site.'); + return _('Basic settings for this StatusNet site'); } /** @@ -90,10 +90,11 @@ class SiteadminpanelAction extends AdminPanelAction function saveSettings() { - static $settings = array('site' => array('name', 'broughtby', 'broughtbyurl', - 'email', 'timezone', 'language', - 'site', 'textlimit', 'dupelimit'), - 'snapshot' => array('run', 'reporturl', 'frequency')); + static $settings = array( + 'site' => array('name', 'broughtby', 'broughtbyurl', + 'email', 'timezone', 'language', + 'site', 'textlimit', 'dupelimit'), + ); $values = array(); @@ -158,25 +159,6 @@ class SiteadminpanelAction extends AdminPanelAction $this->clientError(sprintf(_('Unknown language "%s".'), $values['site']['language'])); } - // Validate report URL - - if (!is_null($values['snapshot']['reporturl']) && - !Validate::uri($values['snapshot']['reporturl'], array('allowed_schemes' => array('http', 'https')))) { - $this->clientError(_("Invalid snapshot report URL.")); - } - - // Validate snapshot run value - - if (!in_array($values['snapshot']['run'], array('web', 'cron', 'never'))) { - $this->clientError(_("Invalid snapshot run value.")); - } - - // Validate snapshot run value - - if (!Validate::number($values['snapshot']['frequency'])) { - $this->clientError(_("Snapshot frequency must be a number.")); - } - // Validate text limit if (!Validate::number($values['site']['textlimit'], array('min' => 140))) { @@ -277,40 +259,14 @@ class SiteAdminPanelForm extends AdminForm $this->unli(); $this->li(); - $this->out->dropdown('language', _('Language'), - get_nice_language_list(), _('Default site language'), + $this->out->dropdown('language', _('Default language'), + get_nice_language_list(), _('Site language when autodetection from browser settings is not available'), false, $this->value('language')); $this->unli(); $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset'); - $this->out->elementStart('fieldset', array('id' => 'settings_admin_snapshots')); - $this->out->element('legend', null, _('Snapshots')); - $this->out->elementStart('ul', 'form_data'); - $this->li(); - $snapshot = array('web' => _('Randomly during Web hit'), - 'cron' => _('In a scheduled job'), - 'never' => _('Never')); - $this->out->dropdown('run', _('Data snapshots'), - $snapshot, _('When to send statistical data to status.net servers'), - false, $this->value('run', 'snapshot')); - $this->unli(); - - $this->li(); - $this->input('frequency', _('Frequency'), - _('Snapshots will be sent once every N web hits'), - 'snapshot'); - $this->unli(); - - $this->li(); - $this->input('reporturl', _('Report URL'), - _('Snapshots will be sent to this URL'), - 'snapshot'); - $this->unli(); - $this->out->elementEnd('ul'); - $this->out->elementEnd('fieldset'); - $this->out->elementStart('fieldset', array('id' => 'settings_admin_limits')); $this->out->element('legend', null, _('Limits')); $this->out->elementStart('ul', 'form_data'); diff --git a/actions/sitenoticeadminpanel.php b/actions/sitenoticeadminpanel.php new file mode 100644 index 000000000..613a2e96b --- /dev/null +++ b/actions/sitenoticeadminpanel.php @@ -0,0 +1,201 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Site notice administration panel + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + +/** + * Update the site-wide notice text + * + * @category Admin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class SitenoticeadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _('Site Notice'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _('Edit site-wide message'); + } + + /** + * Show the site notice admin panel form + * + * @return void + */ + + function showForm() + { + $form = new SiteNoticeAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + $siteNotice = $this->trimmed('site-notice'); + + // assert(all values are valid); + // This throws an exception on validation errors + + $this->validate(&$siteNotice); + + $config = new Config(); + + $result = Config::save('site', 'notice', $siteNotice); + + if (!result) { + $this->ServerError(_("Unable to save site notice.")); + } + } + + function validate(&$siteNotice) + { + // Validate notice text + + if (mb_strlen($siteNotice) > 255) { + $this->clientError( + _('Max length for the site-wide notice is 255 chars') + ); + } + + // scrub HTML input + + $config = array( + 'safe' => 1, + 'deny_attribute' => 'id,style,on*' + ); + + $siteNotice = htmLawed($siteNotice, $config); + } +} + +class SiteNoticeAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_site_notice_admin_panel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('sitenoticeadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('ul', 'form_data'); + + $this->out->elementStart('li'); + $this->out->textarea( + 'site-notice', + _('Site notice text'), + common_config('site', 'notice'), + _('Site-wide notice text (255 chars max; HTML okay)') + ); + $this->out->elementEnd('li'); + + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit( + 'submit', + _('Save'), + 'submit', + null, + _('Save site notice') + ); + } +} diff --git a/actions/snapshotadminpanel.php b/actions/snapshotadminpanel.php new file mode 100644 index 000000000..a0c2315bc --- /dev/null +++ b/actions/snapshotadminpanel.php @@ -0,0 +1,251 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Snapshots administration panel + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Settings + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Manage snapshots + * + * @category Admin + * @package StatusNet + * @author Zach Copley <zach@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class SnapshotadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _('Snapshots'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _('Manage snapshot configuration'); + } + + /** + * Show the snapshots admin panel form + * + * @return void + */ + + function showForm() + { + $form = new SnapshotAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'snapshot' => array('run', 'reporturl', 'frequency') + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] = $this->trimmed($setting); + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // Validate snapshot run value + + if (!in_array($values['snapshot']['run'], array('web', 'cron', 'never'))) { + $this->clientError(_("Invalid snapshot run value.")); + } + + // Validate snapshot frequency value + + if (!Validate::number($values['snapshot']['frequency'])) { + $this->clientError(_("Snapshot frequency must be a number.")); + } + + // Validate report URL + + if (!is_null($values['snapshot']['reporturl']) + && !Validate::uri( + $values['snapshot']['reporturl'], + array('allowed_schemes' => array('http', 'https') + ) + )) { + $this->clientError(_("Invalid snapshot report URL.")); + } + } +} + +class SnapshotAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_snapshot_admin_panel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('snapshotadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_admin_snapshots') + ); + $this->out->element('legend', null, _('Snapshots')); + $this->out->elementStart('ul', 'form_data'); + $this->li(); + $snapshot = array( + 'web' => _('Randomly during Web hit'), + 'cron' => _('In a scheduled job'), + 'never' => _('Never') + ); + $this->out->dropdown( + 'run', + _('Data snapshots'), + $snapshot, + _('When to send statistical data to status.net servers'), + false, + $this->value('run', 'snapshot') + ); + $this->unli(); + + $this->li(); + $this->input( + 'frequency', + _('Frequency'), + _('Snapshots will be sent once every N web hits'), + 'snapshot' + ); + $this->unli(); + + $this->li(); + $this->input( + 'reporturl', + _('Report URL'), + _('Snapshots will be sent to this URL'), + 'snapshot' + ); + $this->unli(); + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit( + 'submit', + _('Save'), + 'submit', + null, + _('Save snapshot settings') + ); + } +} diff --git a/actions/subscribers.php b/actions/subscribers.php index cd3e2ee5b..4bced6284 100644 --- a/actions/subscribers.php +++ b/actions/subscribers.php @@ -143,9 +143,12 @@ class SubscribersListItem extends SubscriptionListItem function showActions() { $this->startActions(); - $this->showSubscribeButton(); - // Relevant code! - $this->showBlockForm(); + if (Event::handle('StartProfileListItemActionElements', array($this))) { + $this->showSubscribeButton(); + // Relevant code! + $this->showBlockForm(); + Event::handle('EndProfileListItemActionElements', array($this)); + } $this->endActions(); } diff --git a/actions/usergroups.php b/actions/usergroups.php index 97faabae6..29bda0a76 100644 --- a/actions/usergroups.php +++ b/actions/usergroups.php @@ -130,22 +130,26 @@ class UsergroupsAction extends OwnerDesignAction _('Search for more groups')); $this->elementEnd('p'); - $offset = ($this->page-1) * GROUPS_PER_PAGE; - $limit = GROUPS_PER_PAGE + 1; + if (Event::handle('StartShowUserGroupsContent', array($this))) { + $offset = ($this->page-1) * GROUPS_PER_PAGE; + $limit = GROUPS_PER_PAGE + 1; + + $groups = $this->user->getGroups($offset, $limit); + + if ($groups) { + $gl = new GroupList($groups, $this->user, $this); + $cnt = $gl->show(); + if (0 == $cnt) { + $this->showEmptyListMessage(); + } + } - $groups = $this->user->getGroups($offset, $limit); + $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE, + $this->page, 'usergroups', + array('nickname' => $this->user->nickname)); - if ($groups) { - $gl = new GroupList($groups, $this->user, $this); - $cnt = $gl->show(); - if (0 == $cnt) { - $this->showEmptyListMessage(); - } + Event::handle('EndShowUserGroupsContent', array($this)); } - - $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE, - $this->page, 'usergroups', - array('nickname' => $this->user->nickname)); } function showEmptyListMessage() diff --git a/classes/File.php b/classes/File.php index 79a7d6681..4ecd3b959 100644 --- a/classes/File.php +++ b/classes/File.php @@ -290,5 +290,12 @@ class File extends Memcached_DataObject } return $enclosure; } + + // quick back-compat hack, since there's still code using this + function isEnclosure() + { + $enclosure = $this->getEnclosure(); + return !empty($enclosure); + } } diff --git a/classes/Notice.php b/classes/Notice.php index 63dc96897..4c7e6ab4b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -211,6 +211,8 @@ class Notice extends Memcached_DataObject * extracting ! tags from content * array 'tags' list of hashtag strings to save with the notice * in place of extracting # tags from content + * array 'urls' list of attached/referred URLs to save with the + * notice in place of extracting links from content * @fixme tag override * * @return Notice @@ -380,8 +382,11 @@ class Notice extends Memcached_DataObject $notice->saveTags(); } - // @fixme pass in data for URLs too? - $notice->saveUrls(); + if (isset($urls)) { + $notice->saveKnownUrls($urls); + } else { + $notice->saveUrls(); + } // Prepare inbox delivery, may be queued to background. $notice->distribute(); @@ -427,6 +432,25 @@ class Notice extends Memcached_DataObject common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id); } + /** + * Save the given URLs as related links/attachments to the db + * + * follow redirects and save all available file information + * (mimetype, date, size, oembed, etc.) + * + * @return void + */ + function saveKnownUrls($urls) + { + // @fixme validation? + foreach ($urls as $url) { + File::processNew($url, $this->id); + } + } + + /** + * @private callback + */ function saveUrl($data) { list($url, $notice_id) = $data; File::processNew($url, $notice_id); @@ -853,7 +877,7 @@ class Notice extends Memcached_DataObject foreach (array_unique($match[1]) as $nickname) { /* XXX: remote groups. */ - $group = User_group::getForNickname($nickname); + $group = User_group::getForNickname($nickname, $profile); if (empty($group)) { continue; @@ -1082,7 +1106,7 @@ class Notice extends Memcached_DataObject return $groups; } - function asAtomEntry($namespace=false, $source=false) + function asAtomEntry($namespace=false, $source=false, $author=true) { $profile = $this->getProfile(); @@ -1127,8 +1151,10 @@ class Notice extends Memcached_DataObject $xs->element('title', null, $this->content); - $xs->raw($profile->asAtomAuthor()); - $xs->raw($profile->asActivityActor()); + if ($author) { + $xs->raw($profile->asAtomAuthor()); + $xs->raw($profile->asActivityActor()); + } $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html', diff --git a/classes/Profile.php b/classes/Profile.php index 470ef3320..0322c9358 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -282,6 +282,32 @@ class Profile extends Memcached_DataObject } } + function getGroups($offset=0, $limit=null) + { + $qry = + 'SELECT user_group.* ' . + 'FROM user_group JOIN group_member '. + 'ON user_group.id = group_member.group_id ' . + 'WHERE group_member.profile_id = %d ' . + 'ORDER BY group_member.created DESC '; + + if ($offset>0 && !is_null($limit)) { + if ($offset) { + if (common_config('db','type') == 'pgsql') { + $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $qry .= ' LIMIT ' . $offset . ', ' . $limit; + } + } + } + + $groups = new User_group(); + + $cnt = $groups->query(sprintf($qry, $this->id)); + + return $groups; + } + function avatarUrl($size=AVATAR_PROFILE_SIZE) { $avatar = $this->getAvatar($size); @@ -717,6 +743,10 @@ class Profile extends Memcached_DataObject case Right::CONFIGURESITE: $result = $this->hasRole(Profile_role::ADMINISTRATOR); break; + case Right::GRANTROLE: + case Right::REVOKEROLE: + $result = $this->hasRole(Profile_role::OWNER); + break; case Right::NEWNOTICE: case Right::NEWMESSAGE: case Right::SUBSCRIBE: diff --git a/classes/Profile_role.php b/classes/Profile_role.php index bf2c453ed..d0a0b31f0 100644 --- a/classes/Profile_role.php +++ b/classes/Profile_role.php @@ -53,4 +53,21 @@ class Profile_role extends Memcached_DataObject const ADMINISTRATOR = 'administrator'; const SANDBOXED = 'sandboxed'; const SILENCED = 'silenced'; + + public static function isValid($role) + { + // @fixme could probably pull this from class constants + $known = array(self::OWNER, + self::MODERATOR, + self::ADMINISTRATOR, + self::SANDBOXED, + self::SILENCED); + return in_array($role, $known); + } + + public static function isSettable($role) + { + $allowedRoles = array('administrator', 'moderator'); + return self::isValid($role) && in_array($role, $allowedRoles); + } } diff --git a/classes/User.php b/classes/User.php index 357c1e501..fade0f35d 100644 --- a/classes/User.php +++ b/classes/User.php @@ -613,28 +613,8 @@ class User extends Memcached_DataObject function getGroups($offset=0, $limit=null) { - $qry = - 'SELECT user_group.* ' . - 'FROM user_group JOIN group_member '. - 'ON user_group.id = group_member.group_id ' . - 'WHERE group_member.profile_id = %d ' . - 'ORDER BY group_member.created DESC '; - - if ($offset>0 && !is_null($limit)) { - if ($offset) { - if (common_config('db','type') == 'pgsql') { - $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; - } else { - $qry .= ' LIMIT ' . $offset . ', ' . $limit; - } - } - } - - $groups = new User_group(); - - $cnt = $groups->query(sprintf($qry, $this->id)); - - return $groups; + $profile = $this->getProfile(); + return $profile->getGroups($offset, $limit); } function getSubscriptions($offset=0, $limit=null) diff --git a/classes/User_group.php b/classes/User_group.php index e92887474..7be55163a 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -279,12 +279,26 @@ class User_group extends Memcached_DataObject return true; } - static function getForNickname($nickname) + static function getForNickname($nickname, $profile=null) { $nickname = common_canonical_nickname($nickname); - $group = User_group::staticGet('nickname', $nickname); + + // Are there any matching remote groups this profile's in? + if ($profile) { + $group = $profile->getGroups(); + while ($group->fetch()) { + if ($group->nickname == $nickname) { + // @fixme is this the best way? + return clone($group); + } + } + } + + // If not, check local groups. + + $group = Local_group::staticGet('nickname', $nickname); if (!empty($group)) { - return $group; + return User_group::staticGet('id', $group->group_id); } $alias = Group_alias::staticGet('alias', $nickname); if (!empty($alias)) { @@ -441,6 +455,11 @@ class User_group extends Memcached_DataObject $group = new User_group(); $group->query('BEGIN'); + + if (empty($uri)) { + // fill in later... + $uri = null; + } $group->nickname = $nickname; $group->fullname = $fullname; @@ -200,7 +200,7 @@ function checkMirror($action_obj, $args) function isLoginAction($action) { - static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds'); + static $loginActions = array('login', 'recoverpassword', 'api', 'doc', 'register', 'publicxrds', 'otp'); $login = null; diff --git a/install.php b/install.php index 435f6d63b..41024c901 100644 --- a/install.php +++ b/install.php @@ -31,6 +31,7 @@ * @author Robin Millette <millette@controlyourself.ca> * @author Sarven Capadisli <csarven@status.net> * @author Tom Adams <tom@holizz.com> + * @author Zach Copley <zach@status.net> * @license GNU Affero General Public License http://www.gnu.org/licenses/ * @version 0.9.x * @link http://status.net @@ -490,15 +491,25 @@ function showForm() <p class="form_guide">Database name</p> </li> <li> - <label for="username">Username</label> + <label for="username">DB username</label> <input type="text" id="username" name="username" /> <p class="form_guide">Database username</p> </li> <li> - <label for="password">Password</label> + <label for="password">DB password</label> <input type="password" id="password" name="password" /> <p class="form_guide">Database password (optional)</p> </li> + <li> + <label for="admin_nickname">Administrator nickname</label> + <input type="text" id="admin_nickname" name="admin_nickname" /> + <p class="form_guide">Nickname for the initial StatusNet user (administrator)</p> + </li> + <li> + <label for="initial_user_password">Administrator password</label> + <input type="password" id="admin_password" name="admin_password" /> + <p class="form_guide">Password for the initial StatusNet user (administrator)</p> + </li> </ul> <input type="submit" name="submit" class="submit" value="Submit" /> </fieldset> @@ -521,6 +532,10 @@ function handlePost() $password = $_POST['password']; $sitename = $_POST['sitename']; $fancy = !empty($_POST['fancy']); + + $adminNick = $_POST['admin_nickname']; + $adminPass = $_POST['admin_password']; + $server = $_SERVER['HTTP_HOST']; $path = substr(dirname($_SERVER['PHP_SELF']), 1); @@ -552,6 +567,16 @@ STR; $fail = true; } + if (empty($adminNick)) { + updateStatus("No initial StatusNet user nickname specified.", true); + $fail = true; + } + + if (empty($adminPass)) { + updateStatus("No initial StatusNet user password specified.", true); + $fail = true; + } + if ($fail) { showForm(); return; @@ -574,13 +599,29 @@ STR; return; } + // Okay, cross fingers and try to register an initial user + if (registerInitialUser($adminNick, $adminPass)) { + updateStatus( + "An initial user with the administrator role has been created." + ); + } else { + updateStatus( + "Could not create initial StatusNet user (administrator).", + true + ); + showForm(); + return; + } + /* TODO https needs to be considered */ $link = "http://".$server.'/'.$path; updateStatus("StatusNet has been installed at $link"); - updateStatus("You can visit your <a href='$link'>new StatusNet site</a>."); + updateStatus( + "You can visit your <a href='$link'>new StatusNet site</a> (login as '$adminNick')." + ); } function Pgsql_Db_installer($host, $database, $username, $password) @@ -756,6 +797,33 @@ function runDbScript($filename, $conn, $type = 'mysqli') return true; } +function registerInitialUser($nickname, $password) +{ + define('STATUSNET', true); + define('LACONICA', true); // compatibility + + require_once INSTALLDIR . '/lib/common.php'; + + $user = User::register( + array('nickname' => $nickname, + 'password' => $password, + 'fullname' => $nickname + ) + ); + + if (empty($user)) { + return false; + } + + // give initial user carte blanche + + $user->grantRole('owner'); + $user->grantRole('moderator'); + $user->grantRole('administrator'); + + return true; +} + ?> <?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?> <!DOCTYPE html @@ -765,10 +833,10 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" <head> <title>Install StatusNet</title> <link rel="shortcut icon" href="favicon.ico"/> - <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.8" media="screen, projection, tv"/> - <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css?version=0.8" /><![endif]--> - <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css?version=0.8" /><![endif]--> - <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/default/css/ie.css?version=0.8" /><![endif]--> + <link rel="stylesheet" type="text/css" href="theme/default/css/display.css" media="screen, projection, tv"/> + <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css" /><![endif]--> + <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css" /><![endif]--> + <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/default/css/ie.css" /><![endif]--> <script src="js/jquery.min.js"></script> <script src="js/install.js"></script> </head> @@ -784,8 +852,10 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" </div> <div id="core"> <div id="content"> - <h1>Install StatusNet</h1> + <div id="content_inner"> + <h1>Install StatusNet</h1> <?php main(); ?> + </div> </div> </div> </div> diff --git a/js/util.js b/js/util.js index 949aec957..3efda0d7b 100644 --- a/js/util.js +++ b/js/util.js @@ -53,7 +53,7 @@ var SN = { // StatusNet NoticeLocationNs: 'notice_data-location_ns', NoticeGeoName: 'notice_data-geo_name', NoticeDataGeo: 'notice_data-geo', - NoticeDataGeoCookie: 'notice_data-geo_cookie', + NoticeDataGeoCookie: 'NoticeDataGeo', NoticeDataGeoSelected: 'notice_data-geo_selected', StatusNetInstance:'StatusNetInstance' } @@ -423,8 +423,11 @@ var SN = { // StatusNet }; notice.find('a.attachment').click(function() { - $().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'}); - return false; + var attachId = ($(this).attr('id').substring('attachment'.length + 1)); + if (attachId) { + $().jOverlay({url: $('address .url')[0].href+'attachment/' + attachId + '/ajax'}); + return false; + } }); if ($('#shownotice').length == 0) { @@ -494,7 +497,7 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeLocationId).val(''); $('#'+SN.C.S.NoticeDataGeo).attr('checked', false); - $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/', expires: SN.U.GetFullYear(2029, 0, 1) }); + $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/' }); } function getJSONgeocodeURL(geocodeURL, data) { @@ -537,7 +540,7 @@ var SN = { // StatusNet NDG: true }; - $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/', expires: SN.U.GetFullYear(2029, 0, 1) }); + $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/' }); }); } diff --git a/lib/action.php b/lib/action.php index a804b8e82..10394c789 100644 --- a/lib/action.php +++ b/lib/action.php @@ -420,13 +420,6 @@ class Action extends HTMLOutputter // lawsuit function showPrimaryNav() { $user = common_current_user(); - $connect = ''; - if (common_config('xmpp', 'enabled')) { - $connect = 'imsettings'; - } else if (common_config('sms', 'enabled')) { - $connect = 'smssettings'; - } - $this->elementStart('dl', array('id' => 'site_nav_global_primary')); $this->element('dt', null, _('Primary site navigation')); $this->elementStart('dd'); @@ -442,14 +435,12 @@ class Action extends HTMLOutputter // lawsuit $tooltip = _m('TOOLTIP', 'Change your email, avatar, password, profile'); // TRANS: Main menu option when logged in for access to user settings $this->menuItem(common_local_url('profilesettings'), - _m('MENU', 'Account'), $tooltip, false, 'nav_account'); - if ($connect) { - // TRANS: Tooltip for main menu option "Services" - $tooltip = _m('TOOLTIP', 'Connect to services'); - // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services - $this->menuItem(common_local_url($connect), - _m('MENU', 'Connect'), $tooltip, false, 'nav_connect'); - } + _('Account'), $tooltip, false, 'nav_account'); + // TRANS: Tooltip for main menu option "Services" + $tooltip = _m('TOOLTIP', 'Connect to services'); + // TRANS: Main menu option when logged in and connection are possible for access to options to connect to other services + $this->menuItem(common_local_url('oauthconnectionssettings'), + _('Connect'), $tooltip, false, 'nav_connect'); if ($user->hasRight(Right::CONFIGURESITE)) { // TRANS: Tooltip for menu option "Admin" $tooltip = _m('TOOLTIP', 'Change site configuration'); diff --git a/lib/activity.php b/lib/activity.php index b20153213..e1bce6f19 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -1044,6 +1044,7 @@ class Activity public $id; // ID of the activity public $title; // title of the activity public $categories = array(); // list of AtomCategory objects + public $enclosures = array(); // list of enclosure URL references /** * Turns a regular old Atom <entry> into a magical activity @@ -1059,6 +1060,18 @@ class Activity } $this->entry = $entry; + + // @fixme Don't send in a DOMDocument + if ($feed instanceof DOMDocument) { + common_log( + LOG_WARNING, + 'Activity::__construct() - ' + . 'DOMDocument passed in for feed by mistake. ' + . "Expecting a 'feed' DOMElement." + ); + $feed = $feed->getElementsByTagName('feed')->item(0); + } + $this->feed = $feed; $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); @@ -1140,6 +1153,10 @@ class Activity $this->categories[] = new AtomCategory($catEl); } } + + foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) { + $this->enclosures[] = $link->getAttribute('href'); + } } /** diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php index a0cdab8a4..f3f86449f 100644 --- a/lib/adminpanelaction.php +++ b/lib/adminpanelaction.php @@ -176,6 +176,34 @@ class AdminPanelAction extends Action } /** + * Show content block. Overrided just to add a special class + * to the content div to allow styling. + * + * @return nothing + */ + function showContentBlock() + { + $this->elementStart('div', array('id' => 'content', 'class' => 'admin')); + $this->showPageTitle(); + $this->showPageNoticeBlock(); + $this->elementStart('div', array('id' => 'content_inner')); + // show the actual content (forms, lists, whatever) + $this->showContent(); + $this->elementEnd('div'); + $this->elementEnd('div'); + } + + /** + * There is no data for aside, so, we don't output + * + * @return nothing + */ + function showAside() + { + + } + + /** * show human-readable instructions for the page, or * a success/failure on save. * @@ -345,32 +373,48 @@ class AdminPanelNav extends Widget // TRANS: Menu item title/tooltip $menu_title = _('User configuration'); // TRANS: Menu item for site administration - $this->out->menuItem(common_local_url('useradminpanel'), _m('MENU', 'User'), - $menu_title, $action_name == 'useradminpanel', 'nav_design_admin_panel'); + $this->out->menuItem(common_local_url('useradminpanel'), _('User'), + $menu_title, $action_name == 'useradminpanel', 'nav_user_admin_panel'); } if (AdminPanelAction::canAdmin('access')) { // TRANS: Menu item title/tooltip $menu_title = _('Access configuration'); // TRANS: Menu item for site administration - $this->out->menuItem(common_local_url('accessadminpanel'), _m('MENU', 'Access'), - $menu_title, $action_name == 'accessadminpanel', 'nav_design_admin_panel'); + $this->out->menuItem(common_local_url('accessadminpanel'), _('Access'), + $menu_title, $action_name == 'accessadminpanel', 'nav_access_admin_panel'); } if (AdminPanelAction::canAdmin('paths')) { // TRANS: Menu item title/tooltip $menu_title = _('Paths configuration'); // TRANS: Menu item for site administration - $this->out->menuItem(common_local_url('pathsadminpanel'), _m('MENU', 'Paths'), - $menu_title, $action_name == 'pathsadminpanel', 'nav_design_admin_panel'); + $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'), + $menu_title, $action_name == 'pathsadminpanel', 'nav_paths_admin_panel'); } if (AdminPanelAction::canAdmin('sessions')) { // TRANS: Menu item title/tooltip $menu_title = _('Sessions configuration'); // TRANS: Menu item for site administration - $this->out->menuItem(common_local_url('sessionsadminpanel'), _m('MENU', 'Sessions'), - $menu_title, $action_name == 'sessionsadminpanel', 'nav_design_admin_panel'); + $this->out->menuItem(common_local_url('sessionsadminpanel'), _('Sessions'), + $menu_title, $action_name == 'sessionsadminpanel', 'nav_sessions_admin_panel'); + } + + if (AdminPanelAction::canAdmin('sitenotice')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Edit site notice'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('sitenoticeadminpanel'), _('Site notice'), + $menu_title, $action_name == 'sitenoticeadminpanel', 'nav_sitenotice_admin_panel'); + } + + if (AdminPanelAction::canAdmin('snapshot')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Snapshots configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('snapshotadminpanel'), _('Snapshots'), + $menu_title, $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel'); } Event::handle('EndAdminPanelNav', array($this)); diff --git a/lib/atom10entry.php b/lib/atom10entry.php deleted file mode 100644 index f8f16d594..000000000 --- a/lib/atom10entry.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Class for building / manipulating an Atom entry in memory - * - * PHP version 5 - * - * LICENCE: This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * @category Feed - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class Atom10EntryException extends Exception -{ -} - -/** - * Class for manipulating an Atom entry in memory. Get the entry as an XML - * string with Atom10Entry::getString(). - * - * @category Feed - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ -class Atom10Entry extends XMLStringer -{ - private $namespaces; - private $categories; - private $content; - private $contributors; - private $id; - private $links; - private $published; - private $rights; - private $source; - private $summary; - private $title; - - function __construct($indent = true) { - parent::__construct($indent); - $this->namespaces = array(); - } - - function addNamespace($namespace, $uri) - { - $ns = array($namespace => $uri); - $this->namespaces = array_merge($this->namespaces, $ns); - } - - function initEntry() - { - - } - - function endEntry() - { - - } - - /** - * Check that all required elements have been set, etc. - * Throws an Atom10EntryException if something's missing. - * - * @return void - */ - function validate() - { - - } - - function getString() - { - $this->validate(); - - $this->initEntry(); - $this->renderEntries(); - $this->endEntry(); - - return $this->xw->outputMemory(); - } - -}
\ No newline at end of file diff --git a/lib/atom10feed.php b/lib/atom10feed.php index 8842840d5..2d342e785 100644 --- a/lib/atom10feed.php +++ b/lib/atom10feed.php @@ -49,6 +49,8 @@ class Atom10FeedException extends Exception class Atom10Feed extends XMLStringer { public $xw; + + // @fixme most of these should probably be read-only properties private $namespaces; private $authors; private $subject; @@ -57,10 +59,12 @@ class Atom10Feed extends XMLStringer private $generator; private $icon; private $links; - private $logo; + private $selfLink; + private $selfLinkType; + public $logo; private $rights; - private $subtitle; - private $title; + public $subtitle; + public $title; private $published; private $updated; private $entries; @@ -172,6 +176,14 @@ class Atom10Feed extends XMLStringer } $this->elementStart('feed', $commonAttrs); + $this->element( + 'generator', array( + 'url' => 'http://status.net', + 'version' => STATUSNET_VERSION + ), + 'StatusNet' + ); + $this->element('id', null, $this->id); $this->element('title', null, $this->title); $this->element('subtitle', null, $this->subtitle); @@ -184,6 +196,10 @@ class Atom10Feed extends XMLStringer $this->renderAuthors(); + if ($this->selfLink) { + $this->addLink($this->selfLink, array('rel' => 'self', + 'type' => $this->selfLinkType)); + } $this->renderLinks(); } @@ -253,6 +269,12 @@ class Atom10Feed extends XMLStringer $this->id = $id; } + function setSelfLink($url, $type='application/atom+xml') + { + $this->selfLink = $url; + $this->selfLinkType = $type; + } + function setTitle($title) { $this->title = $title; diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php index 52ee4c7d6..08c1c707c 100644 --- a/lib/atomgroupnoticefeed.php +++ b/lib/atomgroupnoticefeed.php @@ -49,14 +49,42 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed /** * Constructor * - * @param Group $group the group for the feed (optional) + * @param Group $group the group for the feed * @param boolean $indent flag to turn indenting on or off * * @return void */ - function __construct($group = null, $indent = true) { + function __construct($group, $indent = true) { parent::__construct($indent); $this->group = $group; + + $title = sprintf(_("%s timeline"), $group->nickname); + $this->setTitle($title); + + $sitename = common_config('site', 'name'); + $subtitle = sprintf( + _('Updates from %1$s on %2$s!'), + $group->nickname, + $sitename + ); + $this->setSubtitle($subtitle); + + $avatar = $group->homepage_logo; + $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE); + $this->setLogo($logo); + + $this->setUpdated('now'); + + $self = common_local_url('ApiTimelineGroup', + array('id' => $group->id, + 'format' => 'atom')); + $this->setId($self); + $this->setSelfLink($self); + + $this->addAuthorRaw($group->asAtomAuthor()); + $this->setActivitySubject($group->asActivitySubject()); + + $this->addLink($group->homeUrl()); } function getGroup() diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php index 3c3556cb9..e4df731fe 100644 --- a/lib/atomnoticefeed.php +++ b/lib/atomnoticefeed.php @@ -107,9 +107,19 @@ class AtomNoticeFeed extends Atom10Feed */ function addEntryFromNotice($notice) { - $this->addEntryRaw($notice->asAtomEntry()); - } + $source = $this->showSource(); + $author = $this->showAuthor(); -} + $this->addEntryRaw($notice->asAtomEntry(false, $source, $author)); + } + function showSource() + { + return true; + } + function showAuthor() + { + return true; + } +} diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php index 2ad8de455..428cc2de2 100644 --- a/lib/atomusernoticefeed.php +++ b/lib/atomusernoticefeed.php @@ -49,23 +49,71 @@ class AtomUserNoticeFeed extends AtomNoticeFeed /** * Constructor * - * @param User $user the user for the feed (optional) + * @param User $user the user for the feed * @param boolean $indent flag to turn indenting on or off * * @return void */ - function __construct($user = null, $indent = true) { + function __construct($user, $indent = true) { parent::__construct($indent); $this->user = $user; if (!empty($user)) { $profile = $user->getProfile(); $this->addAuthor($profile->nickname, $user->uri); + $this->setActivitySubject($profile->asActivityNoun('subject')); } + + $title = sprintf(_("%s timeline"), $user->nickname); + $this->setTitle($title); + + $sitename = common_config('site', 'name'); + $subtitle = sprintf( + _('Updates from %1$s on %2$s!'), + $user->nickname, $sitename + ); + $this->setSubtitle($subtitle); + + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE); + $this->setLogo($logo); + + $this->setUpdated('now'); + + $this->addLink( + common_local_url( + 'showstream', + array('nickname' => $user->nickname) + ) + ); + + $self = common_local_url('ApiTimelineUser', + array('id' => $user->id, + 'format' => 'atom')); + $this->setId($self); + $this->setSelfLink($self); + + $this->addLink( + common_local_url('sup', null, null, $user->id), + array( + 'rel' => 'http://api.friendfeed.com/2008/03#sup', + 'type' => 'application/json' + ) + ); } function getUser() { return $this->user; } + + function showSource() + { + return false; + } + + function showAuthor() + { + return false; + } } diff --git a/lib/default.php b/lib/default.php index 7b97c7dda..c46c3eef5 100644 --- a/lib/default.php +++ b/lib/default.php @@ -40,7 +40,8 @@ $default = 'logdebug' => false, 'fancy' => false, 'locale_path' => INSTALLDIR.'/locale', - 'language' => 'en_US', + 'language' => 'en', + 'langdetect' => true, 'languages' => get_all_languages(), 'email' => array_key_exists('SERVER_ADMIN', $_SERVER) ? $_SERVER['SERVER_ADMIN'] : null, @@ -53,10 +54,11 @@ $default = 'ssl' => 'never', 'sslserver' => null, 'shorturllength' => 30, - 'dupelimit' => 60, # default for same person saying the same thing + 'dupelimit' => 60, // default for same person saying the same thing 'textlimit' => 140, 'indent' => true, - 'use_x_sendfile' => false + 'use_x_sendfile' => false, + 'notice' => null // site wide notice text ), 'db' => array('database' => 'YOU HAVE TO SET THIS IN config.php', @@ -283,7 +285,7 @@ $default = 'OpenID' => null), ), 'admin' => - array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions')), + array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice')), 'singleuser' => array('enabled' => false, 'nickname' => null), diff --git a/lib/grantroleform.php b/lib/grantroleform.php new file mode 100644 index 000000000..b5f952746 --- /dev/null +++ b/lib/grantroleform.php @@ -0,0 +1,93 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for granting a role + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net>, Brion Vibber <brion@status.net> + * @copyright 2009-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Form for sandboxing a user + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see UnSandboxForm + */ + +class GrantRoleForm extends ProfileActionForm +{ + function __construct($role, $label, $writer, $profile, $r2args) + { + parent::__construct($writer, $profile, $r2args); + $this->role = $role; + $this->label = $label; + } + + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'grantrole'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return $this->label; + } + + function formData() + { + parent::formData(); + $this->out->hidden('role', $this->role); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return sprintf(_('Grant this user the "%s" role'), $this->label); + } +} diff --git a/lib/profileaction.php b/lib/profileaction.php index 2d4d23265..029c21845 100644 --- a/lib/profileaction.php +++ b/lib/profileaction.php @@ -105,28 +105,30 @@ class ProfileAction extends OwnerDesignAction $this->elementStart('div', array('id' => 'entity_subscriptions', 'class' => 'section')); + if (Event::handle('StartShowSubscriptionsMiniList', array($this))) { + $this->element('h2', null, _('Subscriptions')); - $this->element('h2', null, _('Subscriptions')); + $cnt = 0; - $cnt = 0; + if (!empty($profile)) { + $pml = new ProfileMiniList($profile, $this); + $cnt = $pml->show(); + if ($cnt == 0) { + $this->element('p', null, _('(None)')); + } + } - if (!empty($profile)) { - $pml = new ProfileMiniList($profile, $this); - $cnt = $pml->show(); - if ($cnt == 0) { - $this->element('p', null, _('(None)')); + if ($cnt > PROFILES_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('subscriptions', + array('nickname' => $this->profile->nickname)), + 'class' => 'more'), + _('All subscriptions')); + $this->elementEnd('p'); } - } - if ($cnt > PROFILES_PER_MINILIST) { - $this->elementStart('p'); - $this->element('a', array('href' => common_local_url('subscriptions', - array('nickname' => $this->profile->nickname)), - 'class' => 'more'), - _('All subscriptions')); - $this->elementEnd('p'); + Event::handle('EndShowSubscriptionsMiniList', array($this)); } - $this->elementEnd('div'); } @@ -226,27 +228,29 @@ class ProfileAction extends OwnerDesignAction $this->elementStart('div', array('id' => 'entity_groups', 'class' => 'section')); + if (Event::handle('StartShowGroupsMiniList', array($this))) { + $this->element('h2', null, _('Groups')); + + if ($groups) { + $gml = new GroupMiniList($groups, $this->user, $this); + $cnt = $gml->show(); + if ($cnt == 0) { + $this->element('p', null, _('(None)')); + } + } - $this->element('h2', null, _('Groups')); - - if ($groups) { - $gml = new GroupMiniList($groups, $this->user, $this); - $cnt = $gml->show(); - if ($cnt == 0) { - $this->element('p', null, _('(None)')); + if ($cnt > GROUPS_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('usergroups', + array('nickname' => $this->profile->nickname)), + 'class' => 'more'), + _('All groups')); + $this->elementEnd('p'); } - } - if ($cnt > GROUPS_PER_MINILIST) { - $this->elementStart('p'); - $this->element('a', array('href' => common_local_url('usergroups', - array('nickname' => $this->profile->nickname)), - 'class' => 'more'), - _('All groups')); - $this->elementEnd('p'); + Event::handle('EndShowGroupsMiniList', array($this)); } - - $this->elementEnd('div'); + $this->elementEnd('div'); } } diff --git a/lib/profilelist.php b/lib/profilelist.php index 4f1e84a6a..d970e605a 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -273,18 +273,12 @@ class ProfileListItem extends Widget $usf = new UnsubscribeForm($this->out, $this->profile); $usf->show(); } else { - $other = User::staticGet('id', $this->profile->id); - if (!empty($other)) { + // We can't initiate sub for a remote OMB profile. + $remote = Remote_profile::staticGet('id', $this->profile->id); + if (empty($remote)) { $sf = new SubscribeForm($this->out, $this->profile); $sf->show(); } - else { - $url = common_local_url('remotesubscribe', - array('nickname' => $this->profile->nickname)); - $this->out->element('a', array('href' => $url, - 'class' => 'entity_remote_subscribe'), - _('Subscribe')); - } } $this->out->elementEnd('li'); } diff --git a/lib/revokeroleform.php b/lib/revokeroleform.php new file mode 100644 index 000000000..ec24b9910 --- /dev/null +++ b/lib/revokeroleform.php @@ -0,0 +1,93 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Form for revoking a role + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net>, Brion Vibber <brion@status.net> + * @copyright 2009-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Form for sandboxing a user + * + * @category Form + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see UnSandboxForm + */ + +class RevokeRoleForm extends ProfileActionForm +{ + function __construct($role, $label, $writer, $profile, $r2args) + { + parent::__construct($writer, $profile, $r2args); + $this->role = $role; + $this->label = $label; + } + + /** + * Action this form provides + * + * @return string Name of the action, lowercased. + */ + + function target() + { + return 'revokerole'; + } + + /** + * Title of the form + * + * @return string Title of the form, internationalized + */ + + function title() + { + return $this->label; + } + + function formData() + { + parent::formData(); + $this->out->hidden('role', $this->role); + } + + /** + * Description of the form + * + * @return string description of the form, internationalized + */ + + function description() + { + return sprintf(_('Revoke the "%s" role from this user'), $this->label); + } +} diff --git a/lib/right.php b/lib/right.php index 4e9c5a918..deb451fde 100644 --- a/lib/right.php +++ b/lib/right.php @@ -58,5 +58,7 @@ class Right const EMAILONSUBSCRIBE = 'emailonsubscribe'; const EMAILONFAVE = 'emailonfave'; const MAKEGROUPADMIN = 'makegroupadmin'; + const GRANTROLE = 'grantrole'; + const REVOKEROLE = 'revokerole'; } diff --git a/lib/router.php b/lib/router.php index abbce041d..706120e0b 100644 --- a/lib/router.php +++ b/lib/router.php @@ -98,6 +98,7 @@ class Router 'groupblock', 'groupunblock', 'sandbox', 'unsandbox', 'silence', 'unsilence', + 'grantrole', 'revokerole', 'repeat', 'deleteuser', 'geocode', @@ -649,6 +650,8 @@ class Router $m->connect('admin/access', array('action' => 'accessadminpanel')); $m->connect('admin/paths', array('action' => 'pathsadminpanel')); $m->connect('admin/sessions', array('action' => 'sessionsadminpanel')); + $m->connect('admin/sitenotice', array('action' => 'sitenoticeadminpanel')); + $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel')); $m->connect('getfile/:filename', array('action' => 'getfile'), diff --git a/lib/statusnet.php b/lib/statusnet.php index 7c4df84b4..eba9ab9b8 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -354,10 +354,10 @@ class StatusNet class NoConfigException extends Exception { - public $config_files; + public $configFiles; - function __construct($msg, $config_files) { + function __construct($msg, $configFiles) { parent::__construct($msg); - $this->config_files = $config_files; + $this->configFiles = $configFiles; } } diff --git a/lib/userprofile.php b/lib/userprofile.php index 43dfd05be..8464c2446 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -346,6 +346,16 @@ class UserProfile extends Widget $this->out->elementEnd('ul'); $this->out->elementEnd('li'); } + + if ($cur->hasRight(Right::GRANTROLE)) { + $this->out->elementStart('li', 'entity_role'); + $this->out->element('p', null, _('User role')); + $this->out->elementStart('ul'); + $this->roleButton('administrator', _m('role', 'Administrator')); + $this->roleButton('moderator', _m('role', 'Moderator')); + $this->out->elementEnd('ul'); + $this->out->elementEnd('li'); + } } } @@ -359,6 +369,22 @@ class UserProfile extends Widget } } + function roleButton($role, $label) + { + list($action, $r2args) = $this->out->returnToArgs(); + $r2args['action'] = $action; + + $this->out->elementStart('li', "entity_role_$role"); + if ($this->user->hasRole($role)) { + $rf = new RevokeRoleForm($role, $label, $this->out, $this->profile, $r2args); + $rf->show(); + } else { + $rf = new GrantRoleForm($role, $label, $this->out, $this->profile, $r2args); + $rf->show(); + } + $this->out->elementEnd('li'); + } + function showRemoteSubscribeLink() { $url = common_local_url('remotesubscribe', diff --git a/lib/util.php b/lib/util.php index add1b0ae6..da2799d4f 100644 --- a/lib/util.php +++ b/lib/util.php @@ -105,11 +105,13 @@ function common_language() // Otherwise, find the best match for the languages requested by the // user's browser... - $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; - if (!empty($httplang)) { - $language = client_prefered_language($httplang); - if ($language) - return $language; + if (common_config('site', 'langdetect')) { + $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; + if (!empty($httplang)) { + $language = client_prefered_language($httplang); + if ($language) + return $language; + } } // Finally, if none of the above worked, use the site's default... @@ -853,7 +855,7 @@ function common_valid_profile_tag($str) function common_group_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); - $group = User_group::getForNickname($nickname); + $group = User_group::getForNickname($nickname, $sender); if ($sender && $group && $sender->isMember($group)) { $attrs = array('href' => $group->permalink(), 'class' => 'url'); diff --git a/plugins/Facebook/FacebookPlugin.php b/plugins/Facebook/FacebookPlugin.php index 443cb396f..5dba73a5d 100644 --- a/plugins/Facebook/FacebookPlugin.php +++ b/plugins/Facebook/FacebookPlugin.php @@ -80,6 +80,25 @@ class FacebookPlugin extends Plugin } /** + * Check to see if there is an API key and secret defined + * for Facebook integration. + * + * @return boolean result + */ + + static function hasKeys() + { + $apiKey = common_config('facebook', 'apikey'); + $apiSecret = common_config('facebook', 'secret'); + + if (!empty($apiKey) && !empty($apiSecret)) { + return true; + } + + return false; + } + + /** * Add Facebook app actions to the router table * * Hook for RouterInitialized event. @@ -91,23 +110,26 @@ class FacebookPlugin extends Plugin function onStartInitializeRouter($m) { + $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); - // Facebook App stuff + if (self::hasKeys()) { - $m->connect('facebook/app', array('action' => 'facebookhome')); - $m->connect('facebook/app/index.php', array('action' => 'facebookhome')); - $m->connect('facebook/app/settings.php', - array('action' => 'facebooksettings')); - $m->connect('facebook/app/invite.php', array('action' => 'facebookinvite')); - $m->connect('facebook/app/remove', array('action' => 'facebookremove')); - $m->connect('admin/facebook', array('action' => 'facebookadminpanel')); + // Facebook App stuff - // Facebook Connect stuff + $m->connect('facebook/app', array('action' => 'facebookhome')); + $m->connect('facebook/app/index.php', array('action' => 'facebookhome')); + $m->connect('facebook/app/settings.php', + array('action' => 'facebooksettings')); + $m->connect('facebook/app/invite.php', array('action' => 'facebookinvite')); + $m->connect('facebook/app/remove', array('action' => 'facebookremove')); - $m->connect('main/facebookconnect', array('action' => 'FBConnectAuth')); - $m->connect('main/facebooklogin', array('action' => 'FBConnectLogin')); - $m->connect('settings/facebook', array('action' => 'FBConnectSettings')); - $m->connect('xd_receiver.html', array('action' => 'FBC_XDReceiver')); + // Facebook Connect stuff + + $m->connect('main/facebookconnect', array('action' => 'FBConnectAuth')); + $m->connect('main/facebooklogin', array('action' => 'FBConnectLogin')); + $m->connect('settings/facebook', array('action' => 'FBConnectSettings')); + $m->connect('xd_receiver.html', array('action' => 'FBC_XDReceiver')); + } return true; } @@ -338,6 +360,9 @@ class FacebookPlugin extends Plugin function reqFbScripts($action) { + if (!self::hasKeys()) { + return false; + } // If you're logged in w/FB Connect, you always need the FB stuff @@ -410,42 +435,35 @@ class FacebookPlugin extends Plugin function onStartPrimaryNav($action) { - $user = common_current_user(); + if (self::hasKeys()) { + $user = common_current_user(); + if (!empty($user)) { - $connect = 'FBConnectSettings'; - if (common_config('xmpp', 'enabled')) { - $connect = 'imsettings'; - } else if (common_config('sms', 'enabled')) { - $connect = 'smssettings'; - } + $fbuid = $this->loggedIn(); - if (!empty($user)) { + if (!empty($fbuid)) { - $fbuid = $this->loggedIn(); + /* Default FB silhouette pic for FB users who haven't + uploaded a profile pic yet. */ - if (!empty($fbuid)) { - - /* Default FB silhouette pic for FB users who haven't - uploaded a profile pic yet. */ - - $silhouetteUrl = - 'http://static.ak.fbcdn.net/pics/q_silhouette.gif'; + $silhouetteUrl = + 'http://static.ak.fbcdn.net/pics/q_silhouette.gif'; - $url = $this->getProfilePicURL($fbuid); + $url = $this->getProfilePicURL($fbuid); - $action->elementStart('li', array('id' => 'nav_fb')); + $action->elementStart('li', array('id' => 'nav_fb')); - $action->element('img', array('id' => 'fbc_profile-pic', - 'src' => (!empty($url)) ? $url : $silhouetteUrl, - 'alt' => 'Facebook Connect User', - 'width' => '16'), ''); + $action->element('img', array('id' => 'fbc_profile-pic', + 'src' => (!empty($url)) ? $url : $silhouetteUrl, + 'alt' => 'Facebook Connect User', + 'width' => '16'), ''); - $iconurl = common_path('plugins/Facebook/fbfavicon.ico'); - $action->element('img', array('id' => 'fb_favicon', - 'src' => $iconurl)); - - $action->elementEnd('li'); + $iconurl = common_path('plugins/Facebook/fbfavicon.ico'); + $action->element('img', array('id' => 'fb_favicon', + 'src' => $iconurl)); + $action->elementEnd('li'); + } } } @@ -462,14 +480,15 @@ class FacebookPlugin extends Plugin function onEndLoginGroupNav(&$action) { + if (self::hasKeys()) { - $action_name = $action->trimmed('action'); - - $action->menuItem(common_local_url('FBConnectLogin'), - _m('Facebook'), - _m('Login or register using Facebook'), - 'FBConnectLogin' === $action_name); + $action_name = $action->trimmed('action'); + $action->menuItem(common_local_url('FBConnectLogin'), + _m('Facebook'), + _m('Login or register using Facebook'), + 'FBConnectLogin' === $action_name); + } return true; } @@ -483,13 +502,15 @@ class FacebookPlugin extends Plugin function onEndConnectSettingsNav(&$action) { - $action_name = $action->trimmed('action'); + if (self::hasKeys()) { - $action->menuItem(common_local_url('FBConnectSettings'), - _m('Facebook'), - _m('Facebook Connect Settings'), - $action_name === 'FBConnectSettings'); + $action_name = $action->trimmed('action'); + $action->menuItem(common_local_url('FBConnectSettings'), + _m('Facebook'), + _m('Facebook Connect Settings'), + $action_name === 'FBConnectSettings'); + } return true; } @@ -503,20 +524,22 @@ class FacebookPlugin extends Plugin function onStartLogout($action) { - $action->logout(); - $fbuid = $this->loggedIn(); + if (self::hasKeys()) { - if (!empty($fbuid)) { - try { - $facebook = getFacebook(); - $facebook->expire_session(); - } catch (Exception $e) { - common_log(LOG_WARNING, 'Facebook Connect Plugin - ' . - 'Could\'t logout of Facebook: ' . - $e->getMessage()); + $action->logout(); + $fbuid = $this->loggedIn(); + + if (!empty($fbuid)) { + try { + $facebook = getFacebook(); + $facebook->expire_session(); + } catch (Exception $e) { + common_log(LOG_WARNING, 'Facebook Connect Plugin - ' . + 'Could\'t logout of Facebook: ' . + $e->getMessage()); + } } } - return true; } @@ -562,7 +585,9 @@ class FacebookPlugin extends Plugin function onStartEnqueueNotice($notice, &$transports) { - array_push($transports, 'facebook'); + if (self::hasKeys()) { + array_push($transports, 'facebook'); + } return true; } @@ -575,7 +600,9 @@ class FacebookPlugin extends Plugin */ function onEndInitializeQueueManager($manager) { - $manager->connect('facebook', 'FacebookQueueHandler'); + if (self::hasKeys()) { + $manager->connect('facebook', 'FacebookQueueHandler'); + } return true; } diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index f788639ae..0b37734b7 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -307,23 +307,14 @@ class MobileProfilePlugin extends WAP20Plugin function _showPrimaryNav($action) { $user = common_current_user(); - $connect = ''; - if (common_config('xmpp', 'enabled')) { - $connect = 'imsettings'; - } else if (common_config('sms', 'enabled')) { - $connect = 'smssettings'; - } - $action->elementStart('ul', array('id' => 'site_nav_global_primary')); if ($user) { $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)), _('Home')); $action->menuItem(common_local_url('profilesettings'), _('Account')); - if ($connect) { - $action->menuItem(common_local_url($connect), + $action->menuItem(common_local_url('oauthconnectionssettings'), _('Connect')); - } if ($user->hasRight(Right::CONFIGURESITE)) { $action->menuItem(common_local_url('siteadminpanel'), _('Admin'), _('Change site configuration'), false, 'nav_admin'); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index b69430ea7..033325c9e 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -44,15 +44,19 @@ class OStatusPlugin extends Plugin $m->connect('.well-known/host-meta', array('action' => 'hostmeta')); $m->connect('main/xrd', - array('action' => 'xrd')); + array('action' => 'userxrd')); + $m->connect('main/ownerxrd', + array('action' => 'ownerxrd')); $m->connect('main/ostatus', array('action' => 'ostatusinit')); $m->connect('main/ostatus?nickname=:nickname', array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+')); + $m->connect('main/ostatus?group=:group', + array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+')); $m->connect('main/ostatussub', array('action' => 'ostatussub')); - $m->connect('main/ostatussub', - array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+')); + $m->connect('main/ostatusgroup', + array('action' => 'ostatusgroup')); // PuSH actions $m->connect('main/push/hub', array('action' => 'pushhub')); @@ -109,13 +113,13 @@ class OStatusPlugin extends Plugin { if ($action instanceof ShowstreamAction) { $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); - $url = common_local_url('xrd'); + $url = common_local_url('userxrd'); $url.= '?uri='. $acct; - + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); } } - + /** * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. @@ -210,6 +214,22 @@ class OStatusPlugin extends Plugin return false; } + function onStartGroupSubscribe($output, $group) + { + $cur = common_current_user(); + + if (empty($cur)) { + // Add an OStatus subscribe + $url = common_local_url('ostatusinit', + array('group' => $group->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Join')); + } + + return true; + } + /** * Check if we've got remote replies to send via Salmon. * @@ -233,70 +253,70 @@ class OStatusPlugin extends Plugin function onEndFindMentions($sender, $text, &$mentions) { - preg_match_all('!(?:^|\s+) - @( # Webfinger: - (?:\w+\.)*\w+ # user - @ # @ - (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain - | # Profile: - (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain - (?:/\w+)+ # /path1(/path2...) - )!x', + $matches = array(); + + // Webfinger matches: @user@example.com + if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!', $text, $wmatches, - PREG_OFFSET_CAPTURE); - - foreach ($wmatches[1] as $wmatch) { - $target = $wmatch[0]; - $oprofile = null; - - if (strpos($target, '/') === false) { - $this->log(LOG_INFO, "Checking Webfinger for address '$target'"); + PREG_OFFSET_CAPTURE)) { + foreach ($wmatches[1] as $wmatch) { + list($target, $pos) = $wmatch; + $this->log(LOG_INFO, "Checking webfinger '$target'"); try { $oprofile = Ostatus_profile::ensureWebfinger($target); + if ($oprofile && !$oprofile->isGroup()) { + $profile = $oprofile->localProfile(); + $matches[$pos] = array('mentioned' => array($profile), + 'text' => $target, + 'position' => $pos, + 'url' => $profile->profileurl); + } } catch (Exception $e) { $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); } - } else { - $schemes = array('https', 'http'); + } + } + + // Profile matches: @example.com/mublog/user + if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)!', + $text, + $wmatches, + PREG_OFFSET_CAPTURE)) { + foreach ($wmatches[1] as $wmatch) { + list($target, $pos) = $wmatch; + $schemes = array('http', 'https'); foreach ($schemes as $scheme) { $url = "$scheme://$target"; $this->log(LOG_INFO, "Checking profile address '$url'"); try { $oprofile = Ostatus_profile::ensureProfile($url); - if ($oprofile) { - continue; + if ($oprofile && !$oprofile->isGroup()) { + $profile = $oprofile->localProfile(); + $matches[$pos] = array('mentioned' => array($profile), + 'text' => $target, + 'position' => $pos, + 'url' => $profile->profileurl); + break; } } catch (Exception $e) { $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); } } } + } - if (empty($oprofile)) { - $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'"); - } else { - - $this->log(LOG_INFO, "Ostatus_profile found for address '$target'"); - - if ($oprofile->isGroup()) { - continue; - } - $profile = $oprofile->localProfile(); - - $pos = $wmatch[1]; - foreach ($mentions as $i => $other) { - // If we share a common prefix with a local user, override it! - if ($other['position'] == $pos) { - unset($mentions[$i]); - } - } - $mentions[] = array('mentioned' => array($profile), - 'text' => $target, - 'position' => $pos, - 'url' => $profile->profileurl); + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + $pos = $other['position']; + if (isset($matches[$pos])) { + $mentions[$i] = $matches[$pos]; + unset($matches[$pos]); } } + foreach ($matches as $mention) { + $mentions[] = $mention; + } return true; } @@ -567,7 +587,6 @@ class OStatusPlugin extends Plugin // Drop the PuSH subscription if there are no other subscribers. $oprofile->garbageCollect(); - $member = Profile::staticGet($user->id); $act = new Activity(); @@ -711,23 +730,37 @@ class OStatusPlugin extends Plugin return true; } - function onStartShowAllContent($action) + function onStartShowUserGroupsContent($action) + { + $this->showEntityRemoteSubscribe($action, 'ostatusgroup'); + + return true; + } + + function onEndShowSubscriptionsMiniList($action) { $this->showEntityRemoteSubscribe($action); return true; } - function showEntityRemoteSubscribe($action) + function onEndShowGroupsMiniList($action) + { + $this->showEntityRemoteSubscribe($action, 'ostatusgroup'); + + return true; + } + + function showEntityRemoteSubscribe($action, $target='ostatussub') { $user = common_current_user(); if ($user && ($user->id == $action->profile->id)) { $action->elementStart('div', 'entity_actions'); $action->elementStart('p', array('id' => 'entity_remote_subscribe', 'class' => 'entity_subscribe')); - $action->element('a', array('href' => common_local_url('ostatussub'), + $action->element('a', array('href' => common_local_url($target), 'class' => 'entity_remote_subscribe') - , _m('Subscribe to remote user')); + , _m('Remote')); $action->elementEnd('p'); $action->elementEnd('div'); } @@ -779,4 +812,28 @@ class OStatusPlugin extends Plugin return true; } + + function onStartProfileListItemActionElements($item) + { + if (!common_logged_in()) { + + $profileUser = User::staticGet('id', $item->profile->id); + + if (!empty($profileUser)) { + + $output = $item->out; + + // Add an OStatus subscribe + $output->elementStart('li', 'entity_subscribe'); + $url = common_local_url('ostatusinit', + array('nickname' => $profileUser->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Subscribe')); + $output->elementEnd('li'); + } + } + + return true; + } } diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 3d00b98ae..6d35ada6c 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -32,7 +32,7 @@ class HostMetaAction extends Action parent::handle(); $domain = common_config('site', 'server'); - $url = common_local_url('xrd'); + $url = common_local_url('userxrd'); $url.= '?uri={uri}'; $xrd = new XRD(); diff --git a/plugins/OStatus/actions/ostatusgroup.php b/plugins/OStatus/actions/ostatusgroup.php new file mode 100644 index 000000000..f325ba053 --- /dev/null +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -0,0 +1,181 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009-2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber <brion@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +/** + * Key UI methods: + * + * showInputForm() - form asking for a remote profile account or URL + * We end up back here on errors + * + * showPreviewForm() - surrounding form for preview-and-confirm + * preview() - display profile for a remote group + * + * success() - redirects to groups page on join + */ +class OStatusGroupAction extends OStatusSubAction +{ + protected $profile_uri; // provided acct: or URI of remote entity + protected $oprofile; // Ostatus_profile of remote entity, if valid + + + function validateRemoteProfile() + { + if (!$this->oprofile->isGroup()) { + // Send us to the user subscription form for conf + $target = common_local_url('ostatussub', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); + } + } + + /** + * Show the initial form, when we haven't yet been given a valid + * remote profile. + */ + function showInputForm() + { + $user = common_current_user(); + + $profile = $user->getProfile(); + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_ostatus_sub', + 'class' => 'form_settings', + 'action' => $this->selfLink())); + + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => 'settings_feeds')); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->input('profile', + _m('Join group'), + $this->profile_uri, + _m("OStatus group's address, like http://example.net/group/nickname")); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->submit('validate', _m('Continue')); + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + + /** + * Show a preview for a remote group's profile + * @return boolean true if we're ok to try joining + */ + function preview() + { + $oprofile = $this->oprofile; + $group = $oprofile->localGroup(); + + $cur = common_current_user(); + if ($cur->isMember($group)) { + $this->element('div', array('class' => 'error'), + _m("You are already a member of this group.")); + $ok = false; + } else { + $ok = true; + } + + $this->showEntity($group, + $group->getProfileUrl(), + $group->homepage_logo, + $group->description); + return $ok; + } + + /** + * Redirect on successful remote group join + */ + function success() + { + $cur = common_current_user(); + $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); + common_redirect($url, 303); + } + + /** + * Attempt to finalize subscription. + * validateFeed must have been run first. + * + * Calls showForm on failure or success on success. + */ + function saveFeed() + { + $user = common_current_user(); + $group = $this->oprofile->localGroup(); + if ($user->isMember($group)) { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Already a member!')); + return; + } + + if (Event::handle('StartJoinGroup', array($group, $user))) { + $ok = Group_member::join($this->oprofile->group_id, $user->id); + if ($ok) { + Event::handle('EndJoinGroup', array($group, $user)); + $this->success(); + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join failed!')); + } + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join aborted!')); + } + } + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + // TRANS: Page title for OStatus remote group join form + return _m('Confirm joining remote group'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to groups from other supported sites. Paste the group\'s profile URI below:'); + } + + function selfLink() + { + return common_local_url('ostatusgroup'); + } +} diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index 8ba8dcdcc..1e45025b0 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -29,6 +29,7 @@ class OStatusInitAction extends Action { var $nickname; + var $group; var $profile; var $err; @@ -41,8 +42,9 @@ class OStatusInitAction extends Action return false; } - // Local user the remote wants to subscribe to + // Local user or group the remote wants to subscribe to $this->nickname = $this->trimmed('nickname'); + $this->group = $this->trimmed('group'); // Webfinger or profile URL of the remote user $this->profile = $this->trimmed('profile'); @@ -89,25 +91,33 @@ class OStatusInitAction extends Action function showContent() { + if ($this->group) { + $header = sprintf(_m('Join group %s'), $this->group); + $submit = _m('Join'); + } else { + $header = sprintf(_m('Subscribe to %s'), $this->nickname); + $submit = _m('Subscribe'); + } $this->elementStart('form', array('id' => 'form_ostatus_connect', 'method' => 'post', 'class' => 'form_settings', 'action' => common_local_url('ostatusinit'))); $this->elementStart('fieldset'); - $this->element('legend', null, sprintf(_m('Subscribe to %s'), $this->nickname)); + $this->element('legend', null, $header); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li', array('id' => 'ostatus_nickname')); $this->input('nickname', _m('User nickname'), $this->nickname, _m('Nickname of the user you want to follow')); + $this->hidden('group', $this->group); // pass-through for magic links $this->elementEnd('li'); $this->elementStart('li', array('id' => 'ostatus_profile')); $this->input('profile', _m('Profile Account'), $this->profile, _m('Your account id (i.e. user@identi.ca)')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('submit', _m('Subscribe')); + $this->submit('submit', $submit); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -131,19 +141,17 @@ class OStatusInitAction extends Action function connectWebfinger($acct) { - $disco = new Discovery; + $target_profile = $this->targetProfile(); + $disco = new Discovery; $result = $disco->lookup($acct); if (!$result) { $this->clientError(_m("Couldn't look up OStatus account profile.")); } + foreach ($result->links as $link) { if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { // We found a URL - let's redirect! - - $user = User::staticGet('nickname', $this->nickname); - $target_profile = common_local_url('userbyid', array('id' => $user->id)); - $url = Discovery::applyTemplate($link['template'], $target_profile); common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); common_redirect($url, 303); @@ -155,8 +163,7 @@ class OStatusInitAction extends Action function connectProfile($subscriber_profile) { - $user = User::staticGet('nickname', $this->nickname); - $target_profile = common_local_url('userbyid', array('id' => $user->id)); + $target_profile = $this->targetProfile(); // @fixme hack hack! We should look up the remote sub URL from XRDS $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile); @@ -166,6 +173,30 @@ class OStatusInitAction extends Action common_redirect($suburl, 303); } + /** + * Build the canonical profile URI+URL of the requested user or group + */ + function targetProfile() + { + if ($this->nickname) { + $user = User::staticGet('nickname', $this->nickname); + if ($user) { + return common_local_url('userbyid', array('id' => $user->id)); + } else { + $this->clientError("No such user."); + } + } else if ($this->group) { + $group = Local_group::staticGet('id', $this->group); + if ($group) { + return common_local_url('groupbyid', array('id' => $group->group_id)); + } else { + $this->clientError("No such group."); + } + } else { + $this->clientError("No local user or group nickname provided."); + } + } + function title() { return _m('OStatus Connect'); diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index e318701a2..65dee2392 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * Copyright (C) 2009-2010, StatusNet, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,11 +31,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } * We end up back here on errors * * showPreviewForm() - surrounding form for preview-and-confirm - * previewUser() - display profile for a remote user - * previewGroup() - display profile for a remote group + * preview() - display profile for a remote user * - * successUser() - redirects to subscriptions page on subscribe - * successGroup() - redirects to groups page on join + * success() - redirects to subscriptions page on subscribe */ class OStatusSubAction extends Action { @@ -55,8 +53,7 @@ class OStatusSubAction extends Action $this->elementStart('form', array('method' => 'post', 'id' => 'form_ostatus_sub', 'class' => 'form_settings', - 'action' => - common_local_url('ostatussub'))); + 'action' => $this->selfLink())); $this->hidden('token', common_session_token()); @@ -65,9 +62,9 @@ class OStatusSubAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li'); $this->input('profile', - _m('Address or profile URL'), + _m('Subscribe to'), $this->profile_uri, - _m('Enter the profile URL of a PubSubHubbub-enabled feed')); + _m("OStatus user's address, like nickname@example.com or http://example.net/nickname")); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -87,11 +84,7 @@ class OStatusSubAction extends Action */ function showPreviewForm() { - if ($this->oprofile->isGroup()) { - $ok = $this->previewGroup(); - } else { - $ok = $this->previewUser(); - } + $ok = $this->preview(); if (!$ok) { // @fixme maybe provide a cancel button or link back? return; @@ -104,7 +97,7 @@ class OStatusSubAction extends Action 'id' => 'form_ostatus_sub', 'class' => 'form_remote_authorize', 'action' => - common_local_url('ostatussub'))); + $this->selfLink())); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); $this->hidden('profile', $this->profile_uri); @@ -126,7 +119,7 @@ class OStatusSubAction extends Action * Show a preview for a remote user's profile * @return boolean true if we're ok to try subscribing */ - function previewUser() + function preview() { $oprofile = $this->oprofile; $profile = $oprofile->localProfile(); @@ -150,32 +143,6 @@ class OStatusSubAction extends Action return $ok; } - /** - * Show a preview for a remote group's profile - * @return boolean true if we're ok to try joining - */ - function previewGroup() - { - $oprofile = $this->oprofile; - $group = $oprofile->localGroup(); - - $cur = common_current_user(); - if ($cur->isMember($group)) { - $this->element('div', array('class' => 'error'), - _m("You are already a member of this group.")); - $ok = false; - } else { - $ok = true; - } - - $this->showEntity($group, - $group->getProfileUrl(), - $group->homepage_logo, - $group->description); - return $ok; - } - - function showEntity($entity, $profile, $avatar, $note) { $nickname = $entity->nickname; @@ -254,7 +221,7 @@ class OStatusSubAction extends Action /** * Redirect on successful remote user subscription */ - function successUser() + function success() { $cur = common_current_user(); $url = common_local_url('subscriptions', array('nickname' => $cur->nickname)); @@ -262,104 +229,81 @@ class OStatusSubAction extends Action } /** - * Redirect on successful remote group join - */ - function successGroup() - { - $cur = common_current_user(); - $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); - common_redirect($url, 303); - } - - /** * Pull data for a remote profile and check if it's valid. * Fills out error UI string in $this->error * Fills out $this->oprofile on success. * * @return boolean */ - function validateFeed() + function pullRemoteProfile() { - $profile_uri = trim($this->arg('profile')); - - if ($profile_uri == '') { - $this->showForm(_m('Empty remote profile URL!')); - return; - } - $this->profile_uri = $profile_uri; - + $this->profile_uri = $this->trimmed('profile'); try { if (Validate::email($this->profile_uri)) { $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri); } else if (Validate::uri($this->profile_uri)) { $this->oprofile = Ostatus_profile::ensureProfile($this->profile_uri); } else { - $this->error = _m("Invalid address format."); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug('Invalid address format.', __FILE__); return false; } return true; } catch (FeedSubBadURLException $e) { - $this->error = _m('Invalid URL or could not reach server.'); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug('Invalid URL or could not reach server.', __FILE__); } catch (FeedSubBadResponseException $e) { - $this->error = _m('Cannot read feed; server returned error.'); + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Cannot read feed; server returned error.', __FILE__); } catch (FeedSubEmptyException $e) { - $this->error = _m('Cannot read feed; server returned an empty page.'); + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Cannot read feed; server returned an empty page.', __FILE__); } catch (FeedSubBadHTMLException $e) { - $this->error = _m('Bad HTML, could not find feed link.'); + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Bad HTML, could not find feed link.', __FILE__); } catch (FeedSubNoFeedException $e) { - $this->error = _m('Could not find a feed linked from this URL.'); + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Could not find a feed linked from this URL.', __FILE__); } catch (FeedSubUnrecognizedTypeException $e) { - $this->error = _m('Not a recognized feed type.'); - } catch (FeedSubException $e) { + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Not a recognized feed type.', __FILE__); + } catch (Exception $e) { // Any new ones we forgot about - $this->error = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage()); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug(sprintf('Bad feed URL: %s %s', get_class($e), $e->getMessage()), __FILE__); } return false; } + function validateRemoteProfile() + { + if ($this->oprofile->isGroup()) { + // Send us to the group subscription form for conf + $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); + } + } + /** * Attempt to finalize subscription. * validateFeed must have been run first. * - * Calls showForm on failure or successUser/successGroup on success. + * Calls showForm on failure or success on success. */ function saveFeed() { // And subscribe the current user to the local profile $user = common_current_user(); - - if ($this->oprofile->isGroup()) { - $group = $this->oprofile->localGroup(); - if ($user->isMember($group)) { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Already a member!')); - return; - } - if (Event::handle('StartJoinGroup', array($group, $user))) { - $ok = Group_member::join($this->oprofile->group_id, $user->id); - if ($ok) { - Event::handle('EndJoinGroup', array($group, $user)); - $this->successGroup(); - } else { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Remote group join failed!')); - } - } else { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Remote group join aborted!')); - } + $local = $this->oprofile->localProfile(); + if ($user->isSubscribed($local)) { + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Already subscribed!')); + } elseif ($this->oprofile->subscribeLocalToRemote($user)) { + $this->success(); } else { - $local = $this->oprofile->localProfile(); - if ($user->isSubscribed($local)) { - // TRANS: OStatus remote subscription dialog error. - $this->showForm(_m('Already subscribed!')); - } elseif ($this->oprofile->subscribeLocalToRemote($user)) { - $this->successUser(); - } else { - // TRANS: OStatus remote subscription dialog error. - $this->showForm(_m('Remote subscription failed!')); - } + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Remote subscription failed!')); } } @@ -376,8 +320,9 @@ class OStatusSubAction extends Action return false; } - $this->profile_uri = $this->arg('profile'); - + if ($this->pullRemoteProfile()) { + $this->validateRemoteProfile(); + } return true; } @@ -390,9 +335,6 @@ class OStatusSubAction extends Action if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); } else { - if ($this->arg('profile')) { - $this->validateFeed(); - } $this->showForm(); } } @@ -414,7 +356,7 @@ class OStatusSubAction extends Action return; } - if ($this->validateFeed()) { + if ($this->oprofile) { if ($this->arg('submit')) { $this->saveFeed(); return; @@ -456,7 +398,7 @@ class OStatusSubAction extends Action function title() { // TRANS: Page title for OStatus remote subscription form - return _m('Authorize subscription'); + return _m('Confirm'); } /** @@ -500,4 +442,9 @@ class OStatusSubAction extends Action parent::showScripts(); $this->autofocus('feedurl'); } + + function selfLink() + { + return common_local_url('ostatussub'); + } } diff --git a/plugins/OStatus/actions/ownerxrd.php b/plugins/OStatus/actions/ownerxrd.php new file mode 100644 index 000000000..9c141d8c7 --- /dev/null +++ b/plugins/OStatus/actions/ownerxrd.php @@ -0,0 +1,56 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class OwnerxrdAction extends XrdAction +{ + + public $uri; + + function prepare($args) + { + $this->user = User::siteOwner(); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $nick = common_canonical_nickname($this->user->nickname); + $acct = 'acct:' . $nick . '@' . common_config('site', 'server'); + + $this->xrd = new XRD(); + + // Check to see if a $config['webfinger']['owner'] has been set + if ($owner = common_config('webfinger', 'owner')) { + $this->xrd->subject = Discovery::normalize($owner); + $this->xrd->alias[] = $acct; + } else { + $this->xrd->subject = $acct; + } + + return true; + } +} diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php new file mode 100644 index 000000000..414de9364 --- /dev/null +++ b/plugins/OStatus/actions/userxrd.php @@ -0,0 +1,48 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker <james@status.net> + */ + +if (!defined('STATUSNET')) { exit(1); } + +class UserxrdAction extends XrdAction +{ + + function prepare($args) + { + parent::prepare($args); + + $this->uri = $this->trimmed('uri'); + $acct = Discovery::normalize($this->uri); + + list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); + $nick = common_canonical_nickname($nick); + + $this->user = User::staticGet('nickname', $nick); + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + return true; + } +} diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index a33e95d93..fcca1a252 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -550,7 +550,8 @@ class Ostatus_profile extends Memcached_DataObject 'rendered' => $rendered, 'replies' => array(), 'groups' => array(), - 'tags' => array()); + 'tags' => array(), + 'urls' => array()); // Check for optional attributes... @@ -595,6 +596,12 @@ class Ostatus_profile extends Memcached_DataObject } } + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + try { $saved = Notice::saveNew($oprofile->profile_id, $content, @@ -620,7 +627,8 @@ class Ostatus_profile extends Memcached_DataObject protected function purify($html) { require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; - $config = array('safe' => 1); + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); return htmLawed($html, $config); } @@ -1259,6 +1267,11 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * @param string $addr webfinger address + * @return Ostatus_profile + * @throws Exception on error conditions + */ public static function ensureWebfinger($addr) { // First, try the cache @@ -1267,7 +1280,8 @@ class Ostatus_profile extends Memcached_DataObject if ($uri !== false) { if (is_null($uri)) { - return null; + // Negative cache entry + throw new Exception('Not a valid webfinger address.'); } $oprofile = Ostatus_profile::staticGet('uri', $uri); if (!empty($oprofile)) { @@ -1291,20 +1305,24 @@ class Ostatus_profile extends Memcached_DataObject try { $result = $disco->lookup($addr); } catch (Exception $e) { + // Save negative cache entry so we don't waste time looking it up again. + // @fixme distinguish temporary failures? self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null); - return null; + throw new Exception('Not a valid webfinger address.'); } + $hints = array('webfinger' => $addr); + foreach ($result->links as $link) { switch ($link['rel']) { case Discovery::PROFILEPAGE: - $profileUrl = $link['href']; + $hints['profileurl'] = $profileUrl = $link['href']; break; case Salmon::NS_REPLIES: - $salmonEndpoint = $link['href']; + $hints['salmon'] = $salmonEndpoint = $link['href']; break; case Discovery::UPDATESFROM: - $feedUrl = $link['href']; + $hints['feedurl'] = $feedUrl = $link['href']; break; case Discovery::HCARD: $hcardUrl = $link['href']; @@ -1315,11 +1333,6 @@ class Ostatus_profile extends Memcached_DataObject } } - $hints = array('webfinger' => $addr, - 'profileurl' => $profileUrl, - 'feedurl' => $feedUrl, - 'salmon' => $salmonEndpoint); - if (isset($hcardUrl)) { $hcardHints = self::slurpHcard($hcardUrl); // Note: Webfinger > hcard @@ -1402,7 +1415,7 @@ class Ostatus_profile extends Memcached_DataObject return $oprofile; } - return null; + throw new Exception("Couldn't find a valid profile for '$addr'"); } function saveHTMLFile($title, $rendered) @@ -1492,7 +1505,7 @@ class Ostatus_profile extends Memcached_DataObject if (array_key_exists('url', $hcard)) { if (is_string($hcard['url'])) { $hints['homepage'] = $hcard['url']; - } else if (is_array($hcard['adr'])) { + } else if (is_array($hcard['url'])) { // HACK get the last one; that's how our hcards look $hints['homepage'] = $hcard['url'][count($hcard['url'])-1]; } diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 230d81ba1..fb8c57c71 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -156,18 +156,32 @@ class MagicEnvelope public function verify($env) { if ($env['alg'] != 'RSA-SHA256') { + common_log(LOG_DEBUG, "Salmon error: bad algorithm"); return false; } if ($env['encoding'] != MagicEnvelope::ENCODING) { + common_log(LOG_DEBUG, "Salmon error: bad encoding"); return false; } $text = base64_decode($env['data']); $signer_uri = $this->getAuthor($text); - $verifier = Magicsig::fromString($this->getKeyPair($signer_uri)); + try { + $keypair = $this->getKeyPair($signer_uri); + } catch (Exception $e) { + common_log(LOG_DEBUG, "Salmon error: ".$e->getMessage()); + return false; + } + + $verifier = Magicsig::fromString($keypair); + if (!$verifier) { + common_log(LOG_DEBUG, "Salmon error: unable to parse keypair"); + return false; + } + return $verifier->verify($env['data'], $env['sig']); } diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index 6ca31c485..d1e58f1d6 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -164,46 +164,21 @@ class OStatusQueueHandler extends QueueHandler */ function userFeedForNotice() { - // @fixme this feels VERY hacky... - // should probably be a cleaner way to do it - - ob_start(); - $api = new ApiTimelineUserAction(); - $api->prepare(array('id' => $this->notice->profile_id, - 'format' => 'atom', - 'max_id' => $this->notice->id, - 'since_id' => $this->notice->id - 1)); - $api->showTimeline(); - $feed = ob_get_clean(); - - // ...and override the content-type back to something normal... eww! - // hope there's no other headers that got set while we weren't looking. - header('Content-Type: text/html; charset=utf-8'); - - common_log(LOG_DEBUG, $feed); + $atom = new AtomUserNoticeFeed($this->user); + $atom->addEntryFromNotice($this->notice); + $feed = $atom->getString(); + return $feed; } function groupFeedForNotice($group_id) { - // @fixme this feels VERY hacky... - // should probably be a cleaner way to do it - - ob_start(); - $api = new ApiTimelineGroupAction(); - $args = array('id' => $group_id, - 'format' => 'atom', - 'max_id' => $this->notice->id, - 'since_id' => $this->notice->id - 1); - $api->prepare($args); - $api->handle($args); - $feed = ob_get_clean(); - - // ...and override the content-type back to something normal... eww! - // hope there's no other headers that got set while we weren't looking. - header('Content-Type: text/html; charset=utf-8'); - - common_log(LOG_DEBUG, $feed); + $group = User_group::staticGet('id', $group_id); + + $atom = new AtomGroupNoticeFeed($group); + $atom->addEntryFromNotice($this->notice); + $feed = $atom->getString(); + return $feed; } diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index 85df26c54..aa13ef024 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -57,6 +57,9 @@ class XRD throw new Exception("Invalid XML"); } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); + if (!$xrd_element) { + throw new Exception("Invalid XML, missing XRD root"); + } // Check for host-meta host $host = $xrd_element->getElementsByTagName('Host')->item(0); @@ -149,9 +152,11 @@ class XRD $link['href'] = $element->getAttribute('href'); $link['template'] = $element->getAttribute('template'); foreach ($element->childNodes as $node) { - switch($node->tagName) { - case 'Title': - $link['title'][] = $node->nodeValue; + if ($node instanceof DOMElement) { + switch($node->tagName) { + case 'Title': + $link['title'][] = $node->nodeValue; + } } } diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/lib/xrdaction.php index f574b60ee..6881292ad 100644 --- a/plugins/OStatus/actions/xrd.php +++ b/plugins/OStatus/lib/xrdaction.php @@ -28,32 +28,24 @@ class XrdAction extends Action { public $uri; + + public $user; - function prepare($args) - { - parent::prepare($args); - - $this->uri = $this->trimmed('uri'); - - return true; - } - + public $xrd; + function handle() { - $acct = Discovery::normalize($this->uri); - - $xrd = new XRD(); + $nick = $this->user->nickname; - list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); - $nick = common_canonical_nickname($nick); - - $this->user = User::staticGet('nickname', $nick); - if (!$this->user) { - $this->clientError(_('No such user.'), 404); - return false; + if (empty($this->xrd)) { + $xrd = new XRD(); + } else { + $xrd = $this->xrd; } - $xrd->subject = $this->uri; + if (empty($xrd->subject)) { + $xrd->subject = Discovery::normalize($this->uri); + } $xrd->alias[] = common_profile_url($nick); $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, 'type' => 'text/html', diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css index 13e30ef5d..f7d9853cf 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -41,8 +41,34 @@ min-width:96px; #entity_remote_subscribe { padding:0; float:right; +position:relative; } -#all #entity_remote_subscribe { -margin-top:-52px; +.section .entity_actions { +margin-bottom:0; +} + +#entity_remote_subscribe .dialogbox { +width:405px; +} + +.aside #entity_subscriptions .more, +.aside #entity_groups .more { +float:left; +} + +.section #entity_remote_subscribe { +border:0; +} + +.section .entity_remote_subscribe { +color:#002FA7; +box-shadow:none; +-moz-box-shadow:none; +-webkit-box-shadow:none; +background-color:transparent; +background-position:0 -1183px; +padding:0 0 0 23px; +border:0; + } diff --git a/plugins/TwitterBridge/README b/plugins/TwitterBridge/README index 72278b32e..d0d34b7ef 100644 --- a/plugins/TwitterBridge/README +++ b/plugins/TwitterBridge/README @@ -20,7 +20,7 @@ on Twitter (http://twitter.com/apps). During the application registration process your application will be assigned a "consumer" key and secret, which the plugin will use to make OAuth requests to Twitter. You can either pass the consumer key and secret in when you enable the -plugin, or set it using the Twitter administration panel. +plugin, or set it using the Twitter administration panel**. When registering your application with Twitter set the type to "Browser" and your Callback URL to: @@ -42,11 +42,26 @@ To enable the plugin, add the following to your config.php: ) ); +or just: + + addPlugin('TwitterBridge'); + +if you want to set the consumer key and secret from the Twitter bridge +administration panel. (The Twitter bridge wont work at all +unless you configure it with a consumer key and secret.) + * Note: The plugin will still push notices to Twitter for users who have previously set up the Twitter bridge using their Twitter name and password under an older version of StatusNet, but all new Twitter bridge connections will use OAuth. +** For multi-site setups you can also set a global consumer key and + secret. The Twitter bridge will fall back on the global key pair if + it can't find a local pair, e.g.: + + $config['twitter']['global_consumer_key'] = 'YOUR_CONSUMER_KEY' + $config['twitter']['global_consumer_secret'] = 'YOUR_CONSUMER_SECRET' + Administration panel -------------------- diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 6ce69d5e2..1a0a69682 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -80,6 +80,30 @@ class TwitterBridgePlugin extends Plugin } /** + * Check to see if there is a consumer key and secret defined + * for Twitter integration. + * + * @return boolean result + */ + + static function hasKeys() + { + $ckey = common_config('twitter', 'consumer_key'); + $csecret = common_config('twitter', 'consumer_secret'); + + if (empty($ckey) && empty($csecret)) { + $ckey = common_config('twitter', 'global_consumer_key'); + $csecret = common_config('twitter', 'global_consumer_secret'); + } + + if (!empty($ckey) && !empty($csecret)) { + return true; + } + + return false; + } + + /** * Add Twitter-related paths to the router table * * Hook for RouterInitialized event. @@ -91,18 +115,26 @@ class TwitterBridgePlugin extends Plugin function onRouterInitialized($m) { - $m->connect( - 'twitter/authorization', - array('action' => 'twitterauthorization') - ); - $m->connect('settings/twitter', array('action' => 'twittersettings')); + $m->connect('admin/twitter', array('action' => 'twitteradminpanel')); - if (common_config('twitter', 'signin')) { - $m->connect('main/twitterlogin', array('action' => 'twitterlogin')); + if (self::hasKeys()) { + $m->connect( + 'twitter/authorization', + array('action' => 'twitterauthorization') + ); + $m->connect( + 'settings/twitter', array( + 'action' => 'twittersettings' + ) + ); + if (common_config('twitter', 'signin')) { + $m->connect( + 'main/twitterlogin', + array('action' => 'twitterlogin') + ); + } } - $m->connect('admin/twitter', array('action' => 'twitteradminpanel')); - return true; } @@ -117,7 +149,7 @@ class TwitterBridgePlugin extends Plugin { $action_name = $action->trimmed('action'); - if (common_config('twitter', 'signin')) { + if (self::hasKeys() && common_config('twitter', 'signin')) { $action->menuItem( common_local_url('twitterlogin'), _m('Twitter'), @@ -138,15 +170,16 @@ class TwitterBridgePlugin extends Plugin */ function onEndConnectSettingsNav(&$action) { - $action_name = $action->trimmed('action'); - - $action->menuItem( - common_local_url('twittersettings'), - _m('Twitter'), - _m('Twitter integration options'), - $action_name === 'twittersettings' - ); + if (self::hasKeys()) { + $action_name = $action->trimmed('action'); + $action->menuItem( + common_local_url('twittersettings'), + _m('Twitter'), + _m('Twitter integration options'), + $action_name === 'twittersettings' + ); + } return true; } @@ -188,12 +221,12 @@ class TwitterBridgePlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - // Avoid a possible loop - - if ($notice->source != 'twitter') { - array_push($transports, 'twitter'); + if (self::hasKeys()) { + // Avoid a possible loop + if ($notice->source != 'twitter') { + array_push($transports, 'twitter'); + } } - return true; } @@ -206,18 +239,19 @@ class TwitterBridgePlugin extends Plugin */ function onGetValidDaemons($daemons) { - array_push( - $daemons, - INSTALLDIR - . '/plugins/TwitterBridge/daemons/synctwitterfriends.php' - ); - - if (common_config('twitterimport', 'enabled')) { + if (self::hasKeys()) { array_push( $daemons, INSTALLDIR - . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php' + . '/plugins/TwitterBridge/daemons/synctwitterfriends.php' ); + if (common_config('twitterimport', 'enabled')) { + array_push( + $daemons, + INSTALLDIR + . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php' + ); + } } return true; @@ -232,7 +266,9 @@ class TwitterBridgePlugin extends Plugin */ function onEndInitializeQueueManager($manager) { - $manager->connect('twitter', 'TwitterQueueHandler'); + if (self::hasKeys()) { + $manager->connect('twitter', 'TwitterQueueHandler'); + } return true; } diff --git a/plugins/TwitterBridge/twitteradminpanel.php b/plugins/TwitterBridge/twitteradminpanel.php index b22e6d99f..a78a92c66 100644 --- a/plugins/TwitterBridge/twitteradminpanel.php +++ b/plugins/TwitterBridge/twitteradminpanel.php @@ -225,6 +225,15 @@ class TwitterAdminPanelForm extends AdminForm ); $this->unli(); + $globalConsumerKey = common_config('twitter', 'global_consumer_key'); + $globalConsumerSec = common_config('twitter', 'global_consumer_secret'); + + if (!empty($globalConsumerKey) && !empty($globalConsumerSec)) { + $this->li(); + $this->out->element('p', 'form_guide', _('Note: a global consumer key and secret are set.')); + $this->unli(); + } + $this->li(); $this->input( 'source', diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php index ba45b533d..93f6aadd1 100644 --- a/plugins/TwitterBridge/twitteroauthclient.php +++ b/plugins/TwitterBridge/twitteroauthclient.php @@ -22,7 +22,7 @@ * @category Integration * @package StatusNet * @author Zach Copley <zach@status.net> - * @copyright 2009 StatusNet, Inc. + * @copyright 2009-2010 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -61,8 +61,23 @@ class TwitterOAuthClient extends OAuthClient $consumer_key = common_config('twitter', 'consumer_key'); $consumer_secret = common_config('twitter', 'consumer_secret'); - parent::__construct($consumer_key, $consumer_secret, - $oauth_token, $oauth_token_secret); + if (empty($consumer_key) && empty($consumer_secret)) { + $consumer_key = common_config( + 'twitter', + 'global_consumer_key' + ); + $consumer_secret = common_config( + 'twitter', + 'global_consumer_secret' + ); + } + + parent::__construct( + $consumer_key, + $consumer_secret, + $oauth_token, + $oauth_token_secret + ); } // XXX: the following two functions are to support the horrible hack diff --git a/tests/UserFeedParseTest.php b/tests/UserFeedParseTest.php new file mode 100644 index 000000000..b3f9a6417 --- /dev/null +++ b/tests/UserFeedParseTest.php @@ -0,0 +1,131 @@ +<?php + +if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + print "This script must be run from the command line\n"; + exit(); +} + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); +define('STATUSNET', true); + +require_once INSTALLDIR . '/lib/common.php'; + +class UserFeedParseTests extends PHPUnit_Framework_TestCase +{ + public function testFeed1() + { + global $_testfeed1; + $dom = DOMDocument::loadXML($_testfeed1); + $this->assertFalse(empty($dom)); + + $entries = $dom->getElementsByTagName('entry'); + + $entry1 = $entries->item(0); + $this->assertFalse(empty($entry1)); + + $feedEl = $dom->getElementsByTagName('feed')->item(0); + $this->assertFalse(empty($feedEl)); + + // Test actor (from activity:subject) + + $act1 = new Activity($entry1, $feedEl); + $this->assertFalse(empty($act1)); + $this->assertFalse(empty($act1->actor)); + $this->assertEquals($act1->actor->type, ActivityObject::PERSON); + $this->assertEquals($act1->actor->title, 'Zach Copley'); + $this->assertEquals($act1->actor->id, 'http://localhost/statusnet/user/1'); + $this->assertEquals($act1->actor->link, 'http://localhost/statusnet/zach'); + + $avatars = $act1->actor->avatarLinks; + + $this->assertEquals( + $avatars[0]->url, + 'http://localhost/statusnet/theme/default/default-avatar-profile.png' + ); + + $this->assertEquals( + $avatars[1]->url, + 'http://localhost/statusnet/theme/default/default-avatar-stream.png' + ); + + $this->assertEquals( + $avatars[2]->url, + 'http://localhost/statusnet/theme/default/default-avatar-mini.png' + ); + + $this->assertEquals($act1->actor->displayName, 'Zach Copley'); + + $poco = $act1->actor->poco; + $this->assertEquals($poco->preferredUsername, 'zach'); + $this->assertEquals($poco->address->formatted, 'El Cerrito, CA'); + $this->assertEquals($poco->urls[0]->type, 'homepage'); + $this->assertEquals($poco->urls[0]->value, 'http://zach.copley.name'); + $this->assertEquals($poco->urls[0]->primary, 'true'); + $this->assertEquals($poco->note, 'Zach Hack Attack'); + + // test the post + + //var_export($act1); + $this->assertEquals($act1->object->type, 'http://activitystrea.ms/schema/1.0/note'); + $this->assertEquals($act1->object->title, 'And now for something completely insane...'); + + $this->assertEquals($act1->object->content, 'And now for something completely insane...'); + $this->assertEquals($act1->object->id, 'http://localhost/statusnet/notice/3'); + + } + +} + +$_testfeed1 = <<<TESTFEED1 +<?xml version="1.0" encoding="UTF-8"?> +<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0"> + <id>http://localhost/statusnet/api/statuses/user_timeline/1.atom</id> + <title>zach timeline</title> + <subtitle>Updates from zach on Zach Dev!</subtitle> + <logo>http://localhost/statusnet/theme/default/default-avatar-profile.png</logo> + <updated>2010-03-04T01:41:14+00:00</updated> +<author> + <name>zach</name> + <uri>http://localhost/statusnet/user/1</uri> + +</author> + <link href="http://localhost/statusnet/zach" rel="alternate" type="text/html"/> + <link href="http://localhost/statusnet/main/sup#1" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/> + <link href="http://localhost/statusnet/main/push/hub" rel="hub"/> + <link href="http://localhost/statusnet/main/salmon/user/1" rel="http://salmon-protocol.org/ns/salmon-replies"/> + <link href="http://localhost/statusnet/main/salmon/user/1" rel="http://salmon-protocol.org/ns/salmon-mention"/> + <link href="http://localhost/statusnet/api/statuses/user_timeline/1.atom" rel="self" type="application/atom+xml"/> +<activity:subject> + <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> + <id>http://localhost/statusnet/user/1</id> + <title>Zach Copley</title> + <link rel="alternate" type="text/html" href="http://localhost/statusnet/zach"/> + <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://localhost/statusnet/theme/default/default-avatar-profile.png"/> + <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://localhost/statusnet/theme/default/default-avatar-stream.png"/> + <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://localhost/statusnet/theme/default/default-avatar-mini.png"/> + +<poco:preferredUsername>zach</poco:preferredUsername> +<poco:displayName>Zach Copley</poco:displayName> +<poco:note>Zach Hack Attack</poco:note> +<poco:address> + <poco:formatted>El Cerrito, CA</poco:formatted> +</poco:address> +<poco:urls> + <poco:type>homepage</poco:type> + <poco:value>http://zach.copley.name</poco:value> + <poco:primary>true</poco:primary> + +</poco:urls> +</activity:subject> +<entry> + <title>And now for something completely insane...</title> + <link rel="alternate" type="text/html" href="http://localhost/statusnet/notice/3"/> + <id>http://localhost/statusnet/notice/3</id> + <published>2010-03-04T01:41:07+00:00</published> + <updated>2010-03-04T01:41:07+00:00</updated> + <link rel="ostatus:conversation" href="http://localhost/statusnet/conversation/3"/> + <content type="html">And now for something completely insane...</content> +</entry> + +</feed> +TESTFEED1; diff --git a/theme/base/css/display.css b/theme/base/css/display.css index f32c57ea4..01d5dd134 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -452,6 +452,13 @@ width:100%; float:left; } +#content.admin { +width:95.5%; +} +#content.admin #content_inner { +width:66.3%; +} + #aside_primary { width:27.917%; min-height:259px; @@ -764,10 +771,12 @@ display:none; text-align:center; } -.entity_moderation { +.entity_moderation, +.entity_role { position:relative; } -.entity_moderation p { +.entity_moderation p, +.entity_role p { border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; @@ -775,13 +784,14 @@ font-weight:bold; padding-bottom:2px; margin-bottom:7px; } -.entity_moderation ul { +.entity_moderation ul, +.entity_role ul { display:none; } -.entity_moderation:hover ul { +.entity_moderation:hover ul, +.entity_role:hover ul { display:block; -min-width:21%; -width:100%; +width:110%; padding:11px; position:absolute; top:-1px; diff --git a/theme/base/images/icons/README b/theme/base/images/icons/README index ea582149f..f701959ca 100644 --- a/theme/base/images/icons/README +++ b/theme/base/images/icons/README @@ -45,6 +45,11 @@ White pin with green background White underscore with green background White C with green background + White magic wand with green background + Green badge with white background + Green sandbox with white background + Green speech bubble broken with white background + Green person with tie with white background */ Created by various authors diff --git a/theme/base/images/icons/icons-01.gif b/theme/base/images/icons/icons-01.gif Binary files differindex be884ff48..bf0f1230e 100644 --- a/theme/base/images/icons/icons-01.gif +++ b/theme/base/images/icons/icons-01.gif diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 8ae2b4014..be341813a 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -49,6 +49,7 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_next a, .form_settings fieldset fieldset, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { border-color:#DDDDDD; } @@ -67,6 +68,7 @@ input.submit, .entity_actions a, .entity_actions input, .entity_moderation p, +.entity_role p, button { box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3); -moz-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3); @@ -127,7 +129,8 @@ a, .notice-options input, .entity_actions a, .entity_actions input, -.entity_moderation p { +.entity_moderation p, +.entity_role p { color:#002FA7; } @@ -190,6 +193,9 @@ button.close, .entity_sandbox input.submit, .entity_silence input.submit, .entity_delete input.submit, +.entity_role p, +.entity_role_administrator input.submit, +.entity_role_moderator input.submit, .notice-options .repeated, .form_notice label[for=notice_data-geo], button.minimize, @@ -229,6 +235,7 @@ border-color:transparent; #site_nav_local_views .current a, .entity_send-a-message .form_notice, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { background-color:#FFFFFF; } @@ -319,6 +326,7 @@ background-position: 5px -852px; } .entity_send-a-message .form_notice, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); @@ -350,6 +358,27 @@ background-position: 5px -1445px; .entity_delete input.submit { background-position: 5px -1511px; } +.entity_sandbox .form_user_unsandbox input.submit { +background-position: 5px -2568px; +} +.entity_silence .form_user_unsilence input.submit { +background-position: 5px -2633px; +} +.entity_role p { +background-position: 5px -2436px; +} +.entity_role_administrator .form_user_grantrole input.submit { +background-position: 5px -983px; +} +.entity_role_moderator .form_user_grantrole input.submit { +background-position: 5px -1313px; +} +.entity_role_administrator .form_user_revokerole input.submit { +background-position: 5px -2699px; +} +.entity_role_moderator .form_user_revokerole input.submit { +background-position: 5px -2501px; +} .form_reset_key input.submit { background-position: 5px -1973px; } diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 737e3a103..db85408eb 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -49,6 +49,7 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3); .pagination .nav_next a, .form_settings fieldset fieldset, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { border-color:#DDDDDD; } @@ -67,6 +68,7 @@ input.submit, .entity_actions a, .entity_actions input, .entity_moderation p, +.entity_role p, button { box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3); -moz-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3); @@ -128,7 +130,8 @@ a, .notice-options input, .entity_actions a, .entity_actions input, -.entity_moderation p { +.entity_moderation p, +.entity_role p { color:#002FA7; } @@ -191,6 +194,9 @@ button.close, .entity_sandbox input.submit, .entity_silence input.submit, .entity_delete input.submit, +.entity_role p, +.entity_role_administrator input.submit, +.entity_role_moderator input.submit, .notice-options .repeated, .form_notice label[for=notice_data-geo], button.minimize, @@ -230,6 +236,7 @@ border-color:transparent; #site_nav_local_views .current a, .entity_send-a-message .form_notice, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { background-color:#FFFFFF; } @@ -319,6 +326,7 @@ background-position: 5px -852px; } .entity_send-a-message .form_notice, .entity_moderation:hover ul, +.entity_role:hover ul, .dialogbox { box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); -moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7); @@ -350,6 +358,27 @@ background-position: 5px -1445px; .entity_delete input.submit { background-position: 5px -1511px; } +.entity_sandbox .form_user_unsandbox input.submit { +background-position: 5px -2568px; +} +.entity_silence .form_user_unsilence input.submit { +background-position: 5px -2633px; +} +.entity_role p { +background-position: 5px -2436px; +} +.entity_role_administrator .form_user_grantrole input.submit { +background-position: 5px -983px; +} +.entity_role_moderator .form_user_grantrole input.submit { +background-position: 5px -1313px; +} +.entity_role_administrator .form_user_revokerole input.submit { +background-position: 5px -2699px; +} +.entity_role_moderator .form_user_revokerole input.submit { +background-position: 5px -2501px; +} .form_reset_key input.submit { background-position: 5px -1973px; } |