diff options
author | Brion Vibber <brion@pobox.com> | 2010-03-04 07:00:45 -0800 |
---|---|---|
committer | Brion Vibber <brion@pobox.com> | 2010-03-04 07:00:45 -0800 |
commit | 62d5f1addb1b710a387f7732ace1dd33bd1b86c1 (patch) | |
tree | df1d6d402dd057c330b0eb71b803d595c024d476 | |
parent | 0f1c6e239acbf2c024c3aae9819d891f02b05bfd (diff) | |
parent | 9f861e9d895325adbb2dc7f1d540a442be2c1b2f (diff) |
Merge branch 'testing'
43 files changed, 1329 insertions, 351 deletions
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 e30a08fb5..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/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/siteadminpanel.php b/actions/siteadminpanel.php index 4b29819b7..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))) { @@ -285,32 +267,6 @@ class SiteAdminPanelForm extends AdminForm $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 index 613a2e96b..3931aa982 100644 --- a/actions/sitenoticeadminpanel.php +++ b/actions/sitenoticeadminpanel.php @@ -99,7 +99,7 @@ class SitenoticeadminpanelAction extends AdminPanelAction $result = Config::save('site', 'notice', $siteNotice); - if (!result) { + if (!$result) { $this->ServerError(_("Unable to 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/classes/Notice.php b/classes/Notice.php index 97cb3b8fb..4c7e6ab4b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1106,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(); @@ -1151,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 9c2fa7a0c..0322c9358 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -743,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/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/lib/activity.php b/lib/activity.php index 7334239ec..2cb80f9e1 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -1064,6 +1064,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); diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php index eb622871e..d1aab3dfc 100644 --- a/lib/adminpanelaction.php +++ b/lib/adminpanelaction.php @@ -381,6 +381,11 @@ class AdminPanelNav extends Widget _('Edit site notice'), $action_name == 'sitenoticeadminpanel', 'nav_sitenotice_admin_panel'); } + if (AdminPanelAction::canAdmin('snapshot')) { + $this->out->menuItem(common_local_url('snapshotadminpanel'), _('Snapshots'), + _('Snapshots configuration'), $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel'); + } + Event::handle('EndAdminPanelNav', array($this)); } $this->action->elementEnd('ul'); 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/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/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 7e8e22a7d..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', @@ -650,6 +651,7 @@ class Router $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/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index cc7e75976..ad4f61389 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -44,7 +44,9 @@ 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', @@ -111,7 +113,7 @@ 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"'); 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 index 4fcd0eb39..f325ba053 100644 --- a/plugins/OStatus/actions/ostatusgroup.php +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -72,9 +72,9 @@ class OStatusGroupAction extends OStatusSubAction $this->elementStart('ul', 'form_data'); $this->elementStart('li'); $this->input('profile', - _m('Group profile URL'), + _m('Join group'), $this->profile_uri, - _m('Enter the profile URL of a group on another StatusNet site')); + _m("OStatus group's address, like http://example.net/group/nickname")); $this->elementEnd('li'); $this->elementEnd('ul'); diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 542f7e20c..65dee2392 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -62,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'); @@ -244,25 +244,33 @@ class OStatusSubAction extends Action } 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; @@ -315,7 +323,6 @@ class OStatusSubAction extends Action if ($this->pullRemoteProfile()) { $this->validateRemoteProfile(); } - return true; } @@ -391,7 +398,7 @@ class OStatusSubAction extends Action function title() { // TRANS: Page title for OStatus remote subscription form - return _m('Authorize subscription'); + return _m('Confirm'); } /** 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 7ab031aa5..fcca1a252 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1267,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 @@ -1275,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)) { @@ -1299,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']; @@ -1323,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 @@ -1410,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) 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 f00e1f809..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); 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 ac668623d..f7d9853cf 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -52,7 +52,8 @@ margin-bottom:0; width:405px; } -.aside #entity_subscriptions .more { +.aside #entity_subscriptions .more, +.aside #entity_groups .more { float:left; } 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 b8dd561cc..01d5dd134 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -771,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; @@ -782,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; } |